if ステートメントが有益と見なされる
概要
形式のステートメントはif (a > max) max = a
、一連の数値の最大値を決定する最速の方法です。ただし、ループ インフラストラクチャ自体が CPU 時間の大部分を占めるため、この最適化は最終的には疑わしいものになります。
詳細
luisperezphd による回答は、数値を提供するので興味深いものですが、方法に欠陥があると思います。コンパイラは比較をループの外に移動する可能性が高いため、回答は測定したいものを測定しません。これは、制御ループと測定ループの間の無視できるタイミングの違いを説明しています。
このループの最適化を回避するために、ループ変数に依存する操作を空の制御ループとすべての測定ループに追加しました。数値のリストで最大値を見つける一般的なユース ケースをシミュレートし、3 つのデータ セットを使用しました。
- 最良のケース: 最初の数値が最大で、それ以降の数値はすべて小さい
- 最悪の場合: すべての数値が前の数値よりも大きいため、反復ごとに最大値が変化します
- 平均的なケース: 一連の乱数
コードについては、以下を参照してください。
その結果は、私にとってかなり驚くべきものでした。私の Core i5 2520M ラップトップでは、10 億回の反復で次の結果が得られました (すべてのケースで空のコントロールに約 2.6 秒かかりました)。
max = Math.Max(max, a)
: ベストケース2.0秒 / ワーストケース1.3秒 / 平均ケース2.0秒
max = Math.Max(a, max)
: ベストケース 1.6 秒 / ワーストケース 2.0 秒 / 平均ケース 1.5 秒
max = max > a ? max : a
: ベストケース1.2秒 / ワーストケース1.2秒 / 平均ケース1.2秒
if (a > max) max = a
: ベスト ケース 0.2 秒 / ワースト ケース 0.9 秒 / 平均ケース 0.3 秒
したがって、CPU パイプラインが長くなり、結果として分岐にペナルティが発生するにもかかわらず、if
シミュレートされたすべてのデータ セットでは、古き良きステートメントが明らかに勝者となります。最良の場合は よりも 10 倍速くMath.Max
、最悪の場合でも 30% 以上高速です。
もう 1 つの驚きは、引数の順序が重要であることMath.Max
です。おそらくこれは、CPU 分岐予測ロジックが 2 つのケースで異なる動作をしており、引数の順序に応じて多かれ少なかれ分岐の予測を誤っているためです。
ただし、CPU 時間の大部分はループ インフラストラクチャで費やされるため、最終的にはこの最適化には問題があります。これにより、全体的な実行時間が測定可能ですがわずかに短縮されます。
luisperezphd によって更新されました
これをコメントとして収めることができなかったので、回答の一部としてではなく、文脈に沿ってここに書く方が理にかなっています。
あなたの理論は理にかなっていますが、結果を再現できませんでした。まず、何らかの理由でコードを使用すると、制御ループが作業を含むループよりも長くかかっていました。
そのため、ここでは、制御ループではなく、最低時間に対する相対的な数値を作成しました。結果の秒数は、最速の時間よりもどれだけ長くかかったかです。たとえば、最速時間のすぐ下の結果では、Math.Max(a, max) が最良のケースであったため、他のすべての結果は、それよりどれだけ時間がかかったかを表しています。
以下は私が得た結果です:
max = Math.Max(max, a)
: ベスト ケース 0.012 秒 / ワースト ケース 0.007 秒 / 平均ケース 0.028 秒
max = Math.Max(a, max)
: 0.000 ベスト ケース / 0.021 ワースト ケース / 0.019 秒 平均ケース
max = max > a ? max : a
: ベスト ケース 0.022 秒 / ワースト ケース 0.02 秒 / 平均ケース 0.01 秒
if (a > max) max = a
: ベスト ケース 0.015 秒 / ワースト ケース 0.024 秒 / 平均ケース 0.019 秒
2回目に実行したときは、次のようになりました。
max = Math.Max(max, a
): 0.024 秒のベスト ケース / 0.010 秒のワースト ケース / 0.009 秒の平均ケース
max = Math.Max(a, max)
: ベスト ケース 0.001 秒 / ワースト ケース 0.000 秒 / 平均ケース 0.018 秒
max = max > a ? max : a
: ベスト ケース 0.011 秒 / ワースト ケース 0.005 秒 / 平均ケース 0.018 秒
if (a > max) max = a
: ベスト ケース 0.000 秒 / ワースト ケース 0.005 秒 / 平均ケース 0.039 秒
これらのテストには十分なボリュームがあり、異常は一掃されているはずです。それにもかかわらず、結果はかなり異なります。おそらく、配列の大量のメモリ割り当てが関係しているのでしょう。または、違いが非常に小さいため、その時点でコンピューターで発生しているその他のことが原因である可能性があります。
上記の結果で 0.000 で表される最速の時間は約 8 秒であることに注意してください。したがって、当時の最長ランが 8.039 であったと考えると、時間の変動は約 0.5% (0.5%) であり、小さすぎて問題になりません。
コンピュータ
コードは Windows 8.1、i7 4810MQ 2.8Ghz で実行され、.NET 4.0 でコンパイルされました。
コードの変更
上記の形式で結果を出力するように、コードを少し変更しました。また、アセンブリの実行時に .NET で追加の読み込み時間が必要になる可能性があるため、開始後 1 秒待機するコードを追加しました。
また、CPU の最適化を考慮して、すべてのテストを 2 回実行しました。最後に、int
fori
を aに変更してunit
、ループを 10 億回ではなく 40 億回実行して、より長いタイムスパンを取得できるようにしました。
これはおそらくやり過ぎですが、テストがこれらの要因のいずれにも影響されないようにするためです。
コードはhttp://pastebin.com/84qi2cbDにあります。
コード
using System;
using System.Diagnostics;
namespace ProfileMathMax
{
class Program
{
static double controlTotalSeconds;
const int InnerLoopCount = 100000;
const int OuterLoopCount = 1000000000 / InnerLoopCount;
static int[] values = new int[InnerLoopCount];
static int total = 0;
static void ProfileBase()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
int maxValue;
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
// baseline
total += values[i];
}
}
stopwatch.Stop();
controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
}
static void ProfileMathMax()
{
int maxValue;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = Math.Max(values[i], maxValue);
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("Math.Max(a, max) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileMathMaxReverse()
{
int maxValue;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = Math.Max(maxValue, values[i]);
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("Math.Max(max, a) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileInline()
{
int maxValue = 0;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = maxValue > values[i] ? values[i] : maxValue;
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("max = max > a ? a : max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileIf()
{
int maxValue = 0;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
if (values[i] > maxValue)
maxValue = values[i];
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("if (a > max) max = a: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void Main(string[] args)
{
Random rnd = new Random();
for (int i = 0; i < InnerLoopCount; i++)
{
//values[i] = i; // worst case: every new number biggest than the previous
//values[i] = i == 0 ? 1 : 0; // best case: first number is the maximum
values[i] = rnd.Next(int.MaxValue); // average case: random numbers
}
ProfileBase();
Console.WriteLine();
ProfileMathMax();
Console.WriteLine();
ProfileMathMaxReverse();
Console.WriteLine();
ProfileInline();
Console.WriteLine();
ProfileIf();
Console.ReadLine();
}
}
}