C# Micro Optimizations Part 2 – In Parameter Modifier

In this series of posts, we’re investigating micro-optimizations in C#. As previously mentioned, these may not be applicable to all; but it’s still fun looking at these concepts.

Let’s visit back the last post – Ref arguments. Ref arguments gave us the power of passing structs by value in an extremely efficient manner.

Mutability of a ref struct

Passing structs by ref brings a major disadvantage – the callee might mutate the value of the struct without the caller ever knowing. What if we need to pass structs in an efficient manner, whilst having peace of mind that the callee doesn’t mutate the struct?

Meet the in parameter modifier- C# 7.2

What does the in parameter modifier do? It allows us to pass the argument by reference and giving us the guarantee that the arguments cannot be modified by the callee. Excellent! Let’s run a quick test and make sure our performance is still comparable when passing by ref. Let’s have a struct with 2 properties – let’s have some work done using two different methods – passing by ref and passing by in.

All code can be viewed here – https://github.com/albertherd/csharpmopt2-in

public class SixteenBitStructBenchmark
{
    [Benchmark]
    [Arguments(100000000)]
    public void BenchmarkIncrementByRef(int limit)
    {
        SixteenBitStruct sixteenBitStruct = new SixteenBitStruct();
        int counter = 0;
        do
        {
            IncrementByRef(ref sixteenBitStruct);
            counter++;
        }
        while (limit != counter);
    }

    [Benchmark]
    [Arguments(100000000)]
    public void BenchmarkIncrementByIn(int limit)
    {
        SixteenBitStruct sixteenBitStruct = new SixteenBitStruct();
        int counter = 0;
        do
        {
            IncrementIn(sixteenBitStruct);
            counter++;
        }
        while (limit != counter);
    }

    private void IncrementByRef(ref SixteenBitStruct sixteenBitStruct)
    {
        double sum = sixteenBitStruct.D1 + sixteenBitStruct.D2;
    }

    private void IncrementIn(in SixteenBitStruct sixteenBitStruct)
    {
        double sum2 = sixteenBitStruct.D1 + sixteenBitStruct.D2;
    }
}

public struct SixteenBitStruct
{
    public double D1 { get; }
    public double D2 { get; }
}

Let’s see how they perform.

Method limit Mean Error StdDev
BenchmarkIncrementByRef 100000000 23.83 ms 0.0272 ms 0.0241 ms
BenchmarkIncrementByIn 100000000 238.21 ms 0.3108 ms 0.2755 ms

Performance loss?

Wait a second – why is IncrementByIn 10x slower than IncrementByRef when we’re accessing 2 properties in the same struct? Let’s have a look at the generated IL.

IncrementByRef

 IL_0000: ldarg.1
IL_0001: call instance float64 InOperator.SixteenBitStruct::get_D1()
# Loads argument 1 (SixteenBitStruct) and call the getter

IncrementByIn

 IL_0000: ldarg.1
# Prepare a new local variable on the evaluation stack
IL_0001: ldobj InOperator.SixteenBitStruct
# Copies the value of SixteenBitStruct into the loaed argument variable
IL_0006: stloc.0
IL_0007: ldloca.s V_0
IL_0009: call instance float64 InOperator.SixteenBitStruct::get_D1()
# Pops the newly created argument into location 0, loads local variable 0 (new copy of SixteenBitStruct) and call the getter

Interesting! When we’ve called the method by ref, the resultant IL just loads the argument and calls the getter. When we’ve called the method by in, the resultant IL creates a copy of the struct before the getter is called. It seems that each time we’re referencing the property, C# is generating a copy of the object for us? We’re facing a by-design feature – a defensive copy.

Why do we encounter a defensive copy?

When calling the getter of our properties, the compiler doesn’t know if the getter mutates the object. Although this is a getter, it’s only by convention that changes aren’t made; there is no language construct that prevents us from changing values in our getter. The compiler must honor the in keyword and generate a defensive copy, just in case the getter modifies the struct.

In the end of the day, a getter is just syntactic sugar for a method. Of course, defensive copies will be generated if methods are called on the struct since the compiles can’t provide any guarantee that the method call won’t mutate the struct.

How do we get around this?

We’ll need instruct the compiler that our struct is immutable, so the compiler doesn’t need to worry about creating defensive copies since values cannot change. C# provides this exact functionality in fact! We can slap the “readonly” keyword (and drop any setters) so that we can guarantee that our struct is now immutable.

Here’s how it looks now

 public readonly struct SixteenBitStruct
{
    public double D1 { get; }
    public double D2 { get; }
}

Revisiting our performance numbers

Let’s re-run our benchmarks and assess the performance.

Method limit Mean Error StdDev
BenchmarkIncrementByRef 100000000 23.93 ms 0.1226 ms 0.1147 ms
BenchmarkIncrementByIn 100000000 24.06 ms 0.2183 ms 0.2042 ms

Far better! Performance is now equal (within margin of error). Some closing thoughts about this:

  • Using the in operator is an excellent feature – it allows the callers to safely assume that the values they are going to pass will not have their values changed.
  • Using the readonly modifier with a struct is another excellent feature – it allows the the developer to safely say that its value is immutable and no changes are allowed.
  • The performance uplift is should be considered as a bonus – the design and infrastructure wins using the in / readonly keywords in these context carry far more value.
  • Don’t ever use the in keyword in conjunction with non-readonly structs. Chances are that the performance gained from passing by ref will be lost by accessing the struct’s properties and methods.

Until the next one!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s