เหตุใดโอเปอเรเตอร์จึงช้ากว่าการเรียกใช้เมธอดมาก (โครงสร้างจะช้าลงเฉพาะใน 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.