20

この質問は、インクリメント演算子とプレフィックス/ポストフィックス表記の速度の違いに関するものなので、Eric Lippertがそれを発見して私を怒らせないように、質問を非常に注意深く説明します。

(私が尋ねている理由の詳細と詳細については、http://www.codeproject.com/KB/cs/FastLessCSharpIteration.aspx?msg = 3899456#xx3899456xx /を参照してください)

私は次のように4つのコードスニペットを持っています:-

(1)個別、プレフィックス:

    for (var j = 0; j != jmax;) { total += intArray[j]; ++j; }

(2)個別、後置:

    for (var j = 0; j != jmax;) { total += intArray[j]; j++; }

(3)インデクサー、Postfix:

    for (var j = 0; j != jmax;) { total += intArray[j++]; }

(4)インデクサー、プレフィックス:

    for (var j = -1; j != last;) { total += intArray[++j]; } // last = jmax - 1

私がやろうとしていたのは、このコンテキストでプレフィックス表記とポストフィックス表記の間にパフォーマンスの違いがあるかどうか(つまり、ローカル変数が揮発性ではない、別のスレッドから変更できないなど)があるかどうかを証明/反証することでした。 。

速度テストはそれを示しました:

  • (1)と(2)は同じ速度で動作します。

  • (3)と(4)は同じ速度で動作します。

  • (3)/(4)は(1)/(2)よりも約27%遅くなります。

したがって、後置記法自体よりも前置記法を選択することによるパフォーマンス上の利点はないと結論付けています。ただし、操作の結果を実際に使用すると、コードが単純に破棄される場合よりもコードが遅くなります。

次に、Reflectorを使用して生成されたILを調べたところ、次のことがわかりました。

  • ILバイト数はす​​べての場合で同じです。

  • .maxstackは4から6の間で変化しましたが、これは検証目的でのみ使用されるため、パフォーマンスには関係ないと思います。

  • (1)と(2)はまったく同じILを生成したので、タイミングが同じであることは当然です。したがって、(1)は無視できます。

  • (3)と(4)は、非常によく似たコードを生成しました。唯一の関連する違いは、操作の結果を説明するためのdupオペコードの配置です。繰り返しますが、タイミングが同じであることについては驚くことではありません。

そこで、(2)と(3)を比較して、速度の違いを説明できるものを見つけました。

  • (2)ldloc.0 opを2回使用します(1回はインデクサーの一部として、その後は増分の一部として)。

  • (3)ldloc.0を使用し、直後にdupopを使用しました。

したがって、(1)(および(2))のjの増分に関連するILは次のとおりです。

// ldloc.0 already used once for the indexer operation higher up
ldloc.0
ldc.i4.1
add
stloc.0

(3)次のようになります。

ldloc.0
dup // j on the stack for the *Result of the Operation*
ldc.i4.1
add
stloc.0

(4)は次のようになります。

ldloc.0
ldc.i4.1
add
dup // j + 1 on the stack for the *Result of the Operation*
stloc.0

今(ついに!)質問に:

JITコンパイラはldloc.0/ldc.i4.1/add/stloc.0、ローカル変数を1ずつインクリメントするだけのパターンを認識し、それを最適化するため、(2)は高速ですか?(そして、dup(3)と(4)の存在はそのパターンを壊すので、最適化は失われます)

そして補足:これが本当なら、少なくとも(3)については、dupを別のものに置き換えてldloc.0そのパターンを再導入しませんか?

4

3 に答える 3

10

多くの調査の結果(悲しいことに私は知っています!)、私は自分の質問に答えたと思います:

答えはたぶんです。どうやら、JITコンパイラはパターンを探して(http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspxを参照)決定します配列境界チェックをいつどのように最適化できるかはわかりませんが、それが私が推測していたのと同じパターンであるかどうかはわかりません。

この場合、(2)の相対速度の増加はそれ以上の原因によるものであるため、これは重要なポイントです。x64 JITコンパイラは、配列の長さが一定であるかどうか(および、ループ内の展開数の倍数でもあるように見える)を判断するのに十分賢いことがわかります。したがって、コードは、各反復の終了時に境界チェックのみを行い、各展開はちょうどになりました:-

        total += intArray[j]; j++;
00000081 8B 44 0B 10          mov         eax,dword ptr [rbx+rcx+10h] 
00000085 03 F0                add         esi,eax 

アプリを変更してコマンドラインで配列サイズを指定できるようにし、別のアセンブラー出力を確認することで、これを証明しました。

この演習中に発見された他の事柄:-

  • スタンドアロンのインクリメント操作(つまり、結果が使用されない)の場合、プレフィックスとポストフィックスの間に速度の違いはありません。
  • インデクサーでインクリメント操作を使用すると、アセンブラーはプレフィックス表記がわずかに効率的であることを示します(元のケースでは、タイミングの不一致であると想定し、それらを等しいと呼んだので、非常に近いです-私の間違いです)。x86としてコンパイルすると、違いはより顕著になります。
  • ループ展開は機能します。配列境界が最適化された標準ループと比較して、4つのロールアップでは常に10%〜20%(およびx64 /定数の場合は34%)の改善が見られました。ロールアップの数を増やすと、タイミングが変化し、インデクサーの接尾辞の場合は非常に遅くなるため、展開する場合は4を使用し、特定の場合はタイミングを大きくしてから変更します。
于 2011-05-22T18:21:35.517 に答える
8

興味深い結果。私がすることは:

  • アプリケーションを書き直して、テスト全体を2回実行します。
  • 2つのテスト実行の間にメッセージボックスを配置します。
  • リリース用にコンパイルし、最適化しないなど。
  • デバッガの外部で実行可能ファイルを起動します。
  • メッセージボックスが表示されたら、デバッガーを接続します
  • 次に、ジッターによって2つの異なるケースに対して生成されたコードを調べます。

そして、ジッターが一方に対して他方よりも優れた仕事をしているかどうかがわかります。ジッタは、たとえば、ある場合には配列境界チェックを削除できることを認識しているかもしれませんが、他の場合にはそれを認識していない可能性があります。知らない; 私はジッターの専門家ではありません。

すべてのリガマロールの理由は、デバッガーが接続されているときにジッターが異なるコードを生成する可能性があるためです。通常の状況でそれが何をするのかを知りたい場合は、通常の非デバッガーの状況でコードがジッターされることを確認する必要があります。

于 2011-05-20T22:11:43.910 に答える
7

私はパフォーマンステストが大好きで、高速プログラムが大好きなので、あなたの質問に感心します。

私はあなたの発見を再現しようとしましたが失敗しました。x86|Release構成の.NET4フレームワークでコードサンプルを実行しているInteli7x64システムでは、4つのテストケースすべてでほぼ同じタイミングが生成されました。

テストを行うために、新しいコンソールアプリケーションプロジェクトを作成し、QueryPerformanceCounterAPI呼び出しを使用して高解像度のCPUベースのタイマーを取得しました。私は2つの設定を試しましたjmax

  • jmax = 1000
  • jmax = 1000000

配列の局所性は、パフォーマンスの動作に大きな違いをもたらすことが多く、ループのサイズが大きくなるためです。ただし、私のテストでは、両方の配列サイズが同じように動作しました。

私は多くのパフォーマンスの最適化を行いましたが、私が学んだことの1つは、アプリケーションを非常に簡単に最適化して、特定のコンピューターでの実行速度を上げ、別のコンピューターでの実行速度を不注意に遅くすることができるということです。

私はここで仮想的に話しているのではありません。私は内部ループを微調整し、プログラムをより高速に実行するために何時間も何日も費やしましたが、ワークステーションで最適化していて、ターゲットコンピューターがIntelプロセッサの異なるモデルであったため、希望が打ち砕かれました。

したがって、この話の教訓は次のとおりです。

  • コードスニペット(2)は、コンピューターではコードスニペット(3)よりも高速に実行されますが、私のコンピューターでは実行されません。

これが、サポートされているすべてのハードウェアで1つのバージョンを簡単に実行できる場合でも、一部のコンパイラに異なるプロセッサ用の特別な最適化スイッチがあるか、一部のアプリケーションが異なるバージョンで提供される理由です。

したがって、このようなテストを行う場合は、JITコンパイラの作成者と同じ方法でテストを行う必要があります。さまざまなハードウェアでテストを実行してから、ブレンドを選択する必要があります。最も普及しているハードウェアでのパフォーマンス。

于 2011-05-20T21:38:23.733 に答える