web-dev-qa-db-ja.com

32ビットと64ビットのコンパイル時のパフォーマンスの大きな違い(26倍高速)

値型と参照型のリストにアクセスするときに、forforeachの使用の違いを測定しようとしていました。

次のクラスを使用してプロファイリングを行いました。

_public static class Benchmarker
{
    public static void Profile(string description, int iterations, Action func)
    {
        Console.Write(description);

        // Warm up
        func();

        Stopwatch watch = new Stopwatch();

        // Clean up
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            func();
        }
        watch.Stop();

        Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations);
    }
}
_

値の型にdoubleを使用しました。そして、参照タイプをテストするためにこの「偽のクラス」を作成しました。

_class DoubleWrapper
{
    public double Value { get; set; }

    public DoubleWrapper(double value)
    {
        Value = value;
    }
}
_

最後に、このコードを実行して、時間差を比較しました。

_static void Main(string[] args)
{
    int size = 1000000;
    int iterationCount = 100;

    var valueList = new List<double>(size);
    for (int i = 0; i < size; i++) 
        valueList.Add(i);

    var refList = new List<DoubleWrapper>(size);
    for (int i = 0; i < size; i++) 
        refList.Add(new DoubleWrapper(i));

    double dummy;

    Benchmarker.Profile("valueList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < valueList.Count; i++)
        {
             unchecked
             {
                 var temp = valueList[i];
                 result *= temp;
                 result += temp;
                 result /= temp;
                 result -= temp;
             }
        }
        dummy = result;
    });

    Benchmarker.Profile("valueList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in valueList)
        {
            var temp = v;
            result *= temp;
            result += temp;
            result /= temp;
            result -= temp;
        }
        dummy = result;
    });

    Benchmarker.Profile("refList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < refList.Count; i++)
        {
            unchecked
            {
                var temp = refList[i].Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }
        dummy = result;
    });

    Benchmarker.Profile("refList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in refList)
        {
            unchecked
            {
                var temp = v.Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }

        dummy = result;
    });

    SafeExit();
}
_

Releaseおよび_Any CPU_オプションを選択し、プログラムを実行して次の時間を取得しました。

_valueList for:  average time: 483,967938 ms
valueList foreach:  average time: 477,873079 ms
refList for:  average time: 490,524197 ms
refList foreach:  average time: 485,659557 ms
Done!
_

次に、リリースオプションとx64オプションを選択し、プログラムを実行して、次の時間を取得しました。

_valueList for:  average time: 16,720209 ms
valueList foreach:  average time: 15,953483 ms
refList for:  average time: 19,381077 ms
refList foreach:  average time: 18,636781 ms
Done!
_

X64ビットバージョンの方がずっと速いのはなぜですか?多少の違いはあると思っていましたが、それほど大きなものではありませんでした。

他のコンピューターにアクセスできません。マシンでこれを実行して、結果を教えてください。 Visual Studio 2015を使用していますが、Intel Core i7 930があります。

以下はSafeExit()メソッドです。自分でコンパイル/実行できます:

_private static void SafeExit()
{
    Console.WriteLine("Done!");
    Console.ReadLine();
    System.Environment.Exit(1);
}
_

要求に応じて、DoubleWrapperの代わりに_double?_を使用します。

任意のCPU

_valueList for:  average time: 482,98116 ms
valueList foreach:  average time: 478,837701 ms
refList for:  average time: 491,075915 ms
refList foreach:  average time: 483,206072 ms
Done!
_

x64

_valueList for:  average time: 16,393947 ms
valueList foreach:  average time: 15,87007 ms
refList for:  average time: 18,267736 ms
refList foreach:  average time: 16,496038 ms
Done!
_

最後になりましたが、_x86_プロファイルを作成すると、_Any CPU_を使用した場合とほぼ同じ結果が得られます。

78
Trauer

これを4.5.2で再現できます。ここにはRyuJITはありません。 x86とx64の両方の分解は妥当に見えます。範囲チェックなどは同じです。同じ基本構造。ループの展開はありません。

x86は、異なるフロート命令セットを使用します。これらの命令のパフォーマンスは、x64命令の除算を除き、同等です。

  1. 2ビットx87フロート命令は、内部で10バイトの精度を使用します。
  2. 拡張精度除算は非常に遅いです。

除算操作により、32ビットバージョンは非常に遅くなります。除算のコメントを外すと、パフォーマンスが均等になります(430msから3.25msに32ビットダウン)。

Peter Cordes氏は、2つの浮動小数点ユニットの命令レイテンシはそれほど似ていないと指摘しています。中間結果の一部は、非正規化された数値またはNaNである可能性があります。これらは、いずれかのユニットで低速パスをトリガーする可能性があります。または、10バイトと8バイトの浮動小数点精度のために、2つの実装間で値が異なる場合があります。

Peter Cordes all中間結果がNaNであることも指摘しています ...この問題を除去すると(valueList.Add(i + 1)で除数がゼロにならないため)、結果はほぼ等しくなります。どうやら、32ビットコードはNaNオペランドがまったく好きではありません。いくつかの中間値を印刷してみましょう:if (i % 1000 == 0) Console.WriteLine(result);。これにより、データが正常になったことを確認できます。

ベンチマークを行う場合、現実的なワークロードのベンチマークを行う必要があります。しかし、無邪気な部門があなたのベンチマークを台無しにできると誰が思っただろうか?!

単純に数値を合計して、より良いベンチマークを取得してください。

除算とモジュロは常に非常に遅いです。 BCL Dictionaryコードを変更して、単純にモジュロ演算子を使用してバケットインデックスを計算しない場合、測定可能なパフォーマンスが向上します。これは、除算が遅い方法です。

32ビットコードは次のとおりです。

enter image description here

64ビットコード(同じ構造、高速除算):

enter image description here

これは、not SSE使用されている命令にもかかわらず、ベクトル化されています。

87
usr

valueList[i] = ii=0から始まるため、最初のループの繰り返しは0.0 / 0.0を実行します。 したがって、ベンチマーク全体のすべての操作はNaNsで実行されます。

@ usrが逆アセンブリ出力で示した のように、32ビットバージョンはx87浮動小数点を使用し、64ビットはSSE浮動小数点を使用しました。

私はNaNsのパフォーマンス、またはx87とSSEの違いに関するパフォーマンスの専門家ではありませんが、これは26xのパフォーマンスの違いを説明すると思います。結果はlotvalueList[i] = i+1を初期化すると、32ビットと64ビットの間が近くになります(更新:32ビットと64ビットのパフォーマンスがかなり近いことをusrが確認しました)。

分割は、他の操作と比較して非常に遅いです。 @usrの答えに対する私のコメントをご覧ください。 http://agner.org/optimize/ も参照してください。ハードウェアに関する多くの優れた点、およびasmとC/C++の最適化(C#に関連するものもあります)。彼は、最近のすべてのx86 CPUのほとんどの命令について、レイテンシとスループットの命令表を持っています。

ただし、通常の値の場合、10B x87 fdivは、SSE2の8B倍精度divsdほど遅くありません。 NaN、無限大、または非正規数とのパフォーマンスの違いに関するIDK。

ただし、NaNやその他のFPU例外で発生することに対して異なる制御があります。 x87 FPU制御ワード は、SSE丸め/例外制御レジスタ(MXCSR)とは別です。x87がすべての部門でCPU例外を取得しているが、SSEはそうではありません。これは26の要因を簡単に説明します。または、NaNを処理するときに大きなパフォーマンスの違いがあるだけかもしれません。ハードウェアはnotNaNの後のNaN

resultは常にNaNになると信じているため、SSE denormalsによるスローダウンを回避するためのコントロールがここに登場します。C#がMXCSRで非正規化フラグを設定する場合、またはflush-to-zero-flag(読み戻すときに非正規数をゼロとして扱う代わりに、ゼロを最初に書き込む)。

Intelの記事 about SSE浮動小数点コントロールと、x87 FPUコントロールWordとの対比を見つけました。NaNについてはあまり説明しませんが。これで終わります:

結論

デノーマルとアンダーフロー数によるシリアル化とパフォーマンスの問題を回避するには、SSEおよびSSE2命令を使用して、ハードウェア内でFlush-to-ZeroおよびDenormals-Are-Zeroモードを設定し、浮動小数点アプリケーション。

これがゼロ除算に役立つ場合はIDK。

forとforeach

単一のループキャリー依存チェーンではなく、スループットが制限されたループ本体をテストすることは興味深いかもしれません。現状では、すべての作業は以前の結果に依存しています。 CPUが並列処理を行うことはありません(mul/divチェーンの実行中に次の配列ロードを境界チェックする以外は)。

「実際の作業」がより多くのCPU実行リソースを占有している場合、メソッド間でより大きな違いが見られることがあります。また、Sandybridge以前のIntelでは、28uopループバッファーでのループフィッティングとそうでない場合とで大きな違いがあります。そうでない場合、命令デコードのボトルネックが発生します。平均命令長が長い場合(SSEで発生します)。複数のuopにデコードする命令も、デコーダーにとって良いパターン(2-1-1など)でない限り、デコーダーのスループットを制限します。したがって、ループオーバーヘッドの命令が多いループは、28エントリのuopキャッシュでのループフィッティングの違いを生じさせる可能性があります。

31
Peter Cordes

すべての浮動小数点演算の99.9%がNaNに関係するという観測がありますが、これは少なくとも非常に珍しいことです(最初にPeter Cordesが発見)。 usrによる別の実験があり、除算命令を削除すると時間差がほぼ完全になくなることがわかりました。

ただし、実際には、最初の除算では0.0/0.0が計算され、初期NaNが得られるため、NaNのみが生成されます。除算が行われない場合、結果は常に0.0になり、常に0.0 * temp-> 0.0、0.0 + temp-> temp、temp-temp = 0.0を計算します。したがって、部門を削除すると、部門が削除されるだけでなく、NaNも削除されます。 NaNが実際に問題であり、一方の実装ではNaNの処理が非常に遅く、もう一方の実装では問題がないことを期待します。

I = 1でループを開始し、再度測定する価値があります。 4つの演算結果* temp、+ temp、/ temp、-tempは実質的に加算(1-temp)されるため、ほとんどの演算で異常な数値(0、無限大、NaN)は発生しません。

唯一の問題は、除算が常に整数の結果を与えることであり、正しい結果が多くのビットを使用しない場合、一部の除算の実装にはショートカットがあります。たとえば、310.0/31.0を除算すると、最初の4ビットが10.0になり、残りが0.0になります。一部の実装では、残りの50ビット程度の評価を停止できますが、他の実装ではできません。有意な違いがある場合、結果= 1.0/3.0でループを開始すると違いが生じます。

1
gnasher729