C# does support multiple control structures such as if
, else
, switch
, while
, for
(and some more). With a control structure you can split your code in multiple possible paths, based on a codition.
A general graphical representation of a control structure
The if
and switch
statements are very well-known and adopted in most programming languages nowadays. They are also often applied for the same use-case.
The experiment
Given a three-digit string of month names, like Jan
, Feb
, Mar
, ... we want to get the corresponding month number as integer. The results would be: Jan = 1
, Feb = 2
, Mar = 3
and so on.
The if-implementation
This is the baseline implementation with if
and return
.
private int GetMonthIndexIf(string month)
{
if (month == "Jan")
{
return 1;
}
if (month == "Feb")
{
return 2;
}
if (month == "Mar")
{
return 3;
}
// and so on
}
The switch-implementation
This is the equivalent implementation with a switch
statement.
private int GetMonthIndexSwitch(string month)
{
return month switch
{
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Okt" => 10,
"Nov" => 11,
"Dez" => 12,
_ => throw new ArgumentException(),
};
}
The syntax used above is called a switch expression.
This is available since C# 8.0, see: MSDN Docs
The Dictionary-implementation
For reference, I've also created an implementation based on a Dictionary<string, int>
.
private int GetMonthIndexDictionary(string month)
{
// _monthsDict maps the month name to the index
return _monthsDict[month];
}
Benchmark setup
I'm using BenchmarkDotNet
for running the benchmarks. For every GetMonthIndex*
method, all months from Jan
to Dez
are called N
times, to cover every path of these methods. The benchmark is executed three times, for N=1, 10, 100
.
Benchmark results
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1645 (21H2)
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.104
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
| Method | N | Mean | Error | StdDev |
|------------------------- |---- |------------:|----------:|----------:|
| GetMonthIndex_Switch | 1 | 173.7 ns | 0.77 ns | 0.72 ns |
| GetMonthIndex_If | 1 | 485.6 ns | 3.13 ns | 2.78 ns |
| GetMonthIndex_Dictionary | 1 | 325.8 ns | 1.70 ns | 1.42 ns |
| GetMonthIndex_Switch | 10 | 960.9 ns | 5.56 ns | 5.20 ns |
| GetMonthIndex_If | 10 | 2,528.3 ns | 15.35 ns | 14.36 ns |
| GetMonthIndex_Dictionary | 10 | 1,793.8 ns | 25.68 ns | 22.77 ns |
| GetMonthIndex_Switch | 100 | 8,211.9 ns | 68.39 ns | 53.40 ns |
| GetMonthIndex_If | 100 | 26,460.3 ns | 513.94 ns | 990.18 ns |
| GetMonthIndex_Dictionary | 100 | 17,662.1 ns | 286.67 ns | 254.13 ns |
As you can above, the GetMonthIndex_Switch
is the fastest,
GetMonthIndex_Dictionary
ranks on the second place, and GetMonthIndex_If
is the slowest.
Mind that these are nanoseconds (!) it won't effect your application performance very much.
Now why is switch
faster than if
?
The switch statement distilled
In fact, the switch statement does not exist in C#. There is a step in the compilation of C#, which rewrites switch statements to if-statements. This step is called lowering. It translates high-level language features to low-level language features.
You can read more about it here.
You will also notice that this is very widely used.
With sharplab.io you can see the C#-code which is produced during lowering. When we paste our original GetMonthIndexSwitch
in there, you will see that this is rewritten into if
statements.
private int GetMonthIndexSwitch(string month)
{
uint num = <PrivateImplementationDetails>.ComputeStringHash(month);
if (num <= 1118301483)
{
if (num <= 749839599)
{
if (num != 566134113)
{
if (num != 663571330)
{
if (num == 749839599 && month == "Sep")
{
return 9;
}
}
else if (month == "Feb")
{
return 2;
}
}
else if (month == "Okt")
{
return 10;
}
}
else if (num != 1000858150)
{
if (num != 1046388392)
{
if (num == 1118301483 && month == "Mar")
{
return 3;
}
}
else if (month == "Dez")
{
return 12;
}
}
else if (month == "May")
{
return 5;
}
}
else if (num <= 1190317742)
{
if (num != 1153511100)
{
if (num != 1187066338)
{
if (num == 1190317742 && month == "Jan")
{
return 1;
}
}
else if (month == "Jun")
{
return 6;
}
}
else if (month == "Jul")
{
return 7;
}
}
else if (num != 2213879282u)
{
if (num != 2319303684u)
{
if (num == 2699988948u && month == "Aug")
{
return 8;
}
}
else if (month == "Nov")
{
return 11;
}
}
else if (month == "Apr")
{
return 4;
}
throw new ArgumentException();
}
Compared to our solution with the if-statements, it produces the same output but works in a whole different way.
Instead of going through all cases, it builds up a tree-like structure. This is where the performance gain comes. It's just faster when you look for a match in a tree-structure, than to look for a match when iterating all possible cases. Below you can see an example of a tree-structure.
The compiler only rewrites it into a tree-structure if there are enough possible cases. When there are less than seven cases, no tree-structure is built.
An example tree structure. This does not match the code generated during lowering seen above
Summary
- Switch statements are rewritten by the compiler to if-statements. Due to the rewriting into a tree-structure, it gets faster the more cases are.
- There are many other high-level language features which are rewritten during the lowering step in the compilation.
- In this scenario with 12 cases,
if
is the slowest, thedictionary
ranks second and theswitch
is the fastest. Nevertheless, it probably won't affect your application's performance.
Top comments (2)
Nice article, personally never worked on applications where performance in the nanoseconds mattered that much, but still knowing stuff like this and being able to benchmark is a valuable skill
Yeah, absolutely. The interesting part is of course the compiler magic here.