I’m a bit late to the party because I’ve been ill the last few days.
I don’t have a definite answer on why changing the ordering affects the generated code size, but I have two theories:
Theory 1: Putting the statements in ascending order makes the compiler more likely to generate a jump table because of the way switch
statements are defined in C and C++.
Theory 2: The compile is generating a jump table either way, and the size difference is the knock-on result of other optimisations that are applied to the actual code, and that reordering the code affects which optimisations are applied. (More likely things like jump elision and common subexpression elimination.)
Either way, I think the root cause is the weird way C defined its switch
statements (which, naturally, C++ inherited).
In a more enlightened language you’d probably expect a switch
statement (or equivalent) to consist of multiple independent case
blocks that are entirely unrelated to each other and could theoretically be reordered without issue, which would be really good for optimisation.
But unfortunately in C++, as in C, case
doesn’t act like the beginning of a block statement, it actually acts like a label, so instead of having lots of small unconnected, mutually exclusive, reorderable statements, what you’ve got is actually an ordinary block of statements with a bunch of labels spread throughout, and the break
being a contextual shorthand for goto end
.
That’s why if you want to use declare new local variables after a case
you have to make an explicit scope via a block statement ({
}
) - because jumping to a particular case is technically skipping the initialisation of a variable, and that’s not allowed. (But skipping over an entire block is because the variable only exists for the duration of the block.)
Quite a lot of people know about the monstrosity that is Duff’s device, but few realise that technically switch(0);
is legal code because ;
is an empty statement, which fulfils the switch
statement grammar rule (switch(<expression>) <statement>
).
Anyway, end of rant…
Since the question was about switch
es rather than other potential savings, I’ll mention one thing that might help you shrink your switch
es, or rather ensure that you’re getting a proper jump table without having to resort to compiler flags or assembly.
It’s a compiler extension, so naturally I greatly disapprove of it, but you’re probably already using case ranges, so you might as well be hanged for a sheep as for a lamb.
Presenting: 'Labels as Values`:
void demo(uint8_t selection)
{
constexpr size_t case_count = 3;
// Theoretically it should be possible to mark this
// as static constexpr, which might save a little bit of memory.
const void * jump_table[case_count] = { &&case_0, &&case_1, &&case_2 };
// Guard against buffer overrun
if(selection > case_count)
return;
// Jump to the relevant label
goto *jump_table[selection];
case_0:
// ...
goto end;
case_1:
// ...
goto end;
case_2:
// ...
goto end;
end:
// ...
}
Bear in mind that:
- This might not be a saving because the compiler might be doing this anyway.
- Depending on the situation, there may be a cheaper alternative to a jump table, particularly if your constants have wide gaps between their values.
- This is a bit more awkward because you have to manage the jump table manually, though that does at least allow you the flexibility of reusing the same label at multiple indices, so you can have more or less any pattern you want.
- You can also modify the variable you’re using as an index, e.g.
index % 100
, which may come in handy for some space-saving tricks. E.g. using a jump table for a series of constants that have consecutive values, and plain if
s for constants with more isolated values. (I.e. you don’t want to fill a jump table with loads of unused entries - that’s just wasting space.)
The problem with fallthrough is that it’s not explicit and the compiler may throw up some warnings for unmarked fallthrough.
If you’re going to go down that route it’s best to use the [[fallthrough]]
attribute. It’s actually from C++17 rather than C++11, but hopefully the compiler will still recognise it in gnu++11 mode and know to disable the warnings. Failing that, it’ll just be ignored by the compiler. Either way, it’s a nice explicit way to tell the reader that the fallthrough is intentional.
It’s ironic really that K&R thought that fallthrough would be the common case and break
ing would be the uncommon case when really what the language needed was the opposite - an explicit fallthrough keyword (perhaps reusing continue
to mean ‘continue to the next case
’).