私はこのバージョンが好きです:
int x = Math.Max(5, GetInt());
ただし、これらはすべて時期尚早の最適化であることに注意してください。フレームワークを変更して新しい JIT 最適化または別の JIT 最適化を追加する Windows Update のような単純なもので、今日は速くても、明日は遅くなる可能性があります。
大きなループ内で次のようなチェックを実行している場合、私が時間を費やす可能性があるのは次のとおりです。
var items = Enumerable.Range(0, 1000000);
foreach(int item in items)
{
if (item % 3 == 0)
{
//...
}
else
{
//...
}
}
ループをチェックする理由は、コードが可能な限り迅速に何度も実行され、小さな非効率性に重点が置かれるためではなく、 または を使用するかどうかがif
ループelse
全体で変化するためです。
分岐予測として知られる最新の CPU の機能により、そのコードは非効率的であると予想されます。if
andの内容がelse
十分に重要であり、十分に異なる場合、フロントエンドでこれらすべてのチェックを実行して (そしてすべての予測が失敗するように) 移動し、その後にすべての を一緒に実行することで、そのコードをより高速に実行if
できます。すべてのelse
sによって。if と else を実行する第 2 フェーズでの分岐予測 (これはおそらく実行コストがはるかに高くなります) がより正確になるため、より高速になります。
違いを示す小さなプログラムを次に示します。
class Program
{
static int samplesize = 1000000;
//ensure these are big enough that we don't spend time allocating new buffers while the stopwatch is running
static Dictionary<int, string> ints = new Dictionary<int,string>(samplesize * 4);
static Dictionary<double,string> doubles = new Dictionary<double,string>(samplesize * 4);
static void Main(string[] args)
{
var items = Enumerable.Range(0, samplesize).ToArray() ;
var clock = new Stopwatch();
test1(items); //jit hit, discard first run. Also ensure all keys already exist in the dictionary for both tests
clock.Restart();
test1(items);
clock.Stop();
Console.WriteLine("Time for naive unsorted: " + clock.ElapsedTicks.ToString());
test2(items); //jit hit
clock.Restart();
test2(items);
clock.Stop();
Console.WriteLine("Time for separated/branch prediction friendly: " + clock.ElapsedTicks.ToString());
Console.ReadKey(true);
}
static void test1(IEnumerable<int> items)
{
foreach(int item in items)
{
//different code branches that still do significant work in the cpu
// doing more work here results in a larger branch-prediction win, to a point
if (item % 3 == 0)
{ //force hash computation and multiplication op (both cpu-bound)
ints[item] = (item * 2).ToString();
}
else
{
doubles[(double)item] = (item * 3).ToString();
}
}
}
static void test2(IEnumerable<int> items)
{
//doing MORE work: need to evaluate our items two ways, allocate arrays
var intItems = items.Where(i => i % 3 == 0).ToArray();
var doubleItems = items.Where(i => i % 3 != 0).ToArray();
// but now there is no branching... adding all the ints, then adding all the doubles.
foreach (var item in intItems) { ints[item] = (item * 2).ToString(); }
foreach (var item in doubleItems) { doubles[(double)item] = (item * 3).ToString(); }
}
}
そして、私のマシンでの結果は、より多くの作業を行う 2 番目のテストがより速く実行されたことです。
単純な未ソートの時間: 1118652
分離/分岐予測に適した時間: 1005190
ここで取り上げるべき重要なことは、すべてのループが分岐予測の恩恵を受けることができるかどうかを検討する必要があるということではありません。これは、驚くべきパフォーマンス結果をもたらす数多くの CPU 機能の 1 つにすぎません。ここで重要なことは、コードがどのように機能するかを確実に知るには、実際にパフォーマンスを測定する必要があるということです。これを慎重に構築しなければ、単純な手法でも勝てます (最初の試みでは、期待した速度が得られませんでした)。
さらに、これらのケースではそれほど大きな違いはないことを指摘する必要があります。このパフォーマンスの向上はそれだけの価値がありましたか、それとも他の場所で時間を費やしたほうがよかったでしょうか? それを知る唯一の方法は、アプリ全体のパフォーマンスを実際に測定し、実際に時間を費やしている場所を見つけることです。本来よりも実際に遅いのはどこですか?これはプロファイリングと呼ばれ、これを正確に行うのに役立つツールがあります。