In this article, we delve into the performance nuances of switch and if-else statements in C#, two pivotal control flow mechanisms.
Through a comparative analysis, we’ll explore how each statement impacts application performance under various conditions, supported by benchmarks and compiler optimization insights.
Let’s dive in.
Understanding Control Flow Statements
Control flow statements are fundamental to any kind of programming as they allow us to dictate execution flow based on conditions or loops.
The if-else Statement
Firstly, the if-else
statement allows us to build more complex and nuanced decision-making logic with the capability to evaluate not just equality but also a range of logical conditions:
if (condition1) { return "First case"; } else if (condition2) { return "Second case"; } else { return "Default case"; }
The if-else
statement evaluates the conditions specified in the if
and else if
clauses, executing the corresponding code block. Should these conditions not be satisfied, the code within the else
block is executed.
While the if-else
statement provides greater flexibility, it can become cumbersome and less performant with multiple discrete values of a single variable.
The switch Statement
The alternative is the switch
statement that offers a streamlined method for dispatching execution to different parts of code based on the value of an expression.
It’s particularly efficient when comparing the same variable to multiple constants, offering a clean and efficient alternative to multiple if-else
statements:
switch (variable) { case 1: return "First case"; case 2: return "Second case"; default: return "Default case"; }
The switch
statement matches the variable’s value with one of the case
clauses before executing the appropriate code block. The default
code block executes if the value doesn’t match any of the cases.
This structure is cleaner and potentially faster for multiple comparisons, as the C# compiler can optimize switch
statements in certain scenarios.
Moreover, with the introduction of C# 8.0, the switch
statement is now augmented with switch
expressions that offer a more concise syntax for scenarios requiring a return value based on a single condition:
var result = variable switch { 1 => "First case", 2 => "Second case", _ => "Default case", };
This streamlines code and introduces pattern-matching enhancements that make switch
expressions more powerful.
Performance Comparison of switch and if-else
To evaluate performance, we will use the BenchmarkDotNet Nuget package.
First, let’s define the SwitchVsIfElseBenchmarkTests
class:
public class SwitchVsIfElseBenchmarkTests { private readonly int _iterations; private readonly int[] _inputData; public SwitchVsIfElseBenchmarkTests() { _iterations = 1000; _inputData = new int[_iterations]; for (int i = 0; i < _iterations; i++) { _inputData[i] = i % 10; } } }
We define the SwitchVsIfElseBenchmarkTests
class with private fields and a parameterless constructor.
The _iterations
field is an integer representing the number of test iterations to be performed.
The _inputData
field is an array of integers populated with numbers 0 through 9 in a repeating sequence.
One-Condition Scenario
Now, let’s define test methods that will test the performance of our statements with only one condition:
[Benchmark] public void IfElseWithOneCondition() { int result; for (int i = 0; i < _iterations; i++) { if (_inputData[i] == 1) { result = 1; } else { result = -1; } } } [Benchmark] public void SwitchWithOneCase() { int result; for (int i = 0; i < _iterations; i++) { switch (_inputData[i]) { case 1: result = 1; break; default: result = -1; break; } } } [Benchmark] public void SwitchExpressionWithOneCase() { int result; for (int i = 0; i < _iterations; i++) { result = _inputData[i] switch { 1 => 1, _ => -1 }; } }
Here, we define three methods and mark them with Benchmark
attributes.
Our IfElseWithOneCondition()
method measures the performance of the if-else
statement with a single condition. Within a loop, w checks if each element in the _inputData
array equals 1, and sets a result variable accordingly.
Similarly, we define the SwitchWithOneCase()
method using the switch
statement and the SwitchExpressionWithOneCase()
method using the switch
expression.
Now, let’s compare the execution times of each method:
|Method |Mean |Error |StdDev |Median | |-------------------------------|-----------:|----------:|----------:|-----------:| |SwitchWithOneCase |233.6 ns |2.17 ns |2.03 ns |233.5 ns | |IfElseWithOneCondition |233.9 ns |2.34 ns |2.19 ns |233.9 ns | |SwitchExpressionWithOneCase |251.5 ns |4.62 ns |6.00 ns |250.2 ns |
As we can see, the if-else
and traditional switch
statements deliver virtually identical performance.
The switch
expression, while offering syntactical advantages and cleaner code, exhibits a modest increase in execution time, which could be attributed to its newer implementation and the overhead of additional features it provides.
Two-Condition Scenario
Now, let’s add the next methods with two conditions:
[Benchmark] public void IfElseWithTwoConditions() { int result; for (int i = 0; i < _iterations; i++) { if (_inputData[i] == 1) { result = 1; } else if (_inputData[i] == 2) { result = 2; } else { result = -1; } } } [Benchmark] public void SwitchWithTwoCases() { int result; for (int i = 0; i < _iterations; i++) { switch (_inputData[i]) { case 1: result = 1; break; case 2: result = 2; break; default: result = -1; break; } } } [Benchmark] public void SwitchExpressionWithTwoCases() { int result; for (int i = 0; i < _iterations; i++) { result = _inputData[i] switch { 1 => 1, 2 => 2, _ => -1 }; } }
The testing logic is the same as in our previous tests.
The only difference is an additional else-if
statement in our IfElseWithTwoConditions()
method and an additional case
statement in our SwitchWithTwoCases()
and SwitchExpressionWithTwoCases()
methods.
Now, let’s see how it affects the results:
|Method |Mean |Error |StdDev |Median | |-------------------------------|-----------:|----------:|----------:|-----------:| |SwitchExpressionWithTwoCases |243.9 ns |4.84 ns |6.95 ns |251.8 ns | |SwitchWithTwoCases |251.7 ns |4.79 ns |8.14 ns |238.6 ns | |IfElseWithTwoConditions |581.2 ns |11.22 ns |13.36 ns |561.4 ns |
As we can see the performance of the if-else
statement with two conditions shows a significant increase in mean execution time.
In contrast, the switch
statement and the switch
expression maintain a comparatively lower execution time.
Five-Condition and Ten-Condition Scenarios
By now, the trend in our testing methodology is clear.
Therefore, examples for five and ten conditions can be found in the sample solution. In short, the only difference compared to our previous tests is the increased number of conditions for each statement.
Now, let’s see how five conditions affect our performance:
|Method |Mean |Error |StdDev |Median | |-------------------------------|-----------:|----------:|----------:|-----------:| |SwitchWithFiveCases |237.8 ns |2.59 ns |2.29 ns |238.6 ns | |SwitchExpressionWithFiveCases |251.4 ns |5.03 ns |8.94 ns |251.8 ns | |IfElseWithFiveConditions |948.3 ns |28.44 ns |76.90 ns |922.9 ns |
As we can see, while the performance of if-else
significantly deteriorates as the number of conditions increases, both switch
statements and expressions maintain their efficiency, barely affected by the additional complexity.
Lastly, let’s see how ten conditions affect the performance:
|Method |Mean |Error |StdDev |Median | |-------------------------------|-----------:|----------:|----------:|-----------:| |SwitchExpressionWithTenCases |239.0 ns |1.89 ns |1.48 ns |238.9 ns | |SwitchWithTenCases |250.5 ns |5.04 ns |13.46 ns |245.7 ns | |IfElseWithTenConditions |1,340.5 ns |26.74 ns |53.41 ns |1,337.5 ns |
Without a surprise, the results are consistent with our findings so far.
The performance of if-else
statements decline markedly with an increase in conditions, whereas both switch
statements and expressions retain their efficiency.
To sum up our current results, we can see that using either switch
statements or expressions are beneficial to the execution time of our code whenever we need to verify more than two conditions.
Compiler Optimization of the Switch Statement
Now, it’s important to realize the reason behind such results.
In brief, this happens because the compiler optimizes our switch
statements and expressions during the compilation process.
To visualize it, let’s have a look at an example:
public string AnimalSounds(String animal) { switch (animal) { case "dog": return "Woof"; case "cat": return "Meow"; default: return "<silence>"; } }
Here, we define the AnimalSounds()
method that accepts a string parameter. In the method body, we have a switch
statement that based on the parameter returns a string value.
Now, let’s see the decompiled version of our method:
public string AnimalSounds(string animal) { if (!(animal == "dog")) { if (animal == "cat") { return "Meow"; } return "<silence>"; } return "Woof"; }
As we can see, the switch
statement is converted to a set of nested if
statements.
It gets even more interesting if we add more cases:
public string AnimalSounds(String animal) { switch (animal) { case "dog": return "Woof"; case "cat": return "Meow"; case "horse": return "Neigh"; case "bird": return "Chirp"; case "fish": return "<Bubbles sound>"; case "mouse": return "Peep"; case "oyster": return "..."; default: return "<silence>"; } }
Now, the switch
statement has a total of 9 cases.
Let’s see what the compiler will do with this code:
public string AnimalSounds(string animal) { if (animal != null) { switch (animal.Length) { case 5: { char c = animal[0]; if (c != 'h') { if (c == 'm' && animal == "mouse") { return "Peep"; } } else if (animal == "horse") { return "Neigh"; } break; } case 3: { char c = animal[0]; if (c != 'c') { if (c == 'd' && animal == "dog") { return "Woof"; } } else if (animal == "cat") { return "Meow"; } break; } case 4: { char c = animal[0]; if (c != 'b') { if (c == 'f' && animal == "fish") { return "<Bubbles sound>"; } } else if (animal == "bird") { return "Chirp"; } break; } case 6: if (!(animal == "oyster")) { break; } return "..."; } } return "<silence>"; }
In this case, the compiler restructures the code to minimize expensive operations, like full string comparisons, by performing quicker, more efficient checks, like length and character comparisons.
This kind of optimization demonstrates the compiler’s role in not only code translation but also enhancing its execution efficiency without altering its semantic meaning.
It is also important to note that compiler optimization will likely differ between .NET versions, and some will bring greater performance gains than others.
Performance Comparison of if-else and switch with Pattern Matching
Now, we know the performance difference between the if-else
statement and the switch
statement and expression.
However, as we mentioned before the switch
expression can be enhanced with pattern-matching, let’s see how it affects the performance of our code in comparison to complex if-else
statements.
First, let’s define a few helping classes:
public class Circle { public int Radius { get; set; } } public class Line { public int Length { get; set; } } public class Rectangle { public int Width { get; set; } public int Height { get; set; } }
Now, let’s define the PatternMatchingVsComplexIfElseBenchmarkTests
class:
public class PatternMatchingVsComplexIfElseBenchmarkTests { private object[] _shapes = { new Circle { Radius = 12 }, new Circle { Radius = 4 }, new Rectangle { Width = 4, Height = 4 }, new Rectangle { Width = 4, Height = 10 }, new Line { Length = 10 }, }; [Params(1000)] public int N; [Benchmark] public void IfElseWithComplexConditions() { string shapeName; foreach (object shape in _shapes) { shapeName = ClassifyShapeWithIfElse(shape); } } [Benchmark] public void SwitchExpressionWithPatternMatching() { string shapeName; foreach (object shape in _shapes) { shapeName = ClassifyShapeWithPatternMathing(shape); } } private string ClassifyShapeWithIfElse(object shape) { if (shape is Circle) { if (((Circle) shape).Radius > 10) { return "Large Circle"; } return "Circle"; } if (shape is Rectangle) { var rectangle = shape as Rectangle; if (rectangle.Height == rectangle.Width) { return "Square"; } return "Rectangle"; } return "Unknown Shape"; } private string ClassifyShapeWithPatternMathing(object shape) => shape switch { Circle { Radius: > 10 } => "Large Circle", Circle => "Circle", Rectangle { Width: var w, Height: var h } when w == h => "Square", Rectangle => "Rectangle", _ => "Unknown Shape" }; }
Here, we define the _shapes
field, which is an array of objects and we initialize it with instances of Circle
, Rectangle
and Line
classes. The N
field specifies the total number of test iterations.
Then, we create the IfElseWithComplexConditions()
method that iterates over the _shapes
array, calling the ClassifyShapeWithIfElse()
method to categorize each shape using the if-else
statement and return an appropriate string label.
Similarly, the SwitchExpressionWithPatternMatching()
and ClassifyShapeWithPatternMathing()
methods do the same task, but we leverage the switch
expression enhanced by pattern matching for classification.
Now, let’s check our results:
|Method |Mean |Error |StdDev | |------------------------------------|-----------:|----------:|----------:| |SwitchExpressionWithPatternMatching |8.253 ns |0.1206 ns |0.1128 ns | |IfElseWithComplexCnditions |15.416 ns |0.3344 ns |0.3128 ns |
As we can see from the results, the switch
expression with pattern-matching is almost two times faster than the if-else
equivalent.
It is important to realize that it can enhance performance by reducing the need for multiple if-else
conditions and type checks, enabling the compiler to generate more efficient IL code. However complex pattern-matching conditions might introduce additional computational overhead compared to simple value comparisons.
In general, the performance benefits of pattern matching are most noticeable in scenarios where its use reduces cumbersome type-checking logic and simplifies the flow of control statements.
Common Mistakes and Best Practices
While both statements are indispensable in C# programming, incorrect usage can cause unnecessary complexity, performance issues, and difficulty maintaining or debugging code.
Understanding common mistakes and adhering to best practices that can significantly improve code efficiency and readability is important.
Overusing if-else for Simple Value Checks
Firstly, utilizing multiple if-else
statements for checking a single variable against various constants is less efficient than the switch
statement.
Therefore, we should use the switch
statements for these conditions and the if-else
statements for more complex conditions involving multiple variables or ranges.
Avoid Excessive Nesting
Next, deeply nested if-else
statements can make our code harder to read and maintain.
For this reason, we should consider refactoring deeply nested structures into separate methods or using pattern matching to simplify the logic.
Leverage Pattern Matching with switch Expressions
Lastly, in C# 8 and later, switch
expressions offer a more concise syntax for some scenarios. In general, it allows for pattern matching and improves code readability.
It’s worth noting that switch
expressions can sometimes offer performance benefits due to compiler optimizations, though the compiled Intermediate Language may vary based on the expression complexity.
Conclusion
In conclusion, our tests have demonstrated that switch statements and expressions consistently maintain performance, while if-else statements degrade with each new condition.
Additionally, we have illustrated the critical role that the compiler plays in optimizing code.
These findings highlight the significance of selecting the appropriate control flow statement in C# based on our needs and code complexity.