Почему операторы намного медленнее, чем вызовы методов? (структуры медленнее только на старых JIT)

84

Введение: я пишу высокопроизводительный код на C #. Да, я знаю, что C ++ даст мне лучшую оптимизацию, но я все же предпочитаю использовать C #. Я не хочу обсуждать этот выбор. Скорее, я хотел бы услышать мнение тех, кто, как и я, пытается писать высокопроизводительный код на .NET Framework.

Вопросов:

  • Почему оператор в приведенном ниже коде медленнее, чем вызов эквивалентного метода?
  • Почему метод передает два двойных значения в приведенном ниже коде быстрее, чем эквивалентный метод, передающий структуру с двумя двойными значениями внутри? (A: старые JIT плохо оптимизируют структуры)
  • Есть ли способ заставить компилятор .NET JIT обрабатывать простые структуры так же эффективно, как и члены структуры? (A: получите более новую JIT)

Думаю, я знаю: исходный компилятор .NET JIT не встраивал ничего, что связано со структурой. Причудливые заданные структуры следует использовать только там, где вам нужны небольшие типы значений, которые следует оптимизировать как встроенные, но это правда. К счастью, в .NET 3.5SP1 и .NET 2.0SP2 были внесены некоторые улучшения в оптимизатор JIT, включая улучшения встраивания, особенно для структур. (Я предполагаю, что они сделали это, потому что в противном случае новая структура Complex, которую они вводили, работала бы ужасно ... поэтому команда Complex, вероятно, обрушивалась на команду JIT Optimizer.) Итак, любая документация до .NET 3.5 SP1, вероятно, не слишком актуален для этого вопроса.

Что показывает мое тестирование: я подтвердил, что у меня есть более новый оптимизатор JIT, проверив, что файл C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll имеет версию> = 3053, и поэтому эти улучшения должны быть к оптимизатору JIT. Однако, даже с учетом этого, мои тайминги и взгляды на разборку показывают:

Созданный JIT код для передачи структуры с двумя двойными значениями намного менее эффективен, чем код, который напрямую передает два двойных значения.

Созданный JIT код для метода структуры передает this гораздо эффективнее, чем если бы вы передали структуру в качестве аргумента.

JIT по-прежнему встраивается лучше, если вы передаете две двойные, а не структуру с двумя двойными, даже с множителем из-за того, что вы явно находитесь в цикле.

Время: На самом деле, глядя на разборку, я понимаю, что большую часть времени в циклах происходит просто доступ к тестовым данным из списка. Разница между четырьмя способами выполнения одних и тех же вызовов кардинально отличается, если исключить служебный код цикла и доступ к данным. Я получаю от 5x до 20x ускорений за выполнение PlusEqual (double, double) вместо PlusEqual (Element). И от 10x до 40x для выполнения PlusEqual (double, double) вместо operator + =. Вау. Грустный.

Вот один набор таймингов:

Populating List<Element> took 320ms.
The PlusEqual() method took 105ms.
The 'same' += operator took 131ms.
The 'same' -= operator took 139ms.
The PlusEqual(double, double) method took 68ms.
The do nothing loop took 66ms.
The ratio of operator with constructor to method is 124%.
The ratio of operator without constructor to method is 132%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 64%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 166%.
The ratio of operator without constructor to method is 187%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 5%.

Код:

namespace OperatorVsMethod
{
  public struct Element
  {
    public double Left;
    public double Right;

    public Element(double left, double right)
    {
      this.Left = left;
      this.Right = right;
    }

    public static Element operator +(Element x, Element y)
    {
      return new Element(x.Left + y.Left, x.Right + y.Right);
    }

    public static Element operator -(Element x, Element y)
    {
      x.Left += y.Left;
      x.Right += y.Right;
      return x;
    }    

    /// <summary>
    /// Like the += operator; but faster.
    /// </summary>
    public void PlusEqual(Element that)
    {
      this.Left += that.Left;
      this.Right += that.Right;
    }    

    /// <summary>
    /// Like the += operator; but faster.
    /// </summary>
    public void PlusEqual(double thatLeft, double thatRight)
    {
      this.Left += thatLeft;
      this.Right += thatRight;
    }    
  }    

  [TestClass]
  public class UnitTest1
  {
    [TestMethod]
    public void TestMethod1()
    {
      Stopwatch stopwatch = new Stopwatch();

      // Populate a List of Elements to multiply together
      int seedSize = 4;
      List<double> doubles = new List<double>(seedSize);
      doubles.Add(2.5d);
      doubles.Add(100000d);
      doubles.Add(-0.5d);
      doubles.Add(-100002d);

      int size = 2500000 * seedSize;
      List<Element> elts = new List<Element>(size);

      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        int di = ii % seedSize;
        double d = doubles[di];
        elts.Add(new Element(d, d));
      }
      stopwatch.Stop();
      long populateMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of += operator (calls ctor)
      Element operatorCtorResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        operatorCtorResult += elts[ii];
      }
      stopwatch.Stop();
      long operatorCtorMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of -= operator (+= without ctor)
      Element operatorNoCtorResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        operatorNoCtorResult -= elts[ii];
      }
      stopwatch.Stop();
      long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of PlusEqual(Element) method
      Element plusEqualResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        plusEqualResult.PlusEqual(elts[ii]);
      }
      stopwatch.Stop();
      long plusEqualMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of PlusEqual(double, double) method
      Element plusEqualDDResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        Element elt = elts[ii];
        plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
      }
      stopwatch.Stop();
      long plusEqualDDMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of doing nothing but accessing the Element
      Element doNothingResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        Element elt = elts[ii];
        double left = elt.Left;
        double right = elt.Right;
      }
      stopwatch.Stop();
      long doNothingMS = stopwatch.ElapsedMilliseconds;

      // Report results
      Assert.AreEqual(1d, operatorCtorResult.Left, "The operator += did not compute the right result!");
      Assert.AreEqual(1d, operatorNoCtorResult.Left, "The operator += did not compute the right result!");
      Assert.AreEqual(1d, plusEqualResult.Left, "The operator += did not compute the right result!");
      Assert.AreEqual(1d, plusEqualDDResult.Left, "The operator += did not compute the right result!");
      Assert.AreEqual(1d, doNothingResult.Left, "The operator += did not compute the right result!");

      // Report speeds
      Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
      Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
      Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
      Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
      Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
      Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);

      // Compare speeds
      long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
      Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);

      operatorCtorMS -= doNothingMS;
      operatorNoCtorMS -= doNothingMS;
      plusEqualMS -= doNothingMS;
      plusEqualDDMS -= doNothingMS;
      Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
      percentageRatio = 100L * operatorCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
      Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
    }
  }
}

IL: (также известный как то, во что компилируется часть вышеперечисленного)

public void PlusEqual(Element that)
    {
00000000 push    ebp 
00000001 mov     ebp,esp 
00000003 push    edi 
00000004 push    esi 
00000005 push    ebx 
00000006 sub     esp,30h 
00000009 xor     eax,eax 
0000000b mov     dword ptr [ebp-10h],eax 
0000000e xor     eax,eax 
00000010 mov     dword ptr [ebp-1Ch],eax 
00000013 mov     dword ptr [ebp-3Ch],ecx 
00000016 cmp     dword ptr ds:[04C87B7Ch],0 
0000001d je     00000024 
0000001f call    753081B1 
00000024 nop       
      this.Left += that.Left;
00000025 mov     eax,dword ptr [ebp-3Ch] 
00000028 fld     qword ptr [ebp+8] 
0000002b fadd    qword ptr [eax] 
0000002d fstp    qword ptr [eax] 
      this.Right += that.Right;
0000002f mov     eax,dword ptr [ebp-3Ch] 
00000032 fld     qword ptr [ebp+10h] 
00000035 fadd    qword ptr [eax+8] 
00000038 fstp    qword ptr [eax+8] 
    }
0000003b nop       
0000003c lea     esp,[ebp-0Ch] 
0000003f pop     ebx 
00000040 pop     esi 
00000041 pop     edi 
00000042 pop     ebp 
00000043 ret     10h 
 public void PlusEqual(double thatLeft, double thatRight)
    {
00000000 push    ebp 
00000001 mov     ebp,esp 
00000003 push    edi 
00000004 push    esi 
00000005 push    ebx 
00000006 sub     esp,30h 
00000009 xor     eax,eax 
0000000b mov     dword ptr [ebp-10h],eax 
0000000e xor     eax,eax 
00000010 mov     dword ptr [ebp-1Ch],eax 
00000013 mov     dword ptr [ebp-3Ch],ecx 
00000016 cmp     dword ptr ds:[04C87B7Ch],0 
0000001d je     00000024 
0000001f call    75308159 
00000024 nop       
      this.Left += thatLeft;
00000025 mov     eax,dword ptr [ebp-3Ch] 
00000028 fld     qword ptr [ebp+10h] 
0000002b fadd    qword ptr [eax] 
0000002d fstp    qword ptr [eax] 
      this.Right += thatRight;
0000002f mov     eax,dword ptr [ebp-3Ch] 
00000032 fld     qword ptr [ebp+8] 
00000035 fadd    qword ptr [eax+8] 
00000038 fstp    qword ptr [eax+8] 
    }
0000003b nop       
0000003c lea     esp,[ebp-0Ch] 
0000003f pop     ebx 
00000040 pop     esi 
00000041 pop     edi 
00000042 pop     ebp 
00000043 ret     10h 
Брайан Кеннеди
источник
22
Вау, на это следует ссылаться как на пример того, как может выглядеть хороший вопрос по Stackoverflow! Можно было опустить только автоматически созданные комментарии. К сожалению, я знаю слишком мало, чтобы углубиться в суть проблемы, но мне очень нравится этот вопрос!
Деннис Трауб
2
Я не думаю, что модульный тест - хорошее место для проведения теста.
Хенк Холтерман,
1
Почему структура должна быть быстрее двух удвоений? В .NET структура НИКОГДА не равна сумме размеров ее членов. Так что по определению он больше, поэтому по определению он должен быть медленнее при нажатии на стек, чем просто 2 двойных значения. Если компилятор встроит параметр структуры в строку 2 двойной памяти, что, если внутри метода вы хотите получить доступ к этой структуре с отражением. Где будет информация времени выполнения, связанная с этим объектом структуры? Не правда ли, или я что-то упускаю?
Тигран
3
@Tigran: Вам нужны источники для этих утверждений. Думаю, ты ошибаешься. Только когда тип значения помещается в коробку, метаданные должны храниться вместе со значением. В переменной со статическим типом структуры накладных расходов нет.
Ben Voigt
1
Я думал, что не хватает только сборки. И теперь вы это добавили (обратите внимание, это ассемблер x86, а НЕ MSIL).
Ben Voigt

Ответы:

9

Я получаю совсем другие результаты, гораздо менее драматические. Но не использовал тестовый раннер, я вставил код в приложение консольного режима. Результат 5% составляет ~ 87% в 32-битном режиме, ~ 100% в 64-битном режиме, когда я пробую.

Выравнивание критически важно для двойников, среда выполнения .NET может обещать только выравнивание 4 на 32-битной машине. Мне кажется, что исполнитель тестов запускает методы тестирования с адресом стека, который выровнен по 4 вместо 8. Штраф за несовпадение становится очень большим, когда двойник пересекает границу строки кэша.

Ганс Пассан
источник
Почему .NET может в принципе добиться успеха при выравнивании всего 4 дублей? Выравнивание выполняется с помощью 4-байтовых блоков на 32-битной машине. Что там за проблема?
Тигран
Почему время выполнения выравнивается только до 4 байтов на x86? Я думаю, что он может быть согласован с 64-битным, если он будет уделять особое внимание, когда неуправляемый код вызывает управляемый код. Хотя в спецификации есть лишь слабые гарантии согласования, реализации должны иметь возможность согласовывать более строго. (Спецификация: «8-байтовые данные правильно выровнены, когда они хранятся на той же границе, которая требуется базовому оборудованию для атомарного доступа к собственному int»)
CodesInChaos 01
1
@Code - Да, генераторы кода C делают это, вычисляя указатель стека в прологе функции. Джиттера x86 просто нет. Это гораздо важнее для родных языков, поскольку распределение массивов в стеке гораздо более распространено, и у них есть распределитель кучи, который выравнивается до 8, поэтому никогда не хотелось бы делать выделение стека менее эффективным, чем распределение кучи. Мы застряли с выравниванием 4 из 32-битной кучи gc.
Ханс Пассан
5

Мне трудно воспроизвести ваши результаты.

Я взял твой код:

  • сделал его автономным консольным приложением
  • построил оптимизированную (релизную) сборку
  • увеличен коэффициент "размера" с 2,5 млн до 10 млн
  • запустил его из командной строки (вне IDE)

Когда я это сделал, я получил следующие тайминги, которые сильно отличаются от ваших. Во избежание сомнений выложу именно тот код, который использовал.

Вот мои тайминги

Populating List<Element> took 527ms.
The PlusEqual() method took 450ms.
The 'same' += operator took 386ms.
The 'same' -= operator took 446ms.
The PlusEqual(double, double) method took 413ms.
The do nothing loop took 229ms.
The ratio of operator with constructor to method is 85%.
The ratio of operator without constructor to method is 99%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 91%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 71%.
The ratio of operator without constructor to method is 98%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 83%.

И это мои правки в ваш код:

namespace OperatorVsMethod
{
  public struct Element
  {
    public double Left;
    public double Right;

    public Element(double left, double right)
    {
      this.Left = left;
      this.Right = right;
    }    

    public static Element operator +(Element x, Element y)
    {
      return new Element(x.Left + y.Left, x.Right + y.Right);
    }

    public static Element operator -(Element x, Element y)
    {
      x.Left += y.Left;
      x.Right += y.Right;
      return x;
    }    

    /// <summary>
    /// Like the += operator; but faster.
    /// </summary>
    public void PlusEqual(Element that)
    {
      this.Left += that.Left;
      this.Right += that.Right;
    }    

    /// <summary>
    /// Like the += operator; but faster.
    /// </summary>
    public void PlusEqual(double thatLeft, double thatRight)
    {
      this.Left += thatLeft;
      this.Right += thatRight;
    }    
  }    

  public class UnitTest1
  {
    public static void Main()
    {
      Stopwatch stopwatch = new Stopwatch();

      // Populate a List of Elements to multiply together
      int seedSize = 4;
      List<double> doubles = new List<double>(seedSize);
      doubles.Add(2.5d);
      doubles.Add(100000d);
      doubles.Add(-0.5d);
      doubles.Add(-100002d);

      int size = 10000000 * seedSize;
      List<Element> elts = new List<Element>(size);

      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        int di = ii % seedSize;
        double d = doubles[di];
        elts.Add(new Element(d, d));
      }
      stopwatch.Stop();
      long populateMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of += operator (calls ctor)
      Element operatorCtorResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        operatorCtorResult += elts[ii];
      }
      stopwatch.Stop();
      long operatorCtorMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of -= operator (+= without ctor)
      Element operatorNoCtorResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        operatorNoCtorResult -= elts[ii];
      }
      stopwatch.Stop();
      long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of PlusEqual(Element) method
      Element plusEqualResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        plusEqualResult.PlusEqual(elts[ii]);
      }
      stopwatch.Stop();
      long plusEqualMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of PlusEqual(double, double) method
      Element plusEqualDDResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        Element elt = elts[ii];
        plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
      }
      stopwatch.Stop();
      long plusEqualDDMS = stopwatch.ElapsedMilliseconds;

      // Measure speed of doing nothing but accessing the Element
      Element doNothingResult = new Element(1d, 1d);
      stopwatch.Reset();
      stopwatch.Start();
      for (int ii = 0; ii < size; ++ii)
      {
        Element elt = elts[ii];
        double left = elt.Left;
        double right = elt.Right;
      }
      stopwatch.Stop();
      long doNothingMS = stopwatch.ElapsedMilliseconds;

      // Report speeds
      Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
      Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
      Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
      Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
      Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
      Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);

      // Compare speeds
      long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
      Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);

      operatorCtorMS -= doNothingMS;
      operatorNoCtorMS -= doNothingMS;
      plusEqualMS -= doNothingMS;
      plusEqualDDMS -= doNothingMS;
      Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
      percentageRatio = 100L * operatorCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
      Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
      percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
      Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
    }
  }
}
Кори Косак
источник
Я сделал то же самое, мои результаты больше похожи на ваши. Укажите платформу и тип CPu.
Хенк Холтерман,
Очень интересно! Я попросил других проверить мои результаты ... вы первый, кто изменился. Первый вопрос к вам: каков номер версии файла, который я упоминаю в своем сообщении ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... это тот, который указан в документах Microsoft версия вашего JIT Optimizer. (Если я могу просто посоветовать своим пользователям обновить их .NET, чтобы увидеть большое ускорение, я буду счастливым туристом. Но я предполагаю, что все будет не так просто.)
Брайан Кеннеди
Я работал в Visual Studio ... на Windows XP SP3 ... на виртуальной машине VMware ... на Intel Core i7 с тактовой частотой 2,7 ГГц. Но меня интересуют не абсолютные моменты времени ... а отношения ... Я ожидаю, что все эти три метода будут работать одинаково, что они сделали для Кори, но НЕ для меня.
Брайан Кеннеди,
Свойства моего проекта говорят: Конфигурация: Выпуск; Платформа: Активная (x86); Цель платформы: x86
Кори Косак
1
Что касается вашего запроса на получение версии mscorwks ... Извините, вы хотите, чтобы я запустил эту штуку против .NET 2.0? Мои тесты были на .NET 4.0
Кори Косак
3

Запуск .NET 4.0 здесь. Я скомпилировал "Any CPU", ориентируясь на .NET 4.0 в режиме выпуска. Выполнение было из командной строки. Он работал в 64-битном режиме. У меня немного другое время.

Populating List<Element> took 442ms.
The PlusEqual() method took 115ms.
The 'same' += operator took 201ms.
The 'same' -= operator took 200ms.
The PlusEqual(double, double) method took 129ms.
The do nothing loop took 93ms.
The ratio of operator with constructor to method is 174%.
The ratio of operator without constructor to method is 173%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 112%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 490%.
The ratio of operator without constructor to method is 486%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 163%.

В частности, PlusEqual(Element)немного быстрее, чем PlusEqual(double, double).

Какой бы ни была проблема в .NET 3.5, похоже, что в .NET 4.0 ее нет.

Джим Мишель
источник
2
Да, ответ на Structs выглядит так: «получите более новую JIT». Но, как я спросил в ответе Хенка, почему методы намного быстрее, чем операторы? Оба ваших метода в 5 раз быстрее, чем любой из ваших операторов ... которые делают то же самое. Замечательно, что я снова могу использовать структуры ... но грустно, что мне все еще приходится избегать операторов.
Брайан Кеннеди,
Джим, мне было бы очень интересно узнать версию файла C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll в вашей системе ... если новее моего (.3620), но старше чем у Кори (.5446), то это может объяснить, почему ваши операторы все еще медленные, как мои, а Кори - нет.
Брайан Кеннеди,
@Brian: Версия файла 2.0.50727.4214.
Джим Мишель,
БЛАГОДАРЯ! Итак, мне нужно убедиться, что у моих пользователей 4214 или новее, чтобы получить оптимизацию структуры, и 5446 или новее, чтобы получить оптимизацию оператора. Мне нужно добавить код, чтобы проверить это при запуске и дать несколько предупреждений. Еще раз спасибо.
Брайан Кеннеди,
2

Как и @Corey Kosak, я только что запустил этот код в VS 2010 Express как простое консольное приложение в режиме выпуска. Я получаю совсем другие цифры. Но у меня также есть Fx4.5, так что это может быть не результат для чистого Fx4.0.

Populating List<Element> took 435ms.
The PlusEqual() method took 109ms.
The 'same' += operator took 217ms.
The 'same' -= operator took 157ms.
The PlusEqual(double, double) method took 118ms.
The do nothing loop took 79ms.
The ratio of operator with constructor to method is 199%.
The ratio of operator without constructor to method is 144%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 108%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 460%.
The ratio of operator without constructor to method is 260%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 130%.

Изменить: и теперь запустить из строки cmd. Это имеет значение и меньше вариаций в числах.

Хенк Холтерман
источник
Да, похоже, более поздняя JIT устранила проблему со структурой, но мой вопрос о том, почему методы намного быстрее операторов, остается. Посмотрите, насколько оба метода PlusEqual быстрее, чем эквивалентный оператор + =. И также интересно, насколько быстрее - =, чем + = ... ваши тайминги - первое, где я это видел.
Брайан Кеннеди,
Хенк, мне было бы очень интересно узнать версию файла C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll в вашей системе ... если новее моей (.3620), но старше чем у Кори (.5446), то это может объяснить, почему ваши операторы все еще медленные, как мои, а Кори - нет.
Брайан Кеннеди,
1
Я могу найти только версию .50727, но не уверен, актуальна ли она для Fx40 / Fx45?
Хенк Холтерман,
Вам нужно зайти в «Свойства» и щелкнуть вкладку «Версия», чтобы увидеть остальную часть номера версии.
Брайан Кеннеди,
2

В дополнение к различиям JIT-компилятора, упомянутым в других ответах, еще одно различие между вызовом метода структуры и оператором структуры заключается в том, что вызов метода структуры будет передаваться thisкак refпараметр (и может быть записан для принятия других параметров в качестве refпараметров), в то время как Оператор структуры передаст все операнды по значению. Стоимость передачи структуры любого размера в качестве refпараметра является фиксированной, независимо от размера структуры, в то время как стоимость передачи более крупных структур пропорциональна размеру структуры. Нет ничего плохого в использовании больших структур (даже сотен байтов), если можно избежать их ненужного копирования ; в то время как ненужные копии часто можно предотвратить с помощью методов, их нельзя предотвратить с помощью операторов.

суперкар
источник
Хммм ... ну, это может многое объяснить! Итак, если оператор достаточно короткий, чтобы быть встроенным, я предполагаю, что он не будет делать ненужных копий. Но если нет, и ваша структура состоит из нескольких слов, вы можете не захотеть реализовывать ее как оператор, если скорость критична. Спасибо за понимание.
Брайан Кеннеди,
Кстати, одна вещь, которая меня немного раздражает, когда на вопросы о скорости отвечают "сравните это!" заключается в том, что такой ответ игнорирует тот факт, что во многих случаях имеет значение то, занимает ли операция обычно 10 мс или 20 мс, но может ли небольшое изменение обстоятельств привести к тому, что она займет 1 мс или 10 мс. Важно не то, насколько быстро что-то работает на машине разработчика, а то, будет ли эта операция когда-нибудь достаточно медленной, чтобы иметь значение ; если метод X работает в два раза быстрее, чем метод Y на большинстве машин, но на некоторых машинах он будет в 100 раз медленнее, метод Y может быть лучшим выбором.
supercat
Конечно, здесь мы говорим всего о двух двойных ... не больших структурах. Передача двух двойников в стеке, где к ним можно быстро получить доступ, не обязательно медленнее, чем передача this в стеке, а затем необходимость разыменовать это, чтобы подтянуть их для работы с ними ... но это может вызвать различия. Однако в этом случае он должен быть встроен, поэтому оптимизатор JIT должен иметь точно такой же код.
Брайан Кеннеди,
1

Не уверен, что это актуально, но вот цифры для 64-разрядной версии .NET 4.0 в 64-разрядной версии Windows 7. Моя версия mscorwks.dll - 2.0.50727.5446. Я просто вставил код в LINQPad и запустил его оттуда. Вот результат:

Populating List<Element> took 496ms.
The PlusEqual() method took 189ms.
The 'same' += operator took 295ms.
The 'same' -= operator took 358ms.
The PlusEqual(double, double) method took 148ms.
The do nothing loop took 103ms.
The ratio of operator with constructor to method is 156%.
The ratio of operator without constructor to method is 189%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 78%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 223%.
The ratio of operator without constructor to method is 296%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 52%.
Дэниел Прайден
источник
2
Интересно ... похоже, что оптимизации, которые были добавлены к оптимизатору JIT 32b, еще не дошли до оптимизатора JIT 64b ... ваши соотношения все еще очень похожи на мои. Неутешительно ... но полезно знать.
Брайан Кеннеди
0

Я бы предположил, что когда вы обращаетесь к членам структуры, она фактически выполняет дополнительную операцию для доступа к члену, ЭТОЙ указатель + смещение.

Мэтью
источник
1
Что ж, с объектом класса вы были бы абсолютно правы ... потому что методу просто будет передан указатель this. Однако со структурами этого не должно быть. Структура должна быть передана в методы стека. Итак, первое двойное значение должно сидеть там, где должен быть указатель this, а второе двойное - в позиции сразу после него ... оба, возможно, являются регистрами в ЦП. Итак, JIT должен использовать не более чем смещение.
Брайан Кеннеди,
0

Может, вместо List использовать double [] с «известными» смещениями и приращениями индекса?

Константин Исаев
источник