'Dodging the performance hit from using `in` with a struct without making the struct readonly?
C# 7.2 added two new features:
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 thingReadonly Structs
A way around this is to use
readonly
for astruct
. When you pass it into anin
parameter, the compiler sees that it'sreadonly
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?
Solution 1:[1]
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.
Solution 2:[2]
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.
Solution 3:[3]
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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Patrick Stalph |
Solution 3 | Grzegorz G. |