如果我有一个c#只读结构,它的成员是非只读结构,编译器会用in参数创建防御副本吗

如果我有一个c#只读结构,它的成员是非只读结构,编译器会用in参数创建防御副本吗,c#,optimization,struct,c#-7.2,defensive-copy,C#,Optimization,Struct,C# 7.2,Defensive Copy,例如,如果我有一个struct PlayerData,它在System.Numerics(而不是只读结构)中定义了Vector3结构的成员 我将它们传递给方法: AddPlayerData(new PlayerData(Vector3.Zero, Vector3.UnitX, Vector3.One)) public void AddPlayerData(in PlayerData playerData) { ... } c#是否会因为Vector3成员不是只读结构而是只读字段而创建防御

例如,如果我有一个struct PlayerData,它在System.Numerics(而不是只读结构)中定义了Vector3结构的成员

我将它们传递给方法:

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

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

c#是否会因为Vector3成员不是只读结构而是只读字段而创建防御副本?现有库不提供向量的只读版本,所以如果我不为基本向量编写自己的版本,那么在传递大于intptr的结构时,我是否被迫忘记了参数优化中的全部内容?阅读时,有关用法的信息不太清楚:

有趣的问题。让我们测试一下:

    public void AddPlayerData(in PlayerData pd)
    {
        pd.SpawnPoint.X = 42;
    }
给出编译器错误:

无法修改只读字段“PlayerData.SpawnPoint”的成员(构造函数或变量中除外)

因此,我假设编译器不会创建任何防御副本,因为非只读结构无论如何都无法修改。可能存在允许结构更改的边缘情况,我对语言规范的了解不够,无法确定这一点

然而,讨论编译器优化是很困难的,因为只要结果相同,编译器通常可以自由地做任何它想做的事情,因此在不同的编译器版本之间,行为可能会发生很大的变化。通常的建议是做一个基准测试来比较您的备选方案

让我们这样做:

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);
    }

“为了确保参数的值保持不变,编译器在每次使用方法/属性时都会创建参数的防御副本。如果结构是只读的,则编译器将以与只读字段相同的方式删除防御副本。”对于像这样的带有秒表的微基准来说,即使有宽松的循环条件,这些方法也是粗糙和误导的——你永远无法确定你是否在测量一些无关的开销。这是专门为避免这种陷阱而编写的。@Jeroen Mostert是的,我知道Benchmark.Net,但它需要相当长的时间我想说的是,对于这样一个简单的演示,秒表就足够了。但是,如果你想在Benchmark.Net中进行复制,请这样做。我很想知道是否有任何显著的区别。
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);
    }
Sum1: 1035
Sum2: 1027