Now, the first thing that comes to mind when you are reading the title of this blog is a very “insightful” question that potentially dictates my reluctance to learn switch properly in C# even after all these years. Fret not, I’m the same old uncool dude who learns stuff late and in the process gets his ass kicked.
Today I want to go through the age old switch statement we all have been using. There’s a good number among us who prefers switch over an annoying if-else block. And I claim no crime there, not at all. Up until a couple of days ago I was a happy chump to use switch whenever I’m handling compile time constants like enums and writing if else blocks for my logical operation checks. Simple, happy as ever. All of that changed when my skyrim-ridden dull brain asked “Why there are two prominent branching paradigm here in this statically typed language” and kind of made a good point to find that out. I don’t remember what point my brain made at that time, sorry. But it’s my brain and I can’t deny much like deadpool cant.
Let’s stay on focus, shall we? Lets go ahead and write a simple switch snippet like the following:
public class Program { enum WhoGivesACrap { I_do, Nope_I_dont, Maybe_NotSure } static void Main(string[] args) { WhoGivesACrap whoGivesACrap = WhoGivesACrap.I_do; switch(whoGivesACrap) { case WhoGivesACrap.I_do: System.Console.WriteLine($"{whoGivesACrap}"); break; case WhoGivesACrap.Maybe_NotSure: System.Console.WriteLine($"{whoGivesACrap}"); break; case WhoGivesACrap.Nope_I_dont: System.Console.WriteLine($"{whoGivesACrap}"); break; } } }
Now, this is definitely the first day at C# programming grade code. Dull, blasphemous and quiet frankly not worth of any attention. Same thoughts as mine to be honest. Thus I kept looking, booted up ildasm and asked what we can we see inside. Now, I can post the full de-assembled MSIL code here but that won’t make much of sense. Lets look on the parts we really want to look.
//000014: //000015: switch(whoGivesACrap) IL_0003: ldloc.0 IL_0004: stloc.1 .line 16707566,16707566 : 0,0 '' //000016: { //000017: case WhoGivesACrap.I_do: //000018: System.Console.WriteLine($"{whoGivesACrap}"); //000019: break; //000020: case WhoGivesACrap.Maybe_NotSure: //000021: System.Console.WriteLine($"{whoGivesACrap}"); //000022: break; //000023: case WhoGivesACrap.Nope_I_dont: //000024: System.Console.WriteLine($"{whoGivesACrap}"); //000025: break; //000026: } //000027: } //000028: } //000029: } IL_0005: ldloc.1 IL_0006: switch ( IL_0019, IL_0049, IL_0031) IL_0017: br.s IL_0061 .line 18,18 : 21,66 '' //000018: System.Console.WriteLine($"{whoGivesACrap}");
I opted for dumping with my C# source code. And the thing that catches my eye is the switch invocation with the three jump locations. And as my enums were adjacent it makes sense. CIL switch essentially create a jump table. The three arguments it takes are essentially jump locations which will be compared against my enums. Cool, at least now I have an answer why it is different than if-else-if-else. Remember I didn’t say if-else block because it makes more sense to do if-else in this fashion than checking else for no apparent reason.
If you are still not bored enough why don’t you go and have a look here?
Now, I also thought this would be the end of it. But the voices in my head reminded me to do one more thing. And that’s using non-adjacent values. Thus I modified my enum in the following fashion.
enum WhoGivesACrap { I_do = 17, Nope_I_dont = 57, Maybe_NotSure = 945 }
And hooked up ildasm again to figure out what happened this time. Now, my values are non adjacent. It doesn’t really make sense anymore to create around 900+ entry jump table.
//000014: //000015: switch(whoGivesACrap) IL_0004: ldloc.0 IL_0005: stloc.1 .line 16707566,16707566 : 0,0 '' //000016: { //000017: case WhoGivesACrap.I_do: //000018: System.Console.WriteLine($"{whoGivesACrap}"); //000019: break; //000020: case WhoGivesACrap.Maybe_NotSure: //000021: System.Console.WriteLine($"{whoGivesACrap}"); //000022: break; //000023: case WhoGivesACrap.Nope_I_dont: //000024: System.Console.WriteLine($"{whoGivesACrap}"); //000025: break; //000026: } //000027: } //000028: } //000029: } IL_0006: ldloc.1 IL_0007: ldc.i4.s 17 IL_0009: beq.s IL_001e IL_000b: br.s IL_000d IL_000d: ldloc.1 IL_000e: ldc.i4.s 57 IL_0010: beq.s IL_004e IL_0012: br.s IL_0014 IL_0014: ldloc.1 IL_0015: ldc.i4 0x3b1 IL_001a: beq.s IL_0036 IL_001c: br.s IL_0066 .line 18,18 : 21,66 '' //000018: System.Console.WriteLine($"{whoGivesACrap}");
And I wasn’t wrong. Since the values are non-adjacent now CSC opted for loading the enums separately and used beq.s which stands for branch-if-equal in short form. Technically this looks like a vanilla if-else-if block. Although I can’t possibly say CSC would generate opcodes following this and this only.
I know this whole thing might sound insanely boring while performance wise this would only make a minuscule difference. Still, it’s always fun to answer the voices inside my head and they are much happier when they have something to hold against when they ask why.
Until next time!