Is this a defensive copy of readonly struct passed to a method with in keyword
Asked Answered
P

1

10

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.

Pitiful answered 20/7, 2019 at 15:30 Comment(8)
I believe in that case ldarg is loading the references of that structs into the stack. BTW why is so important to pass those structs as reference?Hardihood
@Hardihood I'm not entirely sure that's the case. If I replace readonly structs with mutable structs, generated IL is exactly the same, but in this case it is expected for a method to create defensive copies for mutable structs passed to a method with in modifier. And passing those structs as a reference to avoid copying themPitiful
@Pitiful None of your IL samples show defensive copy. In this example defensive copy only happens in Test2.Immutable
@PetSerAl I see there extra ldobj instruction, in fact I copy pasted code in sharplab.io and also see ldobj instruction for the method CalculateDistance(in MutablePoint3D point1, in MutablePoint3D point2), but I do not see ldobj instruction in the ILDASM.exe output nor IL Viewer in R# which I used to get the IL instructions posted in the question. Can you explain output difference between sharplab.io & dev tools ?Pitiful
@Pitiful Are you absolutely sure which build artifact you a viewing in ILDasm? Can you create separate project with just code in question, to see what IL code would you get?Immutable
@PetSerAl I am dead sure I'm looking at build artifact of the project that has code in question. Do you suggest you get different output ?Pitiful
Looks like more recent compiler versions do some optimization, when auto-implemented properties are involved: sharplab.io/…Immutable
@PetSerAl can you post it as an answer, seems that's the case although I can't find any official documentation around this optimization.Pitiful
P
3

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.

Pitiful answered 8/10, 2019 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.