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.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!