Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Post History
You can replace it with a look-up table ("LUT"). There's a common manual optimization technique which we had to use a lot back in the days. For any switch applied to an enum where the enum values ...
#3: Post edited
- You can replace it with a look-up table ("LUT").
- There's a common manual optimization technique which we had to use a lot back in the days. For any `switch` applied to an `enum` where the `enum` values are in sequence (ideally from 0 to _n_, or at least adjacent), it can be replaced with a branch-free look-up table.
- That is, instead of a `switch` or a long `if else if`, you can do a single look-up table check. That is, if we take that `enum` from my Q&A and modify it:
- ```c
- typedef enum
- {
- COFFEE_OK,
- COFFEE_IDLE,
- COFFEE_NO_BEANS,
- COFFEE_NO_WATER,
- COFFEE_UNEXPECTED,
- COFFEE_N // lets add this one to the end
- } coffee_result_t;
- ```
- (Or in your case I guess the name might be `OPCODE_N`.)
- Then for a sequential enum `COFFEE_N` equals the number of enumerations supported.
- Similarly as I designed a function-pointer jump table in that Q&A, we can also do a data look-up table in that same manner. Create an array (in flash) that contains the outcome for all possible scenarios pre-programmed. Then we don't have to check it in run-time.
- ```c
- static const uint8_t OPCODE_results [OPCODE_N] =
- {
- [OPCODE_STATUS] = CMD_REQUEST,
- };
- ```
- Done. Huh how can this tiny bit code replace that whole `switch`/`if else if`?
- In that Q&A, I mentioned that having the "OK" value as number zero is a great idea because that's the default implicit initializer.
- > What's important here is that the first state in an enum like this should correspond to OK/no errors, with the value 0. This is also a de facto standard way to write programs, so that any non-zero value corresponds to a specialized status/error code.
- And so we see why that is so nice - now we _don't_ have to write all of this code:
- ```c
- [OPCODE_CHANNEL_ON] = CMD_OK,
- [OPCODE_CHANNEL_OFF] = CMD_OK,
- [OPCODE_RELAY_ON] = CMD_OK,
- [OPCODE_RELAY_OFF] = CMD_OK,
- ```
- Because those are already set to zero by the default initialization of `OPCODE_results` and zero ought to equal `CMD_OK`. So we just have to create a look-up table where we explicitly initialize all the "not OK" scenarios.
- And with that done, we can replace the whole `switch` with a simple
- ```c
- return OPCODE_results[received_opcode];
- ```
- where `received_opcode` is assumed to be verified and sanitized in advance. This is pretty much entirely branch-free and if you keep reusing that same `OPCODE_results[received_opcode]` multiple times inside a function without changing `received_opcode`, then it will likely just get stored in a register. So if you check it twice - no biggie.
- ---
- _However_, please note that modern optimizing compilers actually do this very optimization internally and probably go further still. So you shouldn't replace `switch` with a look-up table to increase performance, but rather to make the code more readable and compact. Which may or may not be the case.
- Also, a MCU which controls a bunch of relays doesn't sound like anything high-end with cache and branch prediction, so you probably don't need to take branching in consideration. But compile-time calculations over run-time do save some CPU no matter what MCU that is used.
- `switch` does have the advantage of `default` in case you do want some defensive programming there even when the data is sanitized. Defensive programming is encouraged by MISRA C - always have a trailing `else` after `if else if` and a trailing `default` in `switch`. The equivalent of that in the look-up table version is the check that the `received_opcode` is in range of 0 to `OPCODE_N`.
- You can replace it with a look-up table ("LUT").
- There's a common manual optimization technique which we had to use a lot back in the days. For any `switch` applied to an `enum` where the `enum` values are in sequence (ideally from 0 to _n_, or at least adjacent), it can be replaced with a branch-free look-up table.
- That is, instead of a `switch` or a long `if else if`, you can do a single look-up table check. That is, if we take that `enum` from my Q&A and modify it:
- ```c
- typedef enum
- {
- COFFEE_OK,
- COFFEE_IDLE,
- COFFEE_NO_BEANS,
- COFFEE_NO_WATER,
- COFFEE_UNEXPECTED,
- COFFEE_N // lets add this one to the end
- } coffee_result_t;
- ```
- (Or in your case I guess the name might be `OPCODE_N`.)
- Then for a sequential enum `COFFEE_N` equals the number of enumerations supported.
- Similarly as I designed a function-pointer jump table in that Q&A, we can also do a data look-up table in that same manner. Create an array (in flash) that contains the outcome for all possible scenarios pre-programmed. Then we don't have to check it in run-time.
- ```c
- static const uint8_t OPCODE_results [OPCODE_N] =
- {
- [OPCODE_STATUS] = CMD_REQUEST,
- };
- ```
- Done. Huh how can this tiny bit code replace that whole `switch`/`if else if`?
- In that Q&A, I mentioned that having the "OK" value as number zero is a great idea because that's the default implicit initializer.
- > What's important here is that the first state in an enum like this should correspond to OK/no errors, with the value 0. This is also a de facto standard way to write programs, so that any non-zero value corresponds to a specialized status/error code.
- And so we see why that is so nice - now we _don't_ have to write all of this code:
- ```c
- [OPCODE_CHANNEL_ON] = CMD_OK,
- [OPCODE_CHANNEL_OFF] = CMD_OK,
- [OPCODE_RELAY_ON] = CMD_OK,
- [OPCODE_RELAY_OFF] = CMD_OK,
- ```
- Because those are already set to zero by the default initialization of `OPCODE_results` and zero ought to equal `CMD_OK`. So we just have to create a look-up table where we explicitly initialize all the "not OK" scenarios.
- And with that done, we can replace the whole `switch` with a simple
- ```c
- return OPCODE_results[received_opcode];
- ```
- where `received_opcode` is assumed to be verified and sanitized in advance. This is pretty much entirely branch-free and if you keep reusing that same `OPCODE_results[received_opcode]` multiple times inside a function without changing `received_opcode`, then it will likely just get stored in a register. So if you check it twice - no biggie.
- But obviously look-up tables are execution speed over memory use optimizations. But for MCUs you generally want to do execution speed > RAM use > flash use, where you got a ton of flash anyway.
- ---
- _However_, please note that modern optimizing compilers actually do this very optimization internally and probably go further still. So you shouldn't replace `switch` with a look-up table to increase performance, but rather to make the code more readable and compact. Which may or may not be the case.
- Also, a MCU which controls a bunch of relays doesn't sound like anything high-end with cache and branch prediction, so you probably don't need to take branching in consideration. But compile-time calculations over run-time do save some CPU no matter what MCU that is used.
- `switch` does have the advantage of `default` in case you do want some defensive programming there even when the data is sanitized. Defensive programming is encouraged by MISRA C - always have a trailing `else` after `if else if` and a trailing `default` in `switch`. The equivalent of that in the look-up table version is the check that the `received_opcode` is in range of 0 to `OPCODE_N`.
#2: Post edited
- You can replace it with a look-up table ("LUT").
- There's a common manual optimization technique which we had to use a lot back in the days. For any `switch` applied to an `enum` where the `enum` values are in sequence (ideally from 0 to _n_, or at least adjacent), it can be replaced with a branch-free look-up table.
- That is, instead of a `switch` or a long `if else if`, you can do a single look-up table check. That is, if we take that `enum` from my Q&A and modify it:
- ```c
- typedef enum
- {
- COFFEE_OK,
- COFFEE_IDLE,
- COFFEE_NO_BEANS,
- COFFEE_NO_WATER,
- COFFEE_UNEXPECTED,
- COFFEE_N // lets add this one to the end
- } coffee_result_t;
- ```
- (Or in your case I guess the name might be `OPCODE_N`.)
- Then for a sequential enum `COFFEE_N` equals the number of enumerations supported.
- Similarly as I designed a function-pointer jump table in that Q&A, we can also do a data look-up table in that same manner. Create an array (in flash) that contains the outcome for all possible scenarios pre-programmed. Then we don't have to check it in run-time.
- ```c
- static const uint8_t OPCODE_results [OPCODE_N] =
- {
- [OPCODE_STATUS] = CMD_REQUEST,
- };
- ```
- Done. Huh how can this tiny bit code replace that whole `switch`/`if else if`?
- In that Q&A, I mentioned that having the "OK" value as number zero is a great idea because that's the default implicit initializer.
- > What's important here is that the first state in an enum like this should correspond to OK/no errors, with the value 0. This is also a de facto standard way to write programs, so that any non-zero value corresponds to a specialized status/error code.
- And so we see why that is so nice - now we _don't_ have to write all of this code:
- ```c
- [OPCODE_CHANNEL_ON] = CMD_OK,
- [OPCODE_CHANNEL_OFF] = CMD_OK,
- [OPCODE_RELAY_ON] = CMD_OK,
- [OPCODE_RELAY_OFF] = CMD_OK,
- ```
- Because those are already set to zero by the default initialization of `OPCODE_results` and zero ought to equal `CMD_OK`. So we just have to create a look-up table where we explicitly initialize all the "not OK" scenarios.
- And with that done, we can replace the whole `switch` with a simple
- ```c
- return OPCODE_results[received_opcode];
- ```
where `received_opcode` is assumed to be verified and sanitized in advance. This is pretty much entirely branch-free and if you keep reusing that same `OPCODE_results[received_opcode]` multiple times inside a function without changing `received_opcode`, then it will likely just get stored in a register.- ---
- _However_, please note that modern optimizing compilers actually do this very optimization internally and probably go further still. So you shouldn't replace `switch` with a look-up table to increase performance, but rather to make the code more readable and compact. Which may or may not be the case.
- Also, a MCU which controls a bunch of relays doesn't sound like anything high-end with cache and branch prediction, so you probably don't need to take branching in consideration. But compile-time calculations over run-time do save some CPU no matter what MCU that is used.
- `switch` does have the advantage of `default` in case you do want some defensive programming there even when the data is sanitized. Defensive programming is encouraged by MISRA C - always have a trailing `else` after `if else if` and a trailing `default` in `switch`. The equivalent of that in the look-up table version is the check that the `received_opcode` is in range of 0 to `OPCODE_N`.
- You can replace it with a look-up table ("LUT").
- There's a common manual optimization technique which we had to use a lot back in the days. For any `switch` applied to an `enum` where the `enum` values are in sequence (ideally from 0 to _n_, or at least adjacent), it can be replaced with a branch-free look-up table.
- That is, instead of a `switch` or a long `if else if`, you can do a single look-up table check. That is, if we take that `enum` from my Q&A and modify it:
- ```c
- typedef enum
- {
- COFFEE_OK,
- COFFEE_IDLE,
- COFFEE_NO_BEANS,
- COFFEE_NO_WATER,
- COFFEE_UNEXPECTED,
- COFFEE_N // lets add this one to the end
- } coffee_result_t;
- ```
- (Or in your case I guess the name might be `OPCODE_N`.)
- Then for a sequential enum `COFFEE_N` equals the number of enumerations supported.
- Similarly as I designed a function-pointer jump table in that Q&A, we can also do a data look-up table in that same manner. Create an array (in flash) that contains the outcome for all possible scenarios pre-programmed. Then we don't have to check it in run-time.
- ```c
- static const uint8_t OPCODE_results [OPCODE_N] =
- {
- [OPCODE_STATUS] = CMD_REQUEST,
- };
- ```
- Done. Huh how can this tiny bit code replace that whole `switch`/`if else if`?
- In that Q&A, I mentioned that having the "OK" value as number zero is a great idea because that's the default implicit initializer.
- > What's important here is that the first state in an enum like this should correspond to OK/no errors, with the value 0. This is also a de facto standard way to write programs, so that any non-zero value corresponds to a specialized status/error code.
- And so we see why that is so nice - now we _don't_ have to write all of this code:
- ```c
- [OPCODE_CHANNEL_ON] = CMD_OK,
- [OPCODE_CHANNEL_OFF] = CMD_OK,
- [OPCODE_RELAY_ON] = CMD_OK,
- [OPCODE_RELAY_OFF] = CMD_OK,
- ```
- Because those are already set to zero by the default initialization of `OPCODE_results` and zero ought to equal `CMD_OK`. So we just have to create a look-up table where we explicitly initialize all the "not OK" scenarios.
- And with that done, we can replace the whole `switch` with a simple
- ```c
- return OPCODE_results[received_opcode];
- ```
- where `received_opcode` is assumed to be verified and sanitized in advance. This is pretty much entirely branch-free and if you keep reusing that same `OPCODE_results[received_opcode]` multiple times inside a function without changing `received_opcode`, then it will likely just get stored in a register. So if you check it twice - no biggie.
- ---
- _However_, please note that modern optimizing compilers actually do this very optimization internally and probably go further still. So you shouldn't replace `switch` with a look-up table to increase performance, but rather to make the code more readable and compact. Which may or may not be the case.
- Also, a MCU which controls a bunch of relays doesn't sound like anything high-end with cache and branch prediction, so you probably don't need to take branching in consideration. But compile-time calculations over run-time do save some CPU no matter what MCU that is used.
- `switch` does have the advantage of `default` in case you do want some defensive programming there even when the data is sanitized. Defensive programming is encouraged by MISRA C - always have a trailing `else` after `if else if` and a trailing `default` in `switch`. The equivalent of that in the look-up table version is the check that the `received_opcode` is in range of 0 to `OPCODE_N`.
#1: Initial revision
You can replace it with a look-up table ("LUT").
There's a common manual optimization technique which we had to use a lot back in the days. For any `switch` applied to an `enum` where the `enum` values are in sequence (ideally from 0 to _n_, or at least adjacent), it can be replaced with a branch-free look-up table.
That is, instead of a `switch` or a long `if else if`, you can do a single look-up table check. That is, if we take that `enum` from my Q&A and modify it:
```c
typedef enum
{
COFFEE_OK,
COFFEE_IDLE,
COFFEE_NO_BEANS,
COFFEE_NO_WATER,
COFFEE_UNEXPECTED,
COFFEE_N // lets add this one to the end
} coffee_result_t;
```
(Or in your case I guess the name might be `OPCODE_N`.)
Then for a sequential enum `COFFEE_N` equals the number of enumerations supported.
Similarly as I designed a function-pointer jump table in that Q&A, we can also do a data look-up table in that same manner. Create an array (in flash) that contains the outcome for all possible scenarios pre-programmed. Then we don't have to check it in run-time.
```c
static const uint8_t OPCODE_results [OPCODE_N] =
{
[OPCODE_STATUS] = CMD_REQUEST,
};
```
Done. Huh how can this tiny bit code replace that whole `switch`/`if else if`?
In that Q&A, I mentioned that having the "OK" value as number zero is a great idea because that's the default implicit initializer.
> What's important here is that the first state in an enum like this should correspond to OK/no errors, with the value 0. This is also a de facto standard way to write programs, so that any non-zero value corresponds to a specialized status/error code.
And so we see why that is so nice - now we _don't_ have to write all of this code:
```c
[OPCODE_CHANNEL_ON] = CMD_OK,
[OPCODE_CHANNEL_OFF] = CMD_OK,
[OPCODE_RELAY_ON] = CMD_OK,
[OPCODE_RELAY_OFF] = CMD_OK,
```
Because those are already set to zero by the default initialization of `OPCODE_results` and zero ought to equal `CMD_OK`. So we just have to create a look-up table where we explicitly initialize all the "not OK" scenarios.
And with that done, we can replace the whole `switch` with a simple
```c
return OPCODE_results[received_opcode];
```
where `received_opcode` is assumed to be verified and sanitized in advance. This is pretty much entirely branch-free and if you keep reusing that same `OPCODE_results[received_opcode]` multiple times inside a function without changing `received_opcode`, then it will likely just get stored in a register.
---
_However_, please note that modern optimizing compilers actually do this very optimization internally and probably go further still. So you shouldn't replace `switch` with a look-up table to increase performance, but rather to make the code more readable and compact. Which may or may not be the case.
Also, a MCU which controls a bunch of relays doesn't sound like anything high-end with cache and branch prediction, so you probably don't need to take branching in consideration. But compile-time calculations over run-time do save some CPU no matter what MCU that is used.
`switch` does have the advantage of `default` in case you do want some defensive programming there even when the data is sanitized. Defensive programming is encouraged by MISRA C - always have a trailing `else` after `if else if` and a trailing `default` in `switch`. The equivalent of that in the look-up table version is the check that the `received_opcode` is in range of 0 to `OPCODE_N`.
