I'm trying to pass a readonly struct
to a method with in
modifier.
When I look at generated IL code, it seems that defensive copy of the readonly struct is made.
The readonly struct
is defined as
public readonly struct ReadonlyPoint3D
{
public ReadonlyPoint3D(double x, double y, double z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
public double X { get; }
public double Y { get; }
public double Z { get; }
}
Method that accepts ReadonlyPoint3D
private static double CalculateDistance(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
And the way I'm calling this method:
static void Main(string[] args)
{
var point1 = new ReadonlyPoint3D(0, 0, 0);
var point2 = new ReadonlyPoint3D(1, 1, 1);
var distance = CalculateDistance(in point1, in point2);
}
If I look at generated IL for CalculateDistance
method calling, I see that ReadonlyPoint3D
instances are passed by reference:
IL_0045: ldloca.s point1
IL_0047: ldloca.s point2
IL_0049: call float64 CSharpTests.Program::CalculateDistance(valuetype CSharpTests.ReadonlyPoint3D&, valuetype CSharpTests.ReadonlyPoint3D&)
IL_004e: stloc.2 // distance
However, CalculateDistance
method's IL seem to make copies of point1
& point2
arguments:
// [25 9 - 25 10]
IL_0000: nop
// [26 13 - 26 54]
IL_0001: ldarg.0 // point1
IL_0002: call instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_0007: ldarg.1 // point2
IL_0008: call instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_000d: sub
IL_000e: stloc.0 // xDifference
// the resit is omitted for the sake of brevity, essentially same code repeated for Y & Z
ldarg.0
& ldarg.1
in CalculateDistance
method's generated IL makes me think that copies of point1
& point2
were made.
What I was expecting to see here are ldloca.s
instructions which I think would've mean to load the address of point1
& point2
.
Do I understand it correctly, defensive copies are made ? Or is my interpretation of IL code is wrong ?
I'm using .NET Core 2.1 with C# 7.3
EDIT
According to Microsoft docs, mutable structs passed with in
modifier will have defensive copies created.
If I define mutable struct
public struct MutablePoint3D
{
public MutablePoint3D(double x, double y, double z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
And pass it with in
private static double CalculateDistance(in MutablePoint3D point1, in MutablePoint3D point2)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
I can see generated IL code is similar to what readonly struct
had generated:
// [26 13 - 26 54]
IL_0001: ldarg.0 // point1
IL_0002: call instance float64 CSharpTests.MutablePoint3D::get_X()
IL_0007: ldarg.1 // point2
IL_0008: call instance float64 CSharpTests.MutablePoint3D::get_X()
IL_000d: sub
IL_000e: stloc.0 // xDifference
// the resit is omitted for the sake of brevity
Another observation is if I remove in
modifier from CalculateDisctance
method which accepts ReadonlyPoint3D
, generated IL code is what I would expect
// [35 13 - 35 54]
IL_0001: ldarga.s point1
IL_0003: call instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_0008: ldarga.s point2
IL_000a: call instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_000f: sub
IL_0010: stloc.0 // xDifference
But this doesn't seem to correspond to the suggestion in Microsoft Docs
EDIT 2
As suggested by @PetSerAl in the comments, sharplab.io produces different IL for this code.
The difference - ldobj
instruction seen only for CalculateDistance(in MutablePoint3D point1, in MutablePoint3D point2)
would explain that defensive copy is done only for this case.
However, the IL instructions posted in the question were taken from ReSharper's IL Viewer and verified by ILDASM.exe tool (for Release configuration, like in sharplab.io). So I'm not sure where this difference comes from and which output to be trusted.
Long discussion can be found on related GitHub issue.
In essence this was a Roslyn bug that was fixed, recent versions of VS 2019 (16.2 and higher) have the fix.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With