Чому оператори набагато повільніші за виклики методів? (структури повільніші лише на старих JIT)


84

Вступ: Я пишу високопродуктивний код на C #. Так, я знаю, що C ++ дасть мені кращу оптимізацію, але я все-таки вирішив використовувати C #. Я не хочу обговорювати цей вибір. Швидше, я хотів би почути тих, хто, як і я, намагається писати високопродуктивний код на .NET Framework.

Запитання:

  • Чому оператор у коді нижче повільніший за виклик еквівалентного методу ??
  • Чому метод передає два подвійні в коді нижче швидше, ніж еквівалентний метод, передаючи структуру, яка має дві подвійні всередині? (A: старіші JIT погано оптимізують структури)
  • Чи є спосіб змусити компілятор .NET JIT обробляти прості структури так само ефективно, як і члени структури? (A: отримати новіший JIT)

Що, на мою думку, я знаю: оригінальний компілятор .NET JIT не вкладав би нічого, що стосувалося б структури. Химерні задані структури слід використовувати лише там, де потрібні типи малих значень, які повинні бути оптимізовані як вбудовані, але справжні. На щастя, в .NET 3.5SP1 та .NET 2.0SP2 вони внесли деякі вдосконалення в JIT Optimizer, включаючи вдосконалення вбудовування, особливо для конструкцій. (Я здогадуюсь, що вони зробили це, бо в іншому випадку нова Складна структура, яку вони запровадили б, працювала жахливо ... тому команда Складного, ймовірно, стукала по команді JIT Optimizer.) Отже, будь-яка документація до .NET 3.5 SP1, ймовірно, є не надто актуальні для цього питання.

Що показує моє тестування: я переконався, що у мене є новіший JIT Optimizer, перевіривши, чи файл C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll має версію> = 3053, і тому маю мати ці вдосконалення до оптимізатора JIT. Однак, навіть незважаючи на це, мої терміни та погляд на демонтаж демонструють:

Створений JIT код для передачі структури із двома подвійними набагато менш ефективний, ніж код, який безпосередньо передає два подвійні.

Код JIT для методу struct передає "this" набагато ефективніше, ніж якби ви передали структуру як аргумент.

JIT все-таки краще вбудовується, якщо ви проходите два дублі, а не передаєте структуру з двома дублями, навіть з мультиплікатором, оскільки явно перебуваєте в циклі.

Часи: Насправді, дивлячись на розбирання, я усвідомлюю, що більшу частину циклів це просто доступ до даних тесту поза списком. Різниця між чотирма способами здійснення однакових дзвінків кардинально відрізняється, якщо врахувати накладний код циклу та доступ до даних. Я отримую десь від 5x до 20x прискорень для того, щоб робити PlusEqual (подвійний, подвійний) замість PlusEqual (Element). І 10x до 40x для виконання PlusEqual (double, double) замість оператора + =. Ого. Сумно.

Ось один набір термінів:

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

Іллінойс: (він же, у що компілюється щось із вищезазначеного)

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
Я не думаю, що Unit Test - це гарне місце для тестування.
Henk Holterman

1
Чому структура повинна бути швидшою, ніж дві подвійні? У .NET struct НІКОЛИ не дорівнює сумі розмірів його членів. Отже, за визначенням він більший, тому за визначенням він повинен бути повільнішим при натисканні на стек, а потім лише 2 подвійні значення. Якщо компілятор вбудує параметр struct у подвійну пам’ять рядка 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

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

Я просто зробив те саме, мої результати більше схожі на ваші. Будь ласка, вкажіть платформу та тип процесора.
Henk Holterman

Дуже цікаво! У мене були інші, які перевіряли мої результати ... ти перший, хто отримав інше. Перше запитання для вас: який номер версії файлу я згадав у своєму дописі ... 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 тут. Я компілював з "Будь-яким процесором", націлюючись на .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?
Henk Holterman

Вам потрібно зайти у Властивості та натиснути на вкладку Версія, щоб побачити решту номера версії.
Брайан Кеннеді

2

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


Мда ... ну, це може багато чого пояснити! Отже, якщо оператор досить короткий, щоб його було вбудовано, я припускаю, що він не буде робити зайвих копій. Але якщо ні, і ваша структура - це більше одного слова, ви можете не захотіти реалізовувати її як оператор, якщо швидкість критична. Дякую за це розуміння.
Брайан Кеннеді

До речі, одна річ, яка мене трохи дратує, коли на питання про швидкість відповідають "порівняти!" полягає в тому, що така реакція ігнорує той факт, що у багатьох випадках важливим є те, чи займає операція зазвичай 10 або 20, але чи може незначна зміна обставин спричинити 1 мс або 10 мс. Важливим є не те, наскільки швидко щось працює на машині розробника, а скоріше, чи буде операція колись досить повільною, щоб мати значення ; якщо метод X працює вдвічі швидше, ніж метод Y на більшості машин, але на деяких машинах він буде в 100 разів повільнішим, метод Y може бути кращим вибором.
supercat

Звичайно, тут ми говоримо лише про 2 дублі ... не великі структури. Передача двох дублів у стек, де їх можна швидко отримати, не обов’язково повільніша, ніж передача „цього” у стек, а потім необхідність переорієнтуватись, щоб втягнути їх, щоб оперувати ними .. але це може спричинити відмінності. Однак у цьому випадку його слід вбудувати, тому в результаті оптимізатор JIT повинен мати точно такий самий код.
Брайан Кеннеді

1

Не впевнений, чи це доречно, але ось цифри для .NET 4.0 64-bit у Windows 7 64-bit. Моя версія 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
Цікаво ... здається, що оптимізації, які були додані до 32b JIT Optimizer, ще не досягли 64b JIT Optimizer ... ваші коефіцієнти все ще дуже схожі на мої. Розчаровує ... але приємно знати.
Брайан Кеннеді,

0

Я вважаю, що коли ви отримуєте доступ до членів структури, це фактично робить додаткову операцію для доступу до члена, ЦЕЙ покажчик + зміщення.


1
Що ж, з об’єктом класу ви були б абсолютно праві ... тому що метод просто передається вказівник 'this'. Однак з конструкціями це не повинно бути так. Структура повинна бути передана в методи у стеку. Отже, перший дубль повинен сидіти там, де знаходився би вказівник 'this', а другий дубль у позиції відразу після нього ... обидва, можливо, були б реєстраторами в ЦП. Отже, JIT повинен просто використовувати зсув щонайбільше.
Брайан Кеннеді

0

Може замість Списку ви повинні використовувати double [] з "добре відомими" зміщеннями та збільшенням індексу?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.