Если у меня есть структура c # только для чтения, которая имеет структуры, не доступные только для чтения, в качестве членов, будет ли компилятор создавать защитную копию с помощью параметра in

#c# #optimization #struct #c#-7.2 #defensive-copy

#c# #оптимизация #struct #c # -7.2 #defensive-copy

Вопрос:

итак, если у меня есть, например, структура PlayerData, в которой элементы структур Vector3 определены в System.Числовые значения (не только для чтения)

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

и я передаю их методу:

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

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

Будет ли c # создавать защитную копию из-за элементов Vector3, которые не являются структурами только для чтения, а являются полями только для чтения? Существующие библиотеки не предлагают версии векторов только для чтения, поэтому я вынужден забывать все в оптимизации параметров при передаче структур, которые больше intptr, если я не пишу свои собственные версии для базовых векторов? Информация об использовании не так понятна при чтении: https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code

Комментарии:

1.«Чтобы убедиться, что значение параметра остается неизменным, компилятор создает защитную копию параметра каждый раз, когда используется метод / свойство. Если структура доступна только для чтения, компилятор удаляет защитную копию так же, как и для полей, доступных только для чтения. « Ссылка

Ответ №1:

Интересный вопрос. Давайте протестируем ее:

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

Для меня, использующего .Net framework 4.8 это дает

 Sum1: 1035
Sum2: 1027
  

Т.е. в пределах ошибок измерения. Так что я бы не стал беспокоиться об этом.

Комментарии:

1. Для микробенчмарков, подобных этим, подходы с Stopwatch являются грубыми и вводящими в заблуждение, даже при щедрых условиях цикла — вы никогда не можете быть полностью уверены, если вместо этого вы не измеряете какие-то несвязанные накладные расходы. BenchmarkDotNet специально написан, чтобы избежать таких ошибок.

2. @Jeroen Mostert Да, я в курсе Benchmark.Net , но настройка занимает значительно больше времени. И я бы сказал, что секундомера достаточно для такой простой демонстрации. Но если вы хотите реплицировать в Benchmark.Net пожалуйста, сделай это. Мне любопытно посмотреть, есть ли какая-либо существенная разница.