2017-08-29 3 views
10

コード:同じusingsで同じコードブロックの実行時間が非常に異なる理由は何ですか?

internal class Program 
{ 
    private static void Main(string[] args) 
    { 
     const int iterCount = 999999999; 

     var sum1 = 0; 
     var sum2 = 0; 

     using (new Dis()) 
     { 
      var sw = DateTime.Now; 
      for (var i = 0; i < iterCount; i++) 
       sum1 += i; 
      Console.WriteLine(sum1); 
      Console.WriteLine(DateTime.Now - sw); 
     } 

     using (new Dis()) 
     { 
      var sw = DateTime.Now; 
      for (var i = 0; i < iterCount; i++) 
       sum2 += i; 
      Console.WriteLine(sum2); 
      Console.WriteLine(DateTime.Now - sw); 
     } 

     Console.ReadLine(); 
    } 

    private class Dis : IDisposable 
    { 
     public void Dispose(){} 
    } 
} 

2つの同一ブロック。

出力:

2051657985 
00:00:00.3690996 
2051657985 
00:00:02.2640266 

第2のブロックが2.2秒かかります!しかし、使用を取り除く場合、期間は同じになりました(最初のものと同様0.3秒)。 私は.NET Framework 4.5と.net core 1.1で試してみましたが、リリースでは結果は同じです。

誰でもその動作を説明できますか?

+2

'Console.WriteLine'の代わりに変数を使ってみてください。それは問題かもしれません。 – ispiro

+0

私はあなたのコードをlinqpadで実行すると、両方のコードブロックが〜2.5秒ということになります。理想的に実行すると、両方のブロックで約0.3秒が得られます:https://ideone.com/ReOpaH。 – Chris

+2

また、適切なストップウォッチを使用してください。ストップウオッチを使用したいが、あなたはストップウォッチを使用しないことを意味するdatetime変数swを呼び出します。 – Chris

答えて

13

ジッタが生成するマシンコードを調べて、根本的な理由を確認する必要があります。 [ツール]> [オプション]> [デバッグ]> [一般]を選択し、[JIT最適化を抑制する]オプションを選択解除します。リリースビルドに切り替えます。最初と2番目のループにブレークポイントを設定します。ヒットしたら、[デバッグ]> [Windows]> [逆アセンブリ]を使用します。

    sum1 += i; 
00000035 add   esi,eax 

そして:あなたがforループのボディのためのマシンコードが表示されます

    sum2 += i; 
000000d9 add   dword ptr [ebp-24h],eax 

換言すれば、sum1変数は、CPUレジスタesiに格納されます。しかし、sum2変数は、メソッドのスタックフレーム上のメモリに格納されます。大きな、大きな違い。レジスタは非常に高速で、メモリは低速です。スタックフレームのメモリはL1キャッシュ内にあり、そのキャッシュにアクセスする最新のマシンでは3サイクルのレイテンシがあります。ストアバッファは、大量の書き込みによってすぐに圧倒され、プロセッサがストールします。

CPUレジスタに変数を保持する方法を見つけることはone of the primary jitter optimization dutiesです。しかし、それには限界があります。特に、x86では利用可能なレジスタがほとんどありません。それらがすべて使い切られている場合、ジッタにはオプションがありませんが、代わりにメモリを使用します。 usingステートメントには、フードの下に追加の隠しローカル変数があることに注意してください。

理想的には、ジッタオプティマイザはレジスタの割り当て方法をより適切に選択するでしょう。それらをループ変数(それが行ったもの)と合計変数に使用します。先験的コンパイラは、コード解析を実行するのに十分な時間を持って、その権利を取得します。しかし、Just-In-Timeコンパイラは厳密な時間制約の下で動作します。

基本的な対策は以下のとおりです。ESIのようなレジスタを再使用できるように

    は別々のメソッドにコードを分割
  • ジッタ強制を削除します([プロジェクト]> [プロパティ]> [ビルド]タブ> [32ビット優先]を選択解除します)。 x64には8つの追加レジスタがあります。

最後の箇条書きは、従来のx64ジッタ(ターゲット.NET 3.5で使用する)では有効ですが、4.6で最初に使用されたx64ジッタ書き換え(RYuJIT)では有効ではありません。レガシージッタがコードを最適化するのに時間がかかり過ぎたために必要だった書き換え。失望しているRyuJITは失望しています。オプティマイザはここでより良い仕事をすることができると思います。

+0

ありがとう、答えのように見えます!興味深いことに、ブロックの使用の間にsum2(var sum2 = 0;)の宣言を置くと、両方のループでレジスタが使用されます。 –

関連する問題