为什么运算符比方法调用慢得多?(仅在较旧的JIT上结构较慢)


84

简介: 我用C#编写高性能的代码。是的,我知道C ++可以为我提供更好的优化,但是我仍然选择使用C#。我不想辩论这种选择。相反,我想听听那些像我一样试图在.NET Framework上编写高性能代码的人。

问题:

  • 为什么下面代码中的运算符比等效方法调用慢?
  • 为什么在下面的代码中传递两个双精度值的方法比在内部传递两个双精度值的结构的等效方法更快?(A:较旧的JIT对结构的优化效果很差)
  • 是否有办法让.NET JIT编译器将简单结构与结构成员一样有效?(A:获取更新的JIT)

我想知道的是: 原始的.NET JIT编译器不会内联任何涉及结构的内容。给定的奇异结构仅应在需要小值类型(如内置)进行优化但适用的情况下使用。幸运的是,在.NET 3.5SP1和.NET 2.0SP2中,他们对JIT Optimizer进行了一些改进,包括对内联的改进,尤其是对结构的改进。(我猜他们是这样做的,因为否则,他们引入的新Complex结构会表现得很糟糕……因此Complex团队可能正在对JIT Optimizer团队加紧努力。)因此,.NET 3.5 SP1之前的任何文档都可能与这个问题不太相关。

我的测试显示: 通过检查C:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll文件确实具有版本> = 3053,我已验证自己具有更新的JIT Optimizer,因此应该进行这些改进到JIT优化器。但是,尽管如此,我的时间安排和对拆卸的观察都显示了:

JIT生成的用于传递具有两个双精度的结构的代码的效率远远低于直接传递两个双精度的代码的效率。

JIT生成的用于struct方法的代码比通过struct作为参数传递“ this”的效率要高得多。

如果传递两个双精度值而不是传递具有两个双精度值的结构,即使使用乘数(由于明显处于循环状态),JIT仍会更好地内联。

时间: 实际上,看一下反汇编,我意识到循环中的大多数时间只是从列表中访问测试数据。如果不考虑循环和数据访问的开销代码,则进行相同调用的四种方式之间的差异将大大不同。对于PlusEqual(double,double)而不是PlusEqual(Element),我得到5倍到20倍的提速。10倍到40倍用于执行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 :(又名以上部分内容编译成的内容)

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
我认为单元测试不是运行基准测试的好地方。
Henk Holterman

1
为什么结构必须更快然后再加倍?在.NET中,struct永远不等于其成员大小的总和。因此,根据定义,它更大,因此根据定义,它在推入堆栈时必须较慢,然后只有2个double值。如果编译器将第2行双倍内存中的struct参数内联,那么如果要在内部方法中使用反射访问该结构该怎么办。链接到该struct对象的运行时信息在哪里?是不是,或者我想念什么?
提格伦

3
@Tigran:您需要这些声明的来源。我想你错了。仅当将值类型装箱时,才需要将元数据与值一起存储。在具有静态结构类型的变量中,没有开销。
Ben Voigt

1
我在想,唯一缺少的就是组装。现在,您已经添加了(请注意,这是x86汇编程序,而不是MSIL)。
Ben Voigt

Answers:


9

我得到的结果截然不同,戏剧性更差。但是没有使用测试运行程序,我将代码粘贴到了控制台模式应用程序中。尝试时,5%的结果在32位模式下为〜87%,在64位模式下为〜100%。

对齐对于双精度至关重要,.NET运行时只能保证在32位计算机上对齐4。在我看来,测试运行程序正在使用对齐到4而不是8的堆栈地址启动测试方法。当双精度项超过缓存行边界时,未对齐代价将变得非常大。


为什么.NET基本上只能在4个双精度对齐的情况下成功?通过在32位计算机上使用4个字节的块来完成对齐。那里有什么问题?
提格伦

为什么运行时在x86上只能对齐4个字节?我认为,如果在非托管代码调用托管代码时要格外小心,它可以与64位对齐。虽然规范仅具有较弱的对齐保证,但实现应能够更严格地对齐。(规范:“当将8字节的数据存储在底层硬件要求以原子方式访问本机int所需的相同边界上时,将正确对齐”)
CodesInChaos 2011年

1
@Code-好吧,C代码生成器可以通过对函数序言中的堆栈指针进行数学运算来实现此目的。x86抖动只是没有。这是很多的,因为在栈上分配阵列母语更重要的是要普遍得多,他们有一个堆分配是对齐8,这样就不会想使堆栈分配比堆分配效率较低。我们在32位gc堆中的对齐方式为4。
汉斯·帕桑

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来看到大幅度的提速,我将是一个快乐的露营者。但是我猜它不会那么简单。)
Brian Kennedy

我在Visual Studio中运行,在Windows XP SP3上运行,在VMware虚拟机中,运行在2.7GHz Intel Core i7上。但是这并不是让我感兴趣的绝对时间...而是比率...我希望这三种方法的性能都相似,这对Corey来说是一样的,但对我而言却不是。
布莱恩·肯尼迪

我的项目属性是:配置:发布;平台:有效(x86);平台目标:x86
Corey Kosak 2011年

1
关于获取mscorwks版本的请求...对不起,您是否要我针对.NET 2.0运行此程序?我的测试是在.NET 4.0上进行的
Corey Kosak 2011年

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”。但是,正如我在Henk的回答中所问的那样,为什么方法要比Operators这么快?两种方法都比任何一种运算符快5倍...它们的作用完全相同。我可以再次使用结构真是太好了……但是很遗憾,我仍然必须避免使用运算符。
布莱恩·肯尼迪

吉姆,我非常想知道您系统上文件C:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll的版本...如果比我的(.3620)更新,但是更旧而不是Corey(.5446),那么这也许可以解释为什么您的操作员仍然像我的一样慢,而Corey却不是。
布莱恩·肯尼迪

@Brian:文件版本2.0.50727.4214。
Jim Mischel

谢谢!因此,我需要确保我的用户具有4214或更高版本来进行结构优化,并具有5​​446或更高版本来进行操作员优化。我需要添加一些代码以在启动时进行检查并给出一些警告。再次感谢。
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似乎解决了struct问题,但是我的问题仍然是为什么方法比运算符要快得多。看看这两个PlusEqual方法比等效的+ =运算符快多少。而且有趣的是-=比+ =快多少……您的计时是我所看到的第一个。
布莱恩·肯尼迪

Henk,我非常想知道您系统上文件C:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll的版本...如果比我的(.3620)更新,但是更旧而不是Corey(.5446),那么这也许可以解释为什么您的操作员仍然像我的一样慢,而Corey却不是。
布莱恩·肯尼迪

1
我只能找到.50727版本,但不确定是否与Fx40 / Fx45有关?
Henk Holterman

您必须进入“属性”,然后单击“版本”选项卡以查看其余的版本号。
布莱恩·肯尼迪

2

除了在其他答案中提到的JIT编译器差异之外,struct方法调用与struct运算符之间的另一个区别是,struct方法调用将this作为ref参数传递(并且可以编写为接受其他参数作为ref参数),而struct运算符将按值传递所有操作数。ref无论结构多大,传递任何大小的结构作为参数的成本都是固定的,而传递较大的结构的成本与结构大小成正比。如果可以避免不必要地复制大型结构(即使是数百个字节),没有任何问题。尽管使用方法通常可以避免不必要的复制,但使用运算符则无法防止不必要的复制。


嗯...那可以解释很多!因此,如果运算符足够短以至于可以内联,则我认为它不会产生不必要的副本。但是如果不是这样,并且您的结构超过一个单词,那么在速度至关重要的情况下,您可能不想将其实现为运算符。感谢您的见解。
布莱恩·肯尼迪

顺便说一句,当回答关于速度的问题时,有一件事让我稍微有些烦恼。是这样的响应忽略了以下事实:在许多情况下,重要的是操作通常需要10us还是20us,但是情况的轻微变化是否可能导致操作花费1ms或10ms。重要的不是开发人员在计算机上运行的速度有多快,而是操作是否足够慢才变得重要。如果在大多数计算机上方法X的运行速度是方法Y的两倍,但是在某些计算机上,它的运行速度将是方法Y的100倍,则方法Y可能是更好的选择。
2015年

当然,这里我们只讨论2个double,而不是大型结构。在堆栈上传递两个可以快速访问的双精度数据并不一定比在堆栈上传递“ this”然后再取消引用该值以拉入它们以对其进行操作..要慢,但这可能会导致差异。但是,在这种情况下,应该对其进行内联,因此JIT Optimizer应该以完全相同的代码结束。
布莱恩·肯尼迪

1

不知道这是否相关,但这是Windows 7 64位上的.NET 4.0 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的优化还没有加入到64b JIT Optimizer中……您的比率仍然与我的比率非常相似。令人失望...但是很高兴知道。
布莱恩·肯尼迪

0

我可以想象,当您访问该结构的成员时,实际上是在做一个额外的操作来访问该成员,即THIS指针+偏移量。


1
好吧,对于类对象,您绝对是正确的……因为该方法将仅通过“ this”指针传递。但是,对于结构而言,事实并非如此。该结构应传递到堆栈上的方法中。因此,第一个双精度数应该位于“ this”指针所在的位置,而第二个双精度数应该位于它之后的位置...两者都可能是CPU中的寄存器。因此,JIT最多只能使用偏移量。
布莱恩·肯尼迪

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.