4

so, if I have for example a struct PlayerData that has members of Vector3 structs defined in System.Numerics (not readonly structs)

public readonly struct PlayerData
{
   public readonly Vector3 SpawnPoint;
   public readonly Vector3 CurrentPosition;
   public readonly Vector3 Scale;    
  
   ...constructor that initializes fields
}

and I pass them to the method:

AddPlayerData(new PlayerData(Vector3.Zero, Vector3.UnitX, Vector3.One))

public void AddPlayerData(in PlayerData playerData)
{
  ...
}

Will c# create defensive copy because of Vector3 members that are not readonly structs but are readonly fields? Existing libraries don't offer readonly versions of Vectors so am I forced to forget the whole in parameter optimization when passing structs that are bigger than intptr if I don't write my own versions for basic vectors? Information about usage is not that clear when reading: https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code

1
  • 1
    "To make sure that the parameter’s value stays the same the compiler make a defensive copy of the parameter every time a method/property is used. If the struct is readonly then the compiler removes the defensive copy the same way as it does for readonly fields." Reference Commented Oct 15, 2020 at 11:31

1 Answer 1

1

Interesting question. Lets test it:

    public void AddPlayerData(in PlayerData pd)
    {
        pd.SpawnPoint.X = 42;
    }

Gives compiler error:

Members of readonly field 'PlayerData.SpawnPoint' cannot be modified (except in a constructor or a variable

From this I would assume the compiler would not create any defensive copy, since the non-readonly struct cannot be modified anyway. There might be some edge case that allow the struct to change, I'm not well versed enough in the language specification to determine that.

It is however difficult to discuss compiler optimizations since the compiler is usually free to do whatever it wants as long as the result is the same, so the behavior might very well change between compiler versions. As usual the recommendation is to do a benchmark to compare your alternatives.

So lets do that:

public readonly struct PlayerData1
{
    public readonly Vector3 A;
    public readonly Vector3 B;
    public readonly Vector3 C;
    public readonly Vector3 D;
    public readonly Vector3 E;
    public readonly Vector3 F;
    public readonly Vector3 G;
    public readonly Vector3 H;
}
public readonly struct PlayerData2
{
    public readonly ReadonlyVector3 A;
    public readonly ReadonlyVector3 B;
    public readonly ReadonlyVector3 C;
    public readonly ReadonlyVector3 D;
    public readonly ReadonlyVector3 E;
    public readonly ReadonlyVector3 F;
    public readonly ReadonlyVector3 G;
    public readonly ReadonlyVector3 H;
}

public readonly struct ReadonlyVector3
{
    public readonly float X;
    public readonly float Y;
    public readonly float Z;
}

    public static float Sum1(in PlayerData1 pd) => pd.A.X + pd.D.Y + pd.H.Z;
    public static float Sum2(in PlayerData2 pd) => pd.A.X + pd.D.Y + pd.H.Z;

    [Test]
    public void TestInParameterPerformance()
    {
        var pd1 = new PlayerData1();
        var pd2 = new PlayerData2();

        // Do warmup 
        Sum1(pd1);
        Sum2(pd2);
        float sum1 = 0;
        
        var sw1 = Stopwatch.StartNew();
        for (int i = 0; i < 1000000000; i++)
        {
            sum1 += Sum1(pd1);
        }


        float sum2 = 0;
        sw1.Stop();
        var sw2 = Stopwatch.StartNew();
        for (int i = 0; i < 1000000000; i++)
        {
            sum2 += Sum2(pd2);
        }
        sw2.Stop();

        Console.WriteLine("Sum1: " + sw1.ElapsedMilliseconds);
        Console.WriteLine("Sum2: " + sw2.ElapsedMilliseconds);
    }

For me using .Net framework 4.8 this gives

Sum1: 1035
Sum2: 1027

I.e. well within measurement errors. So I would not worry about it.

2
  • For microbenchmarks like these approaches with Stopwatch are crude and misleading, even with generous loop conditions -- you can never be quite sure if you're not measuring some unrelated overhead instead. BenchmarkDotNet is specifically written to avoid such pitfalls. Commented Oct 15, 2020 at 14:35
  • @Jeroen Mostert Yes, I'm aware of Benchmark.Net, but it takes significantly longer to setup. And I would claim a stopwatch is sufficient for such a simple demonstration. But if you want to replicate in Benchmark.Net please do. I'm curious to see if there is any significant difference.
    – JonasH
    Commented Oct 15, 2020 at 15:22

Not the answer you're looking for? Browse other questions tagged or ask your own question.