Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

.net-core

cil

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.

like image 844
Mike Avatar asked Jul 20 '19 15:07

Mike


1 Answers

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.

like image 157
Mike Avatar answered Oct 19 '22 05:10

Mike