Switches (Why if / then / else when you can switch?)

No, not the Nintendo ones.

If you have looked at my code, you will know I love a good switch statement for readability. I also have come to the conclusion that although I am using them, they are probably not very memory economical.

Does anyone have any guidelines for the effective ordering of clauses, etc that will make the compiler generate memory efficient code? I am not an assembly junkie so let’s try to keep it at the C++ level!

So, I have always thought that if / then / else statements might be better,

I took the following code:

switch (level.getGameState()) {

    case GameState::Pause_Inventory:
        level.setCounter(0);
        level.setGameState(GameState::Inventory);
        break;

    case GameState::Pause_Map:   
        level.setGameState(GameState::Map);
        break;

    case GameState::Pause_LoadSave:
        if (cookie.hasSavedGame) {
            level.setGameState(GameState::Pause_LoadSave_Save);
        }
        else {
            level.setGameState(GameState::Pause_LoadSave_SaveOnly);
        }
        break;

    case GameState::Pause_More:        
        level.setGameState(GameState::Pause_Stats);
        break;

    case GameState::Pause_MainMenu:
        level.setGameState(GameState::Pause_MainMenu_Confirm);
        break;

    case GameState::Pause_LoadSave_Save:
    case GameState::Pause_LoadSave_SaveOnly:
        level.setCounter(96);
        cookie.hasSavedGame = true;
        saveCookie(true);
        level.setGameState(GameState::Pause_LoadSave_Saving);
        break;

    case GameState::Pause_LoadSave_Load:    
        FX::loadGameState(cookie);
        level.setCounter(96);
        level.setGameState(GameState::Pause_LoadSave_Loading);
        break;

    case GameState::Pause_LoadSave_Clear:   
        level.setCounter(96);
        cookie.hasSavedGame = false;
        saveCookie(true);
        level.setGameState(GameState::Pause_LoadSave_Saving);
        break;

    case GameState::Pause_LoadSave_Saving: 
    case GameState::Pause_LoadSave_Loading:
        level.setGameState(GameState::Pause_LoadSave);
        deactivateLEDs();
        break;

    case GameState::Pause_Stats:                       
        level.setGameState(GameState::Stats);
        break;

    case GameState::Pause_Music_FX:               
        level.setGameState(GameState::Pause_Sound_Start);
        break;

    case GameState::Pause_RollDice:                  
        level.setGameState(GameState::RollDice);
        break;

    case GameState::Pause_Credits:                    
        level.setGameState(GameState::Credits);
        break;

    case GameState::Pause_MainMenu_Confirm:
        level.setGameState(GameState::Title_Init);
        break;

    case GameState::Pause_Sound_Start ... GameState::Pause_Sound_End: 
        EEPROM_Utils::saveSettings(soundSettings);
        level.setGameState(GameState::Pause_Music_FX);
        break;

    default:
        break;

}

And turned it into:

if (level.getGameState() == GameState::Pause_Inventory) {
    level.setCounter(0);
    level.setGameState(GameState::Inventory);
}

if (level.getGameState() == GameState::Pause_Map) {
    level.setGameState(GameState::Map);
}

if (level.getGameState() == GameState::Pause_LoadSave) {
    if (cookie.hasSavedGame) {
        level.setGameState(GameState::Pause_LoadSave_Save);
    }
    else {
        level.setGameState(GameState::Pause_LoadSave_SaveOnly);
    }
}

if (level.getGameState() == GameState::Pause_More) {
    level.setGameState(GameState::Pause_Stats);
}
...

And it added 80 bytes to the code.

As the clauses are mutually exclusive, I added ‘elses’ in:

if (level.getGameState() == GameState::Pause_Inventory) {
    level.setCounter(0);
    level.setGameState(GameState::Inventory);
}

else if (level.getGameState() == GameState::Pause_Map) {
    level.setGameState(GameState::Map);
}

else if (level.getGameState() == GameState::Pause_LoadSave) {
    if (cookie.hasSavedGame) {
        level.setGameState(GameState::Pause_LoadSave_Save);
    }
    else {
        level.setGameState(GameState::Pause_LoadSave_SaveOnly);
    }
}

else if (level.getGameState() == GameState::Pause_More) {
    level.setGameState(GameState::Pause_Stats);
}
...

And the final compile was 6 bytes larger than the original case statement. Maybe the compiler isn’t so bad after all.

You can try adding --param case-values-threshold=<N> to the compile options – it seems to control the cutoff between implementing switch statements as jump tables or if-else. Godbolt experiment

1 Like

Are you supposed to be on holidays? :slight_smile:

Just got back!

1 Like

For a good comparison you should use a temprary variable to assign the level.getGameState() to instead of repeatedly using level.getGameState()

state = level.getGameState();
if (state == .. ) 
else if (state == ..) 
3 Likes

Yeah … good point, I am not sure why I did that :slight_smile:

So I updated a couple of large switches using a temporary variable and it made 6 bytes difference!

I am just looking for miracles!

Running out of space,?

Also worth exploring order of case statements and maximizing use of fallthrough.

Of course!

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 switches rather than other potential savings, I’ll mention one thing that might help you shrink your switches, 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 ifs 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 breaking 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’).

Not me, but just read up on it … and the wiki picture of Duff himself makes him look like a serial killer.

I think your example is missing the original goto *jump_table[selection]. Its an interesting concept but I am not sure I have the stamina to test it :slight_smile:

Of course I am using ranges - you know my code too well. And my ancestors were ‘hanged for a sheep as for a lamb’. Actually it should be ‘shipped for a sheep as for a lamb’.

True. Most languages actually forbid fallthrough.

If you would like to take a quick look? You have access to the repo …

I’m surprised at that, I thought it was one of those old-school jokes like nasal demons and the story of mel and ‘goto considered harmful’.

Oops. Fixed it.

Nor do I at the moment, unfortunately.

I think these days the penalty is just a few years of plain old incarceration, just in case you’re ever considering taking up sheep rustling.

I can’t actually think of any offhand. C# and Java don’t.

Rust comes close, though its match is closer to Haskell’s case than C’s switch - i.e. it’s technically a value-producing expression rather than a sequence of statements.

I didn’t know (or forgot) I had access but that explains why I saw some github emails floating by :smile:

Will have a look.

I had a quick glimpse at the main ino and noticed FX initialisation on lines 48 and 49 are redundant/obselete. That’s some easy ~52 bytes saved to start with.

initRandomSeed on line 51 looks like a leftover to me as random isn’t used anymore?. If that’s true that can save a whopping ~630 bytes

Will do a more thorough look later andwill PM new findings.

1 Like

It’s like old times - me writing bad code, you finding memory savings within it, me using those up again. … rinse and repeat.

4 Likes

I am not sure I am reading your statement correctly. Java has an implicit fallthrough but doesn’t C# demand a break or return? (Ignoring empty clauses)

C# technically allows fallthrough, but you have to make it explicit with a goto case.

switch(expression)
{
	case 1:
		// ...
		goto case 2;
	case 2:
		// ...
		break;
}

The requirement is actually to have a statement that ensures control is passed out of the case, there’s nothing saying that you can’t pass control to another case, hence goto case technically fits that definition.

Of course, you can use that for more than just fallthrough, you can end up producing absolute spaghetti if you want. You could even use it for looping.

The same is true for C and C++ once you start using gotos, the difference is that in C and C++ you’d have to provide your own labels whereas C# lets you use the case as an actual label.