私は今日プロジェクトに取り組んでいて、いくつかの場所でMath.Maxを使用し、他の場所でifステートメントをインラインで使用していることに気付きました。それで、私は誰かがどちらが「より良い」かを知っているかどうか疑問に思いました...というか、本当の違いは何ですか。
たとえば、次のように、c1 = c2
:
Random Rand = new Random();
int a = Rand.next(0,10000);
int b = Rand.next(0,10000);
int c1 = Math.Max(a, b);
int c2 = a>b ? a : b;
具体的にはC#について質問していますが、どの言語が同じような概念を持っているかはわかりませんが、言語によって答えが異なる可能性があると思います。
私がすぐに気付く主な違いの1つは、読みやすさのためです。実装/パフォーマンスのために私が知っている限り、それらはほぼ同等です。
Math.Max(a,b)
は、以前のコーディング知識に関係なく、非常に簡単に理解できます。
a>b ? a : b
は、少なくともユーザーが三項演算子についてある程度の知識を持っている必要があります。
"疑わしい場合-読みやすさを求めてください"
この議論にいくつかの数字を入れるのは楽しいだろうと思ったので、それをプロファイルするためのコードをいくつか書きました。予想通り、それらはすべての実用的な目的でほぼ同じです。
このコードは10億回のループを実行します(10億回)。取得するループのオーバーヘッドを差し引く:
空のループを10億回実行して計算したオーバーヘッドを差し引くと、オーバーヘッドは1.2秒でした。
私はこれをラップトップ、64ビットWindows 7、1.3 Ghz Intel Core i5(U470)で実行しました。コードはリリースモードでコンパイルされ、デバッガーを接続せずに実行されました。
コードは次のとおりです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace TestMathMax {
class Program {
static int Main(string[] args) {
var num1 = 10;
var num2 = 100;
var maxValue = 0;
var LoopCount = 1000000000;
double controlTotalSeconds;
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < LoopCount; i++) {
// do nothing
}
stopwatch.Stop();
controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
}
Console.WriteLine();
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < LoopCount; i++) {
maxValue = Math.Max(num1, num2);
}
stopwatch.Stop();
Console.WriteLine("Math.Max() - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
Console.WriteLine();
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < LoopCount; i++) {
maxValue = num1 > num2 ? num1 : num2;
}
stopwatch.Stop();
Console.WriteLine("Inline Max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
Console.ReadLine();
return maxValue;
}
}
}
更新された結果2015年2月7日
Windows 8.1、Surface 3 Pro、i7 4650U 2.3Ghzでは、デバッガーが接続されていないリリースモードのコンソールアプリケーションとして実行されました。
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
ステートメントはすべてのシミュレートされたデータセットの明確な勝者です。最良の場合は_Math.Max
_の10倍高速であり、最悪の場合はさらに30%以上高速です。
もう1つの驚きは、_Math.Max
_への引数の順序が重要であることです。おそらくこれは、CPU分岐予測ロジックが2つのケースで異なる動作をし、引数の順序に応じて分岐を多かれ少なかれ誤って予測するためです。
ただし、CPU時間の大部分はループインフラストラクチャに費やされているため、最終的にこの最適化には疑問が残ります。これにより、全体的な実行時間は測定可能ですがわずかに短縮されます。
私はこれをコメントとして当てはめることができませんでした、そしてそれが文脈にあるように私の答えの一部としてではなくここにそれを書くことはより理にかなっています。
あなたの理論は理にかなっていますが、私は結果を再現することができませんでした。まず、何らかの理由でコードを使用すると、制御ループが作業を含むループよりも長くかかっていました。
そのため、ここでは、制御ループではなく、最短時間を基準にして数値を作成しました。結果の秒数は、最速の時間よりもどれだけ長くかかったかです。たとえば、最速時間のすぐ下の結果では、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%)です。つまり、小さすぎて問題にはなりません。
コードはWindows8.1、i7 4810MQ 2.8Ghzで実行され、.NET4.0でコンパイルされました。
上記の形式で結果を出力するようにコードを少し変更しました。また、アセンブリの実行時に.NETが必要とする可能性のある追加の読み込み時間を考慮して、開始後1秒待機するコードを追加しました。
また、CPUの最適化を考慮して、すべてのテストを2回実行しました。最後に、int
のi
を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();
}
}
}
_
JITerがMath.Max関数をインライン化することを選択した場合、実行可能コードはifステートメントと同じになります。 Math.Maxがインライン化されていない場合、呼び出しを伴う関数呼び出しとして実行され、ifステートメントに存在しないオーバーヘッドが返されます。したがって、ifステートメントはインライン化の場合はMath.Max()と同じパフォーマンスを提供し、ifステートメントはインライン化されていない場合は数クロックサイクル速くなる可能性がありますが、数十を実行しない限り違いは目立ちません何百万もの比較の。
2つのパフォーマンスの違いはほとんどの状況で無視できるほど小さいので、読みやすいのでMath.Max(a、b)をお勧めします。
Math.Maxが何をしているのかを理解する方が早いと思いますが、それがここでの唯一の決定要因になるはずです。
しかし、耽溺として、Math.Max(a,b)
が引数を1回評価し、a > b ? a : b
それらの1つを2回評価します。ローカル変数の問題ではありませんが、副作用のあるプロパティの場合、副作用が2回発生する可能性があります。
は同等ではありませんすべての場合でa > b ? a : b
になります。
Math.Max
は、2つの引数の大きい方の値を返します。
if (a == b) return a; // or b, doesn't matter since they're identical
else if (a > b && b < a) return a;
else if (b > a && a < b) return b;
else return undefined;
たとえば、double.NaN
の二重オーバーロードの場合、未定義はMath.Max
にマップされます。
aがbより大きい場合、aと評価されます。これは、必ずしもbがaより小さいことを意味するわけではありません。
それらが同等ではないことを示す簡単な例:
var a = 0.0/0.0; // or double.NaN
var b = 1.0;
a > b ? a : b // evaluates to 1.0
Math.Max(a, b) // returns double.NaN
パフォーマンスに関しては、最新のCPUには内部コマンドパイプラインがあり、すべてのアセンブリコマンドがいくつかの内部ステップで実行されます。 (例:フェッチ、解釈、計算、保存)
ほとんどの場合、CPUは、シーケンシャルコマンドに対してこれらのステップを並行して実行するのに十分スマートであるため、全体的なスループットは非常に高くなります。
ブランチが来るまでこれは問題ありません(if
、?:
など)。ブランチはシーケンスを中断し、CPUにパイプラインを破棄させる可能性があります。これには多くのクロックサイクルがかかります。
理論的には、コンパイラが十分に賢い場合、Math.Max
は、built it CPUコマンドを使用して実装でき、分岐を回避できます。
この場合、Math.Max
は実際にはif
よりも高速ですが、コンパイラによって異なります。
ベクトルでの作業のように、より複雑なMaxの場合、double []v; v.Max()
コンパイラは高度に最適化されたライブラリコードを利用できます。これは、通常のコンパイル済みコードよりもはるかに高速です。
したがって、Math.Maxを使用するのが最善ですが、それが十分に重要であるかどうか、特定のターゲットシステムとコンパイラを確認することもお勧めします。
操作を行います。 Nは> = 0でなければなりません
一般的な解決策:
A) N = Math.Max(0, N)
B) if(N < 0){N = 0}
速度による並べ替え:
遅い:Math.Max(A)<(B)if-thenステートメント:速い(ソリューション「A」より3%速い)
しかし、私のソリューションはソリューション「B」よりも4%高速です。
N *= Math.Sign(1 + Math.Sign(N));