なぜ演算子はメソッド呼び出しよりもはるかに遅いのですか?(構造体は古いJITでのみ遅くなります)


84

イントロ: 私はC#で高性能コードを書きます。はい、C ++を使用すると最適化が向上することはわかっていますが、それでもC#を使用することにしました。私はその選択について議論したくありません。むしろ、私のように、.NETFrameworkで高性能のコードを書き込もうとしている人たちから話を聞きたいと思います。

質問:

  • 以下のコードの演算子が同等のメソッド呼び出しよりも遅いのはなぜですか?
  • 以下のコードで2つのdoubleを渡すメソッドが、内部に2つのdoubleを持つ構造体を渡す同等のメソッドよりも速いのはなぜですか?(A:古いJITは構造体の最適化が不十分です)
  • .NET JITコンパイラに単純な構造体を構造体のメンバーと同じくらい効率的に処理させる方法はありますか?(A:新しいJITを入手してください)

私が知っていると思うこと: 元の.NET JITコンパイラは、構造体を含むものをインライン化しませんでした。奇妙な与えられた構造体は、組み込みのように最適化されるべきであるが本当である小さな値型が必要な場合にのみ使用されるべきです。幸い、.NET3.5SP1と.NET2.0SP2では、特に構造体のインライン化の改善など、JITオプティマイザーにいくつかの改善が加えられました。(そうでなければ、彼らが導入した新しいComplex構造体がひどく実行されたので、彼らはそれをしたと思います...したがって、ComplexチームはおそらくJIT Optimizerチームをドキドキしていました。)したがって、.NET 3.5SP1より前のドキュメントはおそらくこの問題にはあまり関係ありません。

私のテストが示すこと: C:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dllファイルのバージョンが3053以上であることを確認することで、新しいJITオプティマイザーがあることを確認しました。 JITオプティマイザに。しかし、それでも、私のタイミングと分解の両方が示すものは次のとおりです。

2つのdoubleを持つ構造体を渡すためのJIT生成コードは、2つのdoubleを直接渡すコードよりもはるかに効率が低くなります。

構造体メソッドのJIT生成コードは、引数として構造体を渡す場合よりもはるかに効率的に「this」を渡します。

明らかにループ内にあるため、乗数を使用しても、2つのdoubleを含む構造体を渡すよりも、2つのdoubleを渡す方が、JITのインライン化が向上します。

タイミング: 実際、逆アセンブルを見ると、ループ内のほとんどの時間はリストからテストデータにアクセスしているだけであることがわかります。ループのオーバーヘッドコードとデータへのアクセスを考慮に入れると、同じ呼び出しを行う4つの方法の違いは劇的に異なります。PlusEqual(Element)の代わりにPlusEqual(double、double)を実行すると、5倍から20倍のスピードアップが得られます。また、演算子+ =の代わりにPlusEqual(double、double)を実行する場合は10倍から40倍になります。ワオ。悲しい。

タイミングのセットは次のとおりです。

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の良い質問がどのように見えるかの例として参照する必要があります!自動生成されたコメントのみを省略できます。残念ながら、私は実際に問題に飛び込むにはほとんど知りませんが、私は質問が本当に好きです!
デニストラウブ2011

2
ユニットテストはベンチマークを実行するのに適した場所ではないと思います。
ヘンクホルターマン2011

1
なぜ構造体は2つのdoubleよりも速くなければならないのですか?.NETでは、構造体はそのメンバーのサイズの合計と決して等しくなりません。したがって、定義上、それは大きいので、定義上、スタックへのプッシュを遅くする必要があり、次に2つのdouble値のみを使用する必要があります。コンパイラが行2のダブルメモリに構造体パラメータをインライン化する場合、メソッド内でリフレクションを使用してその構造体にアクセスする場合はどうでしょうか。その構造体オブジェクトにリンクされているランタイム情報はどこにありますか?そうではないか、何かが足りないのですか?
Tigran 2011

3
@Tigran:これらの主張の情報源が必要です。私はあなたが間違っていると思います。値型がボックス化された場合にのみ、メタデータを値とともに保存する必要があります。静的構造体型の変数では、オーバーヘッドはありません。
Ben Voigt 2011

1
足りないのは組み立てだけだと思っていました。そして今、あなたはそれを追加しました(それはx86アセンブラーでありMSILではないことに注意してください)。
Ben Voigt 2011

回答:


9

私は非常に異なる結果を得ていますが、それほど劇的ではありません。しかし、テストランナーを使用せず、コードをコンソールモードアプリに貼り付けました。5%の結果は、32ビットモードで約87%、64ビットモードで約100%です。

ダブルスではアライメントが重要です。.NETランタイムは、32ビットマシンでは4のアライメントしか約束できません。テストランナーは、8ではなく4にアラインされたスタックアドレスでテストメソッドを開始しているように見えます。ダブルがキャッシュラインの境界を超えると、ミスアラインメントのペナルティが非常に大きくなります。


なぜ.NETは基本的に4つのダブルのアライメントで成功できるのですか?アラインメントは、32ビットマシンで4バイトのチャンクを使用して行われます。そこの問題は何ですか?
Tigran 2011年

ランタイムがx86で4バイトにしか整列しないのはなぜですか?アンマネージコードがマネージコードを呼び出すときにさらに注意を払えば、64ビットにアラインできると思います。仕様には弱いアライメント保証しかありませんが、実装はより厳密にアライメントできるはずです。(仕様:「8バイトデータは、ネイティブintへのアトミックアクセスのために基盤となるハードウェアが必要とする同じ境界に格納されている場合、適切に配置されます」)
CodesInChaos 2011年

1
@ Code-そうですね、Cコードジェネレーターは、関数プロローグのスタックポインターで計算を行うことでこれを実行できます。x86ジッターはそうではありません。スタックへの配列の割り当てははるかに一般的であり、8に整列するヒープアロケーターがあるため、スタック割り当ての効率をヒープ割り当てよりも低くしたくないため、ネイティブ言語にとってははるかに重要です。32ビットのgcヒープからの4のアラインメントで立ち往生しています。
ハンスパッサント2011年

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タイプを記載してください。
ヘンクホルターマン2011

とても興味深い!私は他の人に私の結果を確認してもらいました...あなたは最初に違うものになります。あなたへの最初の質問:私の投稿で言及したファイルのバージョン番号は何ですか... C:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ...それはMicrosoftのドキュメントが示しているものですお持ちのJITオプティマイザーのバージョン。(ユーザーに.NETをアップグレードして大幅なスピードアップを確認するように指示できれば、私は幸せなキャンピングカーになります。しかし、それほど単純ではないと思います。)
Brian Kennedy

VisualStudio内で実行していました... Windows XPSP3で実行しています... VMware仮想マシンで... 2.7GHz Intel Corei7で実行しています。しかし、私が興味を持っているのは絶対的な時間ではありません...それは比率です...これらの3つの方法はすべて、Coreyの場合と同じように機能すると思いますが、私にとってはそうではありません。
ブライアンケネディ

私のプロジェクトのプロパティは次のように言っています:構成:リリース; プラットフォーム:アクティブ(x86); プラットフォームターゲット:x86
Corey Kosak 2011

1
mscorwksのバージョンを取得するためのリクエストについて...申し訳ありませんが、.NET 2.0に対してこれを実行しますか?私のテストは
.NET4.0で

3

ここで.NET4.0を実行します。リリースモードで.NET4.0をターゲットにして、「AnyCPU」でコンパイルしました。実行はコマンドラインからでした。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の問題が何であれ、.NET4.0には存在しないようです。


2
はい、Structsの答えは「新しいJITを入手する」のようです。しかし、ヘンクの答えについて尋ねたところ、なぜメソッドは演算子よりもはるかに高速なのですか?どちらの方法も、まったく同じことをしている演算子のどちらよりも5倍高速です。再び構造体を使用できるのは素晴らしいことです...しかし、それでも演算子を避けなければならないのは悲しいことです。
ブライアンケネディ

ジム、システム上のファイルC:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dllのバージョンを知りたいと思います...私のもの(.3620)よりも新しいが、古い場合Corey(.5446)よりも、それはあなたのオペレーターが私のようにまだ遅い理由を説明するかもしれませんが、Coreyのものはそうではありません。
ブライアンケネディ

@Brian:ファイルバージョン2.0.50727.4214。
ジムミッシェル2011

ありがとう!したがって、構造体の最適化を取得するにはユーザーが4214以降を使用し、演算子の最適化を取得するには5446以降を使用していることを確認する必要があります。起動時にそれを確認し、いくつかの警告を出すために、いくつかのコードを追加する必要があります。再度、感謝します。
ブライアンケネディ

2

@Corey Kosakと同様に、このコードをVS 2010Expressでリリースモードの単純なコンソールアプリとして実行しました。私は非常に異なる数を取得します。しかし、私は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%.

編集:そして今度はコマンドラインから実行します。それは違いを生み、数の変動が少なくなります。


はい、後のJITで構造体の問題が修正されたようですが、メソッドが演算子よりもはるかに高速である理由についての私の質問は残っています。両方のPlusEqualメソッドが同等の+ =演算子よりもどれだけ速いかを見てください。そして、-=が+ =よりどれだけ速いかということも興味深いです...あなたのタイミングは私がそれを見た最初のものです。
ブライアンケネディ

ヘンク、システム上のファイルC:\ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dllのバージョンを知りたいと思います...私のもの(.3620)よりも新しいが、古い場合Corey(.5446)よりも、それはあなたのオペレーターが私のようにまだ遅い理由を説明するかもしれませんが、Coreyのものはそうではありません。
ブライアンケネディ

1
.50727バージョンしか見つかりませんが、それがFx40 / Fx45に関連しているかどうかわかりませんか?
ヘンクホルターマン2011

残りのバージョン番号を表示するには、[プロパティ]に移動して[バージョン]タブをクリックする必要があります。
ブライアンケネディ

2

他の回答で言及されているJITコンパイラの違いに加えて、構造体メソッド呼び出しと構造体演算子の別の違いは、構造体メソッド呼び出しがパラメータthisとして渡されるrefことです(また、他のパラメータをrefパラメータとして受け入れるように記述される場合もあります)。構造体演算子は、すべてのオペランドを値で渡します。ref構造がどれほど大きくても、任意のサイズの構造をパラメーターとして渡すためのコストは固定されていますが、より大きな構造を渡すためのコストは構造のサイズに比例します。大きな構造(数百バイトでも)を不必要にコピーすることを避けることができれば、それを使用しても問題はありません。メソッドを使用する場合は不要なコピーを防ぐことができますが、演算子を使用する場合は防ぐことができません。


うーん...まあ、それは多くのことを説明することができます!したがって、演算子がインライン化されるほど短い場合は、不要なコピーは作成されないと思います。ただし、そうでなく、構造体が複数の単語である場合、速度が重要な場合は、演算子として実装したくない場合があります。その洞察に感謝します。
ブライアンケネディ

ところで、速度に関する質問に「ベンチマーク」と答えると、少しイライラすることが1つあります。そのような応答は、多くの場合、操作に通常10usまたは20usかかるかどうかが重要であるという事実を無視しますが、状況のわずかな変化によって1msまたは10msかかる可能性があるかどうかです。重要なのは、開発者のマシンで何かがどれだけ速く実行されるかではなく、操作が問題になるほど遅くなるかどうかです。ほとんどのマシンでメソッドXがメソッドYの2倍の速度で実行されるが、一部のマシンでは100倍遅い場合は、メソッドYの方が適している可能性があります。
スーパーキャット2015

もちろん、ここでは2つのダブルについて話しているだけです...大きな構造体ではありません。すばやくアクセスできるスタックで2つのdoubleを渡すことは、スタックで「this」を渡し、それを逆参照してそれらを操作するよりも必ずしも遅いとは限りません。ただし、違いが生じる可能性があります。ただし、この場合はインライン化する必要があるため、JITオプティマイザーはまったく同じコードになります。
ブライアンケネディ

1

これが関連するかどうかはわかりませんが、Windows 764ビット上の.NET4.064ビットの数値は次のとおりです。私の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
興味深い... 32bJITオプティマイザーに追加された最適化はまだ64bJITオプティマイザーに到達していないようです...あなたの比率はまだ私のものと非常に似ています。がっかり...でも知っておくといい。
ブライアンケネディ

0

構造体のメンバーにアクセスしているときのように、実際にはメンバーにアクセスするための追加の操作、このポインター+オフセットを実行していると想像します。


1
そうですね、クラスオブジェクトを使用すると、絶対に正しいでしょう...メソッドには「this」ポインタが渡されるだけだからです。ただし、構造体の場合はそうではありません。構造体はスタック上のメソッドに渡される必要があります。したがって、最初のダブルは「this」ポインタがある場所に配置し、2番目のダブルはその直後の位置に配置する必要があります...両方ともCPUのレジスタである可能性があります。したがって、JITはせいぜいオフセットを使用する必要があります。
ブライアンケネディ

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.