1. ホーム
  2. c#

32ビットと64ビットでコンパイルした場合の大きな性能差(26倍速)

2023-07-07 01:44:23

質問

を使用した場合の違いを測定しようとしていました。 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();
}

私が選んだ ReleaseAny 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!

そして、Releaseと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 bit 版はこんなに速いのですか?ある程度の違いは予想していましたが、これほど大きな違いはありません。

私は他のコンピュータにアクセスできません。あなたのマシンでこれを実行し、結果を教えていただけませんか?私は Visual Studio 2015 を使用していて、Intel の コア i7 930.

ここでは SafeExit() メソッドがあるので、自分でコンパイル/実行することができます。

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

要求されたように double? の代わりに DoubleWrapper :

任意の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 .

どのように解決するのですか?

4.5.2にて再現しています。ここでは、RyuJIT はありません。x86 と x64 の両方のディスアセンブリは合理的に見えます。範囲チェックなどは同じです。基本的な構造は同じ。ループ展開なし。

x86は異なるfloat命令群を使用します。これらの命令の性能はx64の命令と同等であると思われる 除算を除けば :

  1. 32 ビット x87 float 命令は、内部で 10 バイトの精度を使用します。
  2. 拡張精度での除算は超遅いです。

除算演算のため、32ビット版では超低速になります。 除算をアンコメントすると、パフォーマンスが等しくなる を大幅に改善しました (32 ビット版では 430ms から 3.25ms に低下)。

Peter Cordes は、2 つの浮動小数点ユニットの命令レイテンシがそれほど異ならないことを指摘しています。おそらく、中間結果のいくつかは、非正規化された数値または NaN であると思われます。これらは、どちらかのユニットで遅いパスをトリガーするかもしれません。あるいは、10バイトと8バイトの浮動小数点精度のために、2つの実装間で値が発散しているのかもしれません。

Peter Cordes はまた、次のように指摘しています。 すべて の中間結果はNaN ... この問題を除去する ( valueList.Add(i + 1) を削除すると、ほとんどの場合結果が同じになります。どうやら、32ビットコードはNaNオペランドを全く好まないようです。いくつかの中間値を表示してみましょう。 if (i % 1000 == 0) Console.WriteLine(result); . これで、データがまともになったことが確認できます。

ベンチマークを行う場合、現実的なワークロードをベンチマークする必要があります。しかし、無害な分割がベンチマークを台無しにするとは、誰が考えたでしょうか!

より良いベンチマークを得るために、単純に数字を合計してみてください。

除算とモジュロは常に非常に遅いです。もし、BCLを修正すると Dictionary のコードを修正し、バケットインデックスを計算するためにモジュロ演算子を使わないようにすれば、パフォーマンスは測定可能なほど改善されます。これは、除算がいかに遅いかということです。

以下は32ビットコードです。

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

これは ではない SSE 命令が使用されているにもかかわらず、ベクトル化されていません。