เหตุใดโอเปอเรเตอร์จึงช้ากว่าการเรียกใช้เมธอดมาก (โครงสร้างจะช้าลงเฉพาะใน JIT ที่เก่ากว่า)


84

Intro: ฉันเขียนโค้ดประสิทธิภาพสูงใน C # ใช่ฉันรู้ว่า C ++ จะให้การเพิ่มประสิทธิภาพที่ดีขึ้น แต่ฉันก็ยังเลือกที่จะใช้ C # ฉันไม่ต้องการที่จะอภิปรายทางเลือกนั้น แต่ฉันอยากได้ยินจากคนที่พยายามเขียนโค้ดประสิทธิภาพสูงบน. NET Framework

คำถาม:

  • เหตุใดตัวดำเนินการในรหัสด้านล่างจึงช้ากว่าการเรียกเมธอดเทียบเท่า ??
  • เหตุใดวิธีการส่งผ่านสองคู่ในโค้ดด้านล่างเร็วกว่าวิธีการที่เทียบเท่ากันโดยผ่านโครงสร้างที่มีสองคู่อยู่ภายใน (A: JIT รุ่นเก่าปรับโครงสร้างให้เหมาะสมไม่ดี)
  • มีวิธีรับ. NET JIT Compiler เพื่อจัดการโครงสร้างอย่างง่ายได้อย่างมีประสิทธิภาพเทียบเท่ากับสมาชิกของโครงสร้างหรือไม่ (A: รับ JIT ใหม่กว่า)

สิ่งที่ฉันคิดว่าฉันรู้: คอมไพเลอร์. NET JIT ดั้งเดิมจะไม่อินไลน์อะไรที่เกี่ยวข้องกับโครงสร้าง ควรใช้โครงสร้างที่แปลกประหลาดเฉพาะเมื่อคุณต้องการประเภทค่าขนาดเล็กที่ควรปรับให้เหมาะสมเช่นบิวท์อิน แต่เป็นจริง โชคดีที่ใน. NET 3.5SP1 และ. (ฉันเดาว่าพวกเขาทำอย่างนั้นเพราะไม่เช่นนั้นโครงสร้างซับซ้อนใหม่ที่พวกเขาแนะนำจะมีประสิทธิภาพที่น่ากลัว ... ไม่เกี่ยวข้องกับปัญหานี้มากเกินไป

สิ่งที่การทดสอบของฉันแสดงให้เห็น: ฉันได้ตรวจสอบแล้วว่าฉันมี JIT Optimizer รุ่นใหม่กว่าโดยตรวจสอบว่าไฟล์ C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll มีเวอร์ชัน> = 3053 ดังนั้นควรมีการปรับปรุงเหล่านั้น ไปยังเครื่องมือเพิ่มประสิทธิภาพ JIT อย่างไรก็ตามแม้จะเป็นเช่นนั้นสิ่งที่ฉันกำหนดเวลาและดูการถอดชิ้นส่วนทั้งสองแสดงคือ:

รหัสที่ผลิตโดย JIT สำหรับการส่งผ่านโครงสร้างที่มีสองคู่นั้นมีประสิทธิภาพน้อยกว่ารหัสที่ส่งผ่านสองคู่โดยตรง

โค้ดที่สร้างโดย JIT สำหรับเมธอด struct จะส่งผ่าน 'this' ได้อย่างมีประสิทธิภาพมากกว่าถ้าคุณส่ง struct เป็นอาร์กิวเมนต์

JIT ยังคงอินไลน์ได้ดีกว่าหากคุณผ่านสองคู่แทนที่จะส่งผ่านโครงสร้างด้วยสองคู่แม้ว่าจะมีตัวคูณเนื่องจากอยู่ในวงอย่างชัดเจน

การกำหนดเวลา: จริงๆแล้วการดูการถอดชิ้นส่วนฉันรู้ว่าเวลาส่วนใหญ่ในลูปเป็นเพียงการเข้าถึงข้อมูลการทดสอบจากรายการ ความแตกต่างระหว่างสี่วิธีในการโทรเดียวกันนั้นแตกต่างกันอย่างมากหากคุณแยกรหัสเหนือศีรษะของลูปและการเข้าถึงข้อมูล ฉันได้รับทุกที่ตั้งแต่ 5x ถึง 20x speedups สำหรับการทำ PlusEqual (double, double) แทนที่จะเป็น 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);
    }
  }
}

IL: (aka. สิ่งที่รวบรวมไว้ข้างต้น)

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 จะเป็นอย่างไร! เฉพาะความคิดเห็นที่สร้างขึ้นโดยอัตโนมัติเท่านั้นที่สามารถละเว้นได้ น่าเสียดายที่ฉันรู้น้อยเกินไปที่จะดำดิ่งสู่ปัญหา แต่ฉันชอบคำถามนี้มาก!
Dennis Traub

2
ฉันไม่คิดว่าการทดสอบหน่วยเป็นสถานที่ที่ดีในการใช้เกณฑ์มาตรฐาน
Henk Holterman

1
ทำไมโครงสร้างต้องเร็วกว่าสองเท่า? ในโครงสร้าง. NET ไม่เคยเท่ากับผลรวมของขนาดของสมาชิก ตามนิยามแล้วมันใหญ่กว่าดังนั้นตามนิยามแล้วมันจะต้องช้ากว่าในการผลักสแต็กจากนั้นให้เพิ่มเพียง 2 ค่าคู่ หากคอมไพเลอร์จะอินไลน์พารามิเตอร์ struct ในหน่วยความจำคู่แถวที่ 2 จะเกิดอะไรขึ้นถ้าคุณต้องการเข้าถึงโครงสร้างนั้นด้วยการสะท้อนกลับ ข้อมูลรันไทม์ที่เชื่อมโยงกับวัตถุโครงสร้างนั้นจะอยู่ที่ใด ไม่ใช่หรือฉันขาดอะไรไป?
Tigran

3
@Tigran: คุณต้องการแหล่งข้อมูลสำหรับการเรียกร้องเหล่านั้น ฉันคิดว่าคุณคิดผิด เฉพาะเมื่อประเภทค่าได้รับในกล่องเท่านั้นข้อมูลเมตาจะต้องถูกจัดเก็บด้วยค่าหรือไม่ ในตัวแปรที่มีประเภทโครงสร้างคงที่จะไม่มีค่าใช้จ่าย
Ben Voigt

1
ฉันคิดว่าสิ่งเดียวที่ขาดหายไปคือการชุมนุม และตอนนี้คุณได้เพิ่มสิ่งนั้นแล้ว (โปรดทราบนั่นคือแอสเซมเบลอร์ x86 ไม่ใช่ MSIL)
Ben Voigt

คำตอบ:


9

ฉันได้รับผลลัพธ์ที่แตกต่างกันมากน้อยกว่ามาก แต่ไม่ได้ใช้นักวิ่งทดสอบฉันวางโค้ดลงในแอปโหมดคอนโซล ผลลัพธ์ 5% คือ ~ 87% ในโหมด 32 บิต ~ 100% ในโหมด 64 บิตเมื่อฉันลอง

การจัดตำแหน่งมีความสำคัญอย่างยิ่งต่อการเพิ่มเป็นสองเท่ารันไทม์. NET สามารถสัญญาว่าจะจัดตำแหน่ง 4 บนเครื่อง 32 บิตเท่านั้น สำหรับฉันแล้วนักวิ่งทดสอบกำลังเริ่มต้นวิธีการทดสอบด้วยที่อยู่สแต็กที่อยู่ในแนวเดียวกันกับ 4 แทนที่จะเป็น 8 บทลงโทษการจัดแนวไม่ตรงจะมีขนาดใหญ่มากเมื่อเส้นคู่ข้ามขอบเขตเส้นแคช


เหตุใด. NET จึงประสบความสำเร็จในการจัดตำแหน่งเพียง 4 คู่? การจัดตำแหน่งทำได้โดยใช้ชิ้นส่วน 4 ไบต์บนเครื่อง 32 บิต มีปัญหาอะไร
Tigran

เหตุใดรันไทม์จึงจัดเรียงเป็น 4 ไบต์บน x86 เท่านั้น ฉันคิดว่ามันสามารถจัดแนวเป็น 64 บิตได้หากใช้ความระมัดระวังเพิ่มเติมเมื่อรหัสที่ไม่มีการจัดการเรียกใช้รหัสที่มีการจัดการ แม้ว่าข้อมูลจำเพาะจะมีเพียงการรับประกันการจัดตำแหน่งที่อ่อนแอ แต่การนำไปใช้งานควรจะสามารถจัดตำแหน่งได้อย่างเข้มงวดมากขึ้น (ข้อมูลจำเพาะ: "ข้อมูล 8 ไบต์ได้รับการจัดแนวอย่างเหมาะสมเมื่อเก็บไว้ในขอบเขตเดียวกันที่ฮาร์ดแวร์พื้นฐานกำหนดสำหรับการเข้าถึงอะตอมไปยัง int แบบเนทีฟ")
CodesInChaos

1
@Code - มันทำได้เครื่องกำเนิดรหัส C ทำได้โดยการคำนวณทางคณิตศาสตร์บนตัวชี้สแต็กในอารัมภบทของฟังก์ชัน กระวนกระวายใจ x86 ไม่เพียง มีความสำคัญมากกว่าสำหรับภาษาพื้นเมืองเนื่องจากการจัดสรรอาร์เรย์บนสแต็กเป็นเรื่องธรรมดามากและมีตัวจัดสรรฮีปที่จัดแนวเป็น 8 ดังนั้นจึงไม่ต้องการให้การจัดสรรสแต็กมีประสิทธิภาพน้อยกว่าการจัดสรรแบบฮีป เราติดอยู่กับการจัดตำแหน่ง 4 จากฮีป 32 บิต gc
Hans Passant

5

ฉันประสบปัญหาในการจำลองผลลัพธ์ของคุณ

ฉันเอารหัสของคุณ:

  • ทำให้เป็นแอปพลิเคชันคอนโซลแบบสแตนด์อโลน
  • สร้างรุ่นที่ปรับให้เหมาะสม (ปล่อย)
  • เพิ่มปัจจัย "ขนาด" จาก 2.5M เป็น 10M
  • เรียกใช้จากบรรทัดคำสั่ง (นอก 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
Henk Holterman

น่าสนใจมาก! ฉันให้คนอื่นตรวจสอบผลลัพธ์ของฉัน ... คุณเป็นคนแรกที่เห็นต่าง คำถามแรกสำหรับคุณ: หมายเลขเวอร์ชันของไฟล์ที่ฉันพูดถึงในโพสต์ของฉันคืออะไร ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... นั่นคือเอกสารของ Microsoft ที่ระบุไว้ JIT Optimizer เวอร์ชันที่คุณมี (ถ้าฉันสามารถบอกให้ผู้ใช้ของฉันอัปเกรด. NET เพื่อดู speedups ขนาดใหญ่ฉันจะเป็นผู้ออกค่ายที่มีความสุข แต่ฉันเดาว่ามันจะไม่ง่ายขนาดนั้น)
Brian Kennedy

ฉันทำงานใน Visual Studio ... ทำงานบน Windows XP SP3 ... ในเครื่องเสมือน VMware ... บน Intel Core i7 2.7GHz แต่มันไม่ใช่ช่วงเวลาที่แน่นอนที่ฉันสนใจ ... มันคืออัตราส่วน ... ฉันคาดหวังว่าทั้งสามวิธีจะทำงานในลักษณะเดียวกันซึ่งพวกเขาทำกับ Corey แต่ไม่ได้ทำเพื่อฉัน
Brian Kennedy

คุณสมบัติโครงการของฉันบอกว่า: การกำหนดค่า: ปล่อย; แพลตฟอร์ม: ใช้งานอยู่ (x86); เป้าหมายแพลตฟอร์ม: x86
Corey Kosak

1
เกี่ยวกับคำขอของคุณเพื่อรับเวอร์ชันของ mscorwks ... ขออภัยคุณต้องการให้ฉันรันสิ่งนี้กับ. NET 2.0 หรือไม่? การทดสอบของฉันอยู่บน. NET 4.0
Corey Kosak

3

เรียกใช้. NET 4.0 ที่นี่ ฉันรวบรวมด้วย "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 ที่ใหม่กว่า" แต่อย่างที่ถามกับคำตอบของเฮงค์ทำไมวิธีการจึงเร็วกว่า Operators มาก ทั้งสองวิธีของคุณเร็วกว่าตัวดำเนินการอย่างใดอย่างหนึ่ง 5 เท่า ... ซึ่งทำสิ่งเดียวกันทุกประการ มันเยี่ยมมากที่ฉันสามารถใช้โครงสร้างได้อีกครั้ง ... แต่น่าเศร้าที่ฉันยังต้องหลีกเลี่ยงตัวดำเนินการ
Brian Kennedy

จิมฉันสนใจที่จะทราบเวอร์ชันของไฟล์ C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ในระบบของคุณ ... ถ้าใหม่กว่าของฉัน (.3620) แต่เก่ากว่า มากกว่า Corey (.5446) นั่นอาจอธิบายได้ว่าทำไมตัวดำเนินการของคุณยังช้าเหมือนของฉัน แต่ Corey ไม่ใช่
Brian Kennedy

@ ไบรอัน: ไฟล์เวอร์ชัน 2.0.50727.4214
Jim Mischel

ขอบคุณ! ดังนั้นฉันต้องตรวจสอบให้แน่ใจว่าผู้ใช้ของฉันมี 4214 หรือใหม่กว่าเพื่อรับการเพิ่มประสิทธิภาพโครงสร้างและ 5446 หรือใหม่กว่าเพื่อรับการเพิ่มประสิทธิภาพตัวดำเนินการ ฉันต้องการเพิ่มรหัสเพื่อตรวจสอบว่าเมื่อเริ่มต้นและให้คำเตือนบางอย่าง ขอบคุณอีกครั้ง.
Brian Kennedy

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 ทั้งสองเร็วกว่าตัวดำเนินการ + = ที่เทียบเท่ากันมากแค่ไหน และยังน่าสนใจว่า - = นั้นเร็วกว่า + = ... การกำหนดเวลาของคุณเป็นครั้งแรกที่ฉันได้เห็นมันเร็วแค่ไหน
Brian Kennedy

เฮงค์ฉันสนใจที่จะทราบเวอร์ชันของไฟล์ C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ในระบบของคุณ ... ถ้าใหม่กว่าของฉัน (.3620) แต่เก่ากว่า มากกว่า Corey (.5446) นั่นอาจอธิบายได้ว่าทำไมตัวดำเนินการของคุณยังช้าเหมือนของฉัน แต่ Corey ไม่ใช่
Brian Kennedy

1
ฉันสามารถค้นหาเวอร์ชัน. 50727 ได้ แต่ไม่แน่ใจว่าเกี่ยวข้องกับ Fx40 / Fx45 หรือไม่
Henk Holterman

คุณต้องเข้าไปที่ Properties และคลิกที่แท็บ Version เพื่อดูหมายเลขเวอร์ชันที่เหลือ
Brian Kennedy

2

นอกเหนือจากความแตกต่างของคอมไพลเลอร์ JIT ที่กล่าวถึงในคำตอบอื่นความแตกต่างอีกอย่างระหว่างการเรียกวิธีการโครงสร้างและตัวดำเนินการโครงสร้างคือการเรียกวิธีการโครงสร้างจะส่งผ่านthisเป็นrefพารามิเตอร์ (และอาจเขียนให้ยอมรับพารามิเตอร์อื่นเป็นrefพารามิเตอร์ด้วย) ในขณะที่ a ตัวดำเนินการโครงสร้างจะส่งผ่านตัวถูกดำเนินการทั้งหมดตามค่า ค่าใช้จ่ายในการส่งผ่านโครงสร้างทุกขนาดเป็นrefพารามิเตอร์ได้รับการแก้ไขไม่ว่าโครงสร้างจะมีขนาดใหญ่เพียงใดในขณะที่ต้นทุนในการส่งผ่านโครงสร้างที่ใหญ่ขึ้นจะเป็นสัดส่วนกับขนาดโครงสร้าง มีอะไรผิดปกติกับการใช้โครงสร้างขนาดใหญ่ (แม้หลายร้อย bytes) คือถ้าใครสามารถหลีกเลี่ยงการคัดลอกโดยไม่จำเป็น ; ในขณะที่มักจะป้องกันสำเนาที่ไม่จำเป็นได้เมื่อใช้วิธีการ แต่ก็ไม่สามารถป้องกันได้เมื่อใช้ตัวดำเนินการ


อืม ... นั่นอธิบายอะไรได้มากมาย! ดังนั้นหากตัวดำเนินการสั้นพอที่จะอยู่ในบรรทัดเดียวฉันถือว่าจะไม่ทำสำเนาโดยไม่จำเป็น แต่ถ้าไม่และโครงสร้างของคุณมีมากกว่าหนึ่งคำคุณอาจไม่ต้องการใช้มันเป็นตัวดำเนินการหากความเร็วเป็นสิ่งสำคัญ ขอบคุณสำหรับข้อมูลเชิงลึก
Brian Kennedy

BTW สิ่งหนึ่งที่ทำให้ฉันรำคาญเล็กน้อยเมื่อมีการตอบคำถามเกี่ยวกับความเร็วว่า "Benchmark it!" คือการตอบสนองดังกล่าวไม่สนใจข้อเท็จจริงที่ว่าในหลาย ๆ กรณีสิ่งที่สำคัญคือการดำเนินการมักใช้เวลา 10us หรือ 20us แต่การเปลี่ยนแปลงของสถานการณ์เล็กน้อยอาจทำให้ใช้เวลา 1ms หรือ 10ms สิ่งที่สำคัญไม่ใช่สิ่งที่ทำงานเร็วเพียงใดบนเครื่องของผู้พัฒนา แต่เป็นการทำงานที่ช้าพอที่จะมีความสำคัญหรือไม่ ถ้าวิธี X ทำงานเร็วกว่าวิธี Y สองเท่าในเครื่องส่วนใหญ่ แต่ในบางเครื่องจะช้ากว่า 100 เท่าวิธี Y อาจเป็นทางเลือกที่ดีกว่า
supercat

แน่นอนที่นี่เรากำลังพูดถึงแค่ 2 คู่ ... ไม่ใช่โครงสร้างขนาดใหญ่ การผ่านสองคู่บนสแต็กซึ่งสามารถเข้าถึงได้อย่างรวดเร็วไม่จำเป็นต้องช้าไปกว่าการส่ง 'this' บนสแต็กจากนั้นต้องหักล้างสิ่งนั้นเพื่อดึงพวกมันเข้ามาเพื่อทำงานกับพวกมัน .. แต่มันอาจทำให้เกิดความแตกต่างได้ อย่างไรก็ตามในกรณีนี้ควรมีการแทรกในบรรทัดดังนั้น JIT Optimizer ควรลงท้ายด้วยรหัสเดียวกัน
Brian Kennedy

1

ไม่แน่ใจว่าเกี่ยวข้องหรือไม่ แต่นี่คือตัวเลขสำหรับ. NET 4.0 64 บิตบน Windows 7 64 บิต เวอร์ชัน 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 ยังไม่ได้ทำในเครื่องมือเพิ่มประสิทธิภาพ JIT 64b ... อัตราส่วนของคุณยังคงใกล้เคียงกับของฉันมาก น่าผิดหวัง ... แต่ก็ดีที่ได้รู้
Brian Kennedy

0

ฉันจะนึกภาพเหมือนตอนที่คุณกำลังเข้าถึงสมาชิกของโครงสร้างว่า infact ทำการดำเนินการพิเศษเพื่อเข้าถึงสมาชิกตัวชี้นี้ + ออฟเซ็ต


1
ด้วยคลาสออบเจ็กต์คุณจะพูดถูกอย่างแน่นอน ... เพราะเมธอดจะถูกส่งผ่านตัวชี้ 'this' อย่างไรก็ตามด้วยโครงสร้างที่ไม่ควรเป็นเช่นนั้น โครงสร้างควรถูกส่งไปยังเมธอดบนสแต็ก ดังนั้นคู่แรกควรนั่งโดยที่ตัวชี้ "this" จะเป็นและคู่ที่สองอยู่ในตำแหน่งถัดจากนั้น ... ทั้งคู่อาจจะลงทะเบียนใน CPU ดังนั้น JIT ควรใช้ออฟเซ็ตมากที่สุด
Brian Kennedy

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.