5

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?

Community
  • 1
  • 1
AustinWBryan
  • 3,249
  • 3
  • 24
  • 42
  • 1
    I think you have answered your own question, its either a reference or its not, you can either modify it or you cant, i dont think there is a fit for all your needs honestly – TheGeneral May 26 '18 at 06:09
  • Had a feeling that was the case. I was just curious if there was some clever way to lazy load the defensive copy when it's being used as an `in` parameter. – AustinWBryan May 26 '18 at 06:15
  • Do you mean you'd want to be able to modify the field within the method, but the compiler would create a copy at that point? That feels like it would be very confusing. (A complete example of how you'd want to use this would be helpful, as then we could suggest alternatives.) – Jon Skeet May 26 '18 at 07:22

3 Answers3

6

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.

svick
  • 236,525
  • 50
  • 385
  • 514
  • 3
    For the sake of completeness: since c# 8, we have ability to mark individual methods in struct as readonly. So we now have an option to avoid defensive copy in Bar() method by marking only M() method as readonly, without making the whole struct readonly – Misa Jovanovic Jun 20 '20 at 09:44
  • @MisaJovanovic Interesting, is that similar to C++ const functions, which guarantee the method won't mutate the object? – AustinWBryan Oct 20 '22 at 19:55
1

What about this declaration like this:

public struct ExampleStruct
{
    public double A { readonly get => _a; set => _a = value; }
    public double B { readonly get => _b; set => _b = value; }
    public double C { readonly get => _c; set => _c = value; }

    private double _a;
    private double _b;
    private double _c;

    public ExampleStruct(double a, double b, double c)
    {
        _a = a;
        _b = b;
        _c = c;
    }
}

It allows me to modify data, and perform well in benchmarks

Grzegorz G.
  • 1,285
  • 2
  • 14
  • 27
0

First off, I would avoid non-readonly structs at all cost. Only very strong performance requirements could justify a mutable struct in heap-allocation-free, hot execution paths. And if your struct is mutable at its core, why would you make it readonly for one method? Thats a dangerous, error-prone path.

Fact: Combining in parameter passing with non-readonly structs will lead to a defensive copy before the reference to that copy is passed into the method.

Thus, any mutability will work on the compiler's copy visible inside the method context, not visible for the caller. Confusing and not maintainable.

I thought in parameters are useful to help the compiler make smart decisions for better performance. That's definitely not true! I've experimented with in and readonly structs for performance reasons and my conclusion is: there are too many pitfalls that actually make your code slower. If you are the only developer on a project, if your structs are large enough, you know deeply all the compiler trickery, and you run micro benchmarks frequently... then you might benefit in terms of performance.

Patrick Stalph
  • 792
  • 1
  • 9
  • 19