編集 3
タイミング エラーを修正して元のテストに戻ると、次のような出力が得られます。
条件付きで 67 ミリ秒かかりました
通常は83msかかりました
キャッシュにかかった時間は 73 ミリ秒
for
これは、三項/条件付き演算子がループでわずかに高速になる可能性があることを示しています。論理分岐がループの外に抽象化されると、if
ブロックが三項/条件付き演算子を打ち負かすという以前の発見を考えると、条件付き/三項演算子が反復的に使用される場合、少なくともある場合。
これらの最適化が適用されない理由、または標準if
ブロックに適用されない理由は明確ではありません。実際の差はかなり小さく、議論の余地があると私は主張します。
編集2
実際、ここで強調表示されているテスト コードには明らかなエラーがあります。
Stopwatch
呼び出し間でリセットされません。代わりに使用しStopwatch.Restart
てStopwatch.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
}