When writing a switch statement, there appears to be two limitations on what you can switch on in case statements.
For example (and yes, I know, if you're doing this sort of thing it probably means your object-oriented (OO) architecture is iffy – this is just a contrived example!),
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
Here the switch() statement fails with 'A value of an integral type expected' and the case statements fail with 'A constant value is expected'.
Why are these restrictions in place, and what is the underlying justification? I don't see any reason why the switch statement has to succumb to static analysis only, and why the value being switched on has to be integral (that is, primitive). What is the justification?
Best Answer
It's important not to confuse the C# switch statement with the CIL switch instruction.
The CIL switch is a jump table, that requires an index into a set of jump addresses.
This is only useful if the C# switch's cases are adjacent:
But of little use if they aren't:
(You'd need a table ~3000 entries in size, with only 3 slots used)
With non-adjacent expressions, the compiler may start to perform linear if-else-if-else checks.
With larger non- adjacent expression sets, the compiler may start with a binary tree search, and finally if-else-if-else the last few items.
With expression sets containing clumps of adjacent items, the compiler may binary tree search, and finally a CIL switch.
This is full of "mays" & "mights", and it is dependent on the compiler (may differ with Mono or Rotor).
I replicated your results on my machine using adjacent cases:
Then I also did using non-adjacent case expressions:
What's funny here, is that the binary tree search appears a little (probably not statistically) quicker than the CIL switch instruction.
Brian, you've used the word "constant", which has a very definite meaning from a computational complexity theory perspective. While the simplistic adjacent integer example may produce CIL that is considered O(1) (constant), a sparse example is O(log n) (logarithmic), clustered examples lie somewhere in between, and small examples are O(n) (linear).
This doesn't even address the String situation, in which a static
Generic.Dictionary<string,int32>
may be created, and will suffer definite overhead on first use. Performance here will be dependent on the performance ofGeneric.Dictionary
.If you check the C# Language Specification (not the CIL spec) you'll find "15.7.2 The switch statement" makes no mention of "constant time" or that the underlying implementation even uses the CIL switch instruction (be very careful of assuming such things).
At the end of the day, a C# switch against an integer expression on a modern system is a sub-microsecond operation, and not normally worth worrying about.
Of course these times will depend on machines and conditions. I wouldn’t pay attention to these timing tests, the microsecond durations we’re talking about are dwarfed by any “real” code being run (and you must include some “real code” otherwise the compiler will optimise the branch away), or jitter in the system. My answers are based on using IL DASM to examine the CIL created by the C# compiler. Of course, this isn’t final, as the actual instructions the CPU runs are then created by the JIT.
I have checked the final CPU instructions actually executed on my x86 machine, and can confirm a simple adjacent set switch doing something like:
Where a binary tree search is full of: