Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dodging the performance hit from using `in` with a struct without making the struct readonly?

C# 7.2 added two new features:

  1. In Parameters

    Using in for a parameter let's us pass by reference, but then prevents us from assigning a value to it. However the performance can actually become worse, because it creates a "defensive copy" of the struct, copying the whole thing

  2. Readonly Structs

    A way around this is to use readonly for a struct. When you pass it into an in parameter, the compiler sees that it's readonly and won't create the defensive copy, thereby making it the better alternative for preformance.

That's all great, but every field in the struct has to be readonly. This doesn't work:

public readonly struct Coord
{
    public int X, Y;    // Error: instance fields of readonly structs must be read only
}

Auto-properties also have to be readonly.

Is there a way to get the benefits of in parameters (compile-time checking to enforce that the parameter isn't changed, passing by reference) while still being able to modify the fields of the struct, without the significant performance hit of in caused by creating the defensive copy?

like image 919
AustinWBryan Avatar asked May 26 '18 05:05

AustinWBryan


1 Answers

When you pass [a readonly struct] into an in parameter, the compiler sees that it's readonly and won't create the defensive copy.

I think you misunderstood. The compiler creates a defensive copy of a readonly variable that contains a struct (that could be an in parameter, but also a readonly field) when you call a method on that struct.

Consider the following code:

struct S
{
    int x, y;

    public void M() {}
}

class C
{
    static void Foo()
    {
        S s = new S();
        Bar(s);
    }

    static void Bar(in S s)
    {
        s.M();
    }
}

You can inspect the IL generated for the code above to see what's actually going to happen.

For Foo, the IL is:

ldloca.s 0 // load address of the local s to the stack
initobj S  // initialize struct S at the address on the stack
ldloca.s 0 // load address of the local s to the stack again
call void C::Bar(valuetype S&) // call Bar
ret        // return

Notice that there is no copying: the local s is initialized and then the address to that local is directly passed to Bar.

The IL for Bar is:

ldarg.0     // load argument s (which is an address) to the stack
ldobj S     // copy the value from the address on the stack to the stack
stloc.0     // store the value from the stack to an unnamed local variable
ldloca.s 0  // load the address of the unnamed local variable to the stack
call instance void S::M() // call M
ret         // return

Here, the ldobj and stloc instructions create the defensive copy, to make sure that if M mutates the struct, s won't be mutated (since it's readonly).

If you change the code to make S a readonly struct, then the IL for Foo stays the same, but for Bar it changes to:

ldarg.0 // load argument s (which is an address) to the stack
call instance void S::M() // call M
ret     // return

Notice that there is no copying here anymore.

This is the defensive copy that marking your struct as readonly avoids. But if you don't call any instance methods on the struct, there won't be any defensive copies.

Also note that the language dictates that when the code executes, it has to behave as if the defensive copy was there. If the JIT can figure out that the copy is not actually necessary, it is permitted to avoid it.

like image 66
svick Avatar answered Oct 10 '22 16:10

svick