1

同じことを行う 3 つのコードがありますが、x64 リリースではパフォーマンスが異なります。

Branch Predictionのせいだと思います。誰でもさらに詳しく説明できますか?

条件付き: 41 ミリ秒かかります

for (int j = 0; j < 10000; j++)
{
    ret = (j * 11 / 3 % 5) + (ret % 11 == 4 ? 2 : 1);
}

通常: 51ミリ秒かかります

for (int j = 0; j < 10000; j++)
{
    if (ret % 11 == 4)
    {
        ret = 2 + (j * 11 / 3 % 5);
    }
    else
    {
        ret = 1 + (j * 11 / 3 % 5);
    }
}

キャッシュ: 44 ミリ秒かかります

for (int j = 0; j < 10000; j++)
{
    var tmp = j * 11 / 3 % 5;
    if (ret % 11 == 4)
    {
        ret = 2 + tmp;
    }
    else
    {
        ret = 1 + tmp;
    }
}
4

2 に答える 2

2

編集 3 タイミング エラーを修正して元のテストに戻ると、次のような出力が得られます。

条件付きで 67 ミリ秒かかりました

通常は83msかかりました

キャッシュにかかった時間は 73 ミリ秒

forこれは、三項/条件付き演算子がループでわずかに高速になる可能性があることを示しています。論理分岐がループの外に抽象化されると、ifブロックが三項/条件付き演算子を打ち負かすという以前の発見を考えると、条件付き/三項演算子が反復的に使用される場合、少なくともある場合。

これらの最適化が適用されない理由、または標準ifブロックに適用されない理由は明確ではありません。実際の差はかなり小さく、議論の余地があると私は主張します。

編集2

実際、ここで強調表示されているテスト コードには明らかなエラーがあります。

Stopwatch呼び出し間でリセットされません。代わりに使用しStopwatch.RestartStopwatch.Start反復を 1000000000 にすると、結果が得られます

条件付きで 22404ms かかりました

通常は 21403ms かかりました

これは、私が期待していた結果に似ており、抽出された CIL によって裏付けられています。したがって、「通常」ifは、周囲のコードから分離されている場合、Ternary\Conditional 演算子よりもわずかに高速です。

編集

以下に概説した調査の結果、論理条件を使用して 2 つの定数またはリテラルを選択する場合、条件付き/三項演算子は標準ブロックよりも大幅に高速になる可能性があることをお勧めします。私のテストでは、約 2 倍の速さでした。if

しかし、私はその理由を完全に理解することはできません。法線によって生成される CILifはより長くなりますが、両方の関数の平均実行パスは、3 つのロードと 1 つまたは 2 つのジャンプを含む 6 行のようです。何かアイデアはありますか? .


このコードを使用すると、

using System.Diagnostics;

class Program
{
    static void Main()
    {
        var stopwatch = new Stopwatch();

        var conditional = Conditional(10);
        var normal = Normal(10);
        var cached = Cached(10);

        if (new[] { conditional, normal }.Any(x => x != cached))
        {
            throw new Exception();
        }

        stopwatch.Start();
        conditional = Conditional(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Conditional took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        ////stopwatch.Start(); incorrect
        stopwatch.Restart();
        normal = Normal(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Normal took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        ////stopwatch.Start(); incorrect
        stopwatch.Restart();
        cached = Cached(10000000);
        stopWatch.Stop();
        Console.WriteLine(
            "Cached took {0}ms", 
            stopwatch.ElapsedMilliseconds);

        if (new[] { conditional, normal }.Any(x => x != cached))
        {
            throw new Exception();
        }

        Console.ReadKey();
    }

    static int Conditional(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            ret = (j * 11 / 3 % 5) + (ret % 11 == 4 ? 2 : 1);
        }

        return ret;
    }

    static int Normal(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            if (ret % 11 == 4)
            {
                ret = 2 + (j * 11 / 3 % 5);
            }
            else
            {
                ret = 1 + (j * 11 / 3 % 5);
            }
        }

        return ret;
    }

    static int Cached(int iterations)
    {
        var ret = 0;
        for (int j = 0; j < iterations; j++)
        {
            var tmp = j * 11 / 3 % 5;
            if (ret % 11 == 4)
            {
                ret = 2 + tmp;
            }
            else
            {
                ret = 1 + tmp;
            }
        }

        return ret;
    }
}

x64 リリース モードでコンパイルされ、最適化され、デバッガーを接続せずに実行されます。私はこの出力を得ます、

条件付きで 65 ミリ秒かかりました

通常は148ミリ秒かかりました

キャッシュには 217 ミリ秒かかりました

例外はスローされません。


ILDASM を使用してコードを逆アセンブルする 3 つのメソッドの CIL が異なり、メソッドのコードConditionalがやや短いことを確認できます。


「なぜ」という質問に本当に答えるには、コンパイラのコードを理解する必要があります。コンパイラがそのように書かれた理由を知る必要があるでしょう。


これをさらに分解して、実際に論理関数だけを比較し、他のすべてのアクティビティを無視することができます。

static int Conditional(bool condition, int value)
{
    return value + (condition ? 2 : 1);
}

static int Normal(bool condition, int value)
{
    if (condition)
    {
        return 2 + value;
    }

    return 1 + value;
}

あなたが繰り返すことができるもの

static int Looper(int iterations, Func<bool, int, int> operation)
{
    var ret = 0;
    for (var j = 0; j < iterations; j++)
    {
        var condition = ret % 11 == 4;
        var value = ((j * 11) / 3) % 5;
        ret = operation(condition, value);
    }
}

このテストでもパフォーマンスの差が見られますが、逆に単純化した IL を以下に示します。

... Conditional ...
{
     : ldarg.1      // push second arg
     : ldarg.0      // push first arg
     : brtrue.s T   // if first arg is true jump to T
     : ldc.i4.1     // push int32(1)
     : br.s F       // jump to F
    T: ldc.i4.2     // push int32(2)
    F: add          // add either 1 or 2 to second arg
     : ret          // return result
}

... Normal ...
{
     : ldarg.0      // push first arg
     : brfalse.s F  // if first arg is false jump to F
     : ldc.i4.2     // push int32(2)
     : ldarg.1      // push second arg
     : add          // add second arg to 2
     : ret          // return result
    F: ldc.i4.1     // push int32(1)
     : ldarg.1      // push second arg
     : add          // add second arg to 1
     : ret          // return result
}
于 2012-09-17T11:37:56.867 に答える
1

同じことをする3つのコードがありますが、それらのパフォーマンスは異なります

それはそれほど驚くべきことではありませんか?少し違った書き方をすると、タイミングも異なります。

分岐予測のせいだと思います。

これは、一部、最初のスニペットが高速である理由を説明している可能性があります。?:しかし、それはまだ分岐していることに注意してください。
もう1つ注意すべき点は、これは1つの大きな式であり、オプティマイザーにとって理想的な領域であるということです。

問題は、このようなコードを見て、特定の演算子が速い/遅いと結論付けることができないことです。周囲のコードは少なくとも同じくらい重要です。

于 2012-09-17T11:55:42.300 に答える