私はJavaでコードを書いています。ある時点で、プログラムの流れは2つのint変数 "a"と "b"がゼロでないかどうかによって決定されます。整数のオーバーフロー範囲内になることはありません。
私はそれを評価することができます
if (a != 0 && b != 0) { /* Some code */ }
または代わりに
if (a*b != 0) { /* Some code */ }
そのコードが1回の実行で何百万回も実行されることを期待しているので、どれがより速くなるかと思いました。ランダムに生成された巨大な配列でそれらを比較して実験を行いました。また、配列の疎密性(データの小数部= 0)が結果にどのように影響するかを知りたかったのです。
long time;
final int len = 50000000;
int arbitrary = 0;
int[][] nums = new int[2][len];
for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) {
for(int i = 0 ; i < 2 ; i++) {
for(int j = 0 ; j < len ; j++) {
double random = Math.random();
if(random < fraction) nums[i][j] = 0;
else nums[i][j] = (int) (random*15 + 1);
}
}
time = System.currentTimeMillis();
for(int i = 0 ; i < len ; i++) {
if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++;
}
System.out.println(System.currentTimeMillis() - time);
}
そして、その結果は、 "a"または "b"が0%に等しい時間より3%以上大きいと予想する場合、a*b != 0
はa!=0 && b!=0
より速いことを示しています。
私はその理由を知りたがっています。誰かが光を当てることができますか?それはコンパイラですか、それともハードウェアレベルですか。
編集:好奇心から...分岐予測について学んだ今、私は何を疑問に思っていましたアナログ比較では、aまたはbが0以外の値で示されます。
予想と同じ分岐予測の効果が見られます。興味深いことに、グラフはX軸に沿って多少反転しています。
1-私は何が起こるかを見るために!(a==0 || b==0)
を分析に加えました。
2-私はまた、分岐予測について学んだ後、a != 0 || b != 0
、(a+b) != 0
、および(a|b) != 0
を好奇心から除外しました。ただし、trueを返すにはaまたはbのみがゼロ以外である必要があるため、それらは他の式と論理的に等価ではありません。効率。
3-また、分析に使用した実際のベンチマークも追加しました。これは、単に任意のint変数を反復しているだけです。
4-一部の人々は、a != 0 & b != 0
ではなくa != 0 && b != 0
を含めることを提案していました。分岐予測の影響を取り除くため、a*b != 0
に近い動作をすると予測しています。私は&
がブール変数で使えることを知りませんでした、私はそれが整数での二項演算のためにだけ使われると思いました。
注:これをすべて検討していたコンテキストでは、intオーバーフローは問題になりませんが、一般的なコンテキストではこれは間違いなく重要な考慮事項です。
CPU:Intel Core i7 - 3610QM @ 2.3GHz
Javaのバージョン:1.8.0_45
Java(TM)SEランタイム環境(ビルド1.8.0_45-b14)
Java HotSpot(TM)64ビットサーバーVM(ビルド25.45-b02、混合モード)
ベンチマークの - may に欠陥があるという問題は無視しており、その結果を額面どおりに取っています。
それはコンパイラですか、それともハードウェアレベルですか。
後者だと思います。
if (a != 0 && b != 0)
2つのメモリロードと2つの条件付きブランチにコンパイルします
if (a * b != 0)
2つのメモリロード、乗算と1つの条件分岐にコンパイルします。
ハードウェアレベルの分岐予測が無効な場合、乗算は2番目の条件付き分岐よりも高速になる可能性があります。比率を大きくすると...分岐予測は効果が少なくなります。
条件付き分岐が遅いのは、それらが命令実行パイプラインを停止させるからです。分岐予測とは、分岐がどの方向に進むかを予測し、それに基づいて次の命令を投機的に選択することによって、機能停止を回避することです。予測が失敗した場合、他の方向の命令がロードされるまで遅延があります。
(注:上記の説明は単純化しすぎています。より正確な説明については、アセンブリ言語のコーダーおよびコンパイラーのライターに関するCPUの製造元から提供される文献を参照する必要があります。 Branch Predictors 良い背景です。
ただし、この最適化について注意する必要があることが1つあります。 a * b != 0
が間違った答えを与えることになる値はありますか?積を計算すると整数オーバーフローが発生する場合を考えてみましょう。
UPDATE
あなたのグラフは私が言ったことを確認する傾向があります。
条件付き分岐a * b != 0
の場合にも「分岐予測」効果があり、これはグラフに現れています。
X軸上で0.9を超える曲線を投影すると、1)それらは約1.0で合流し、2)合流点はX = 0.0の場合とほぼ同じY値になります。
アップデート2
a + b != 0
とa | b != 0
の場合で曲線が異なる理由はわかりません。 は分岐予測子ロジックに 賢いものがあるかもしれません。それとも他の何かを示す可能性があります。
(この種のことは特定のチップモデル番号やバージョンに特定のものになる可能性があります。ベンチマークの結果は他のシステムでは異なる可能性があります。)
ただし、どちらもa
およびb
の負でないすべての値に対して機能するという利点があります。
あなたのベンチマークにはいくつかの欠陥があり、実際のプログラムについて推測するのには役に立たないかもしれないと思います。これが私の考えです:
(a+b)!=0
は、合計がゼロになる正と負の値に対して間違ったことをするので、ここでうまくいっても一般的な場合には使えません。
同様に、(a*b)!=0
はオーバーフローする値に対して間違ったことをします。 (ランダムな例:196608 * 327680は0です。本当の結果は偶然に2で割り切れるためです)32つまり、その下位32ビットは0で、int
操作であれば、それらのビットはすべて入手できます。)
both valueがゼロ以外の場合、(a|b)!=0
および(a+b)!=0
テストは、bothがゼロ以外の場合、a != 0 && b != 0
および(a*b)!=0
テストします。だから、あなたは算術だけのタイミングを比較しているのではありません。もし条件がもっと頻繁に真であれば、それはif
本体のより多くの実行を引き起こし、それはより多くの時間がかかります。
VMは、外側の(fraction
)ループの最初の数回の実行中(fraction
が0のとき)、分岐がほとんど実行されないときに式を最適化します。 fraction
を0.5から始めると、オプティマイザは異なる動作をするかもしれません。
VMがここでいくつかの配列境界チェックを削除できない場合を除き、式には境界チェックが原因で他に4つの分岐があるため、低レベルで何が起こっているのかを突き止めようとすると複雑になります。 。 nums[0][i]
とnums[1][i]
をnums0[i]
とnums1[i]
に変更して、2次元配列を2つのフラット配列に分割した場合、結果が異なる場合があります。
CPU分岐予測子は、データ内の短いパターン、または実行中または実行されていないすべての分岐の実行を検出します。ランダムに生成されたベンチマークデータは、分岐予測子にとって最悪のシナリオです。実社会のデータに予測可能なパターンがある場合、またはすべてゼロおよびすべてのゼロ以外の値が長期にわたって存在する場合、分岐には多くの費用がかかりますもっと少なく。
条件が満たされた後に実行される特定のコードは、ループを展開できるかどうか、使用可能なCPUレジスタ、および必要なnums
値のいずれかなどに影響を与えるため、条件自体の評価のパフォーマンスに影響を与えます。条件を評価した後に再利用されます。ベンチマークで単にカウンタを増やすことは、実際のコードが何をするのかについての完全なプレースホルダではありません。
System.currentTimeMillis()
はほとんどのシステムで+/- 10 msより正確ではありません。 System.nanoTime()
は通常より正確です。
多くの不確実性があります、そしてこれらの種類のマイクロ最適化に関して明確なことを言うのは常に難しいです。なぜなら、あるVMやCPUのほうが速いトリックは他のものより遅くなるからです。 64ビット版ではなく32ビットHotSpot JVMを実行している場合は、2つの種類があります。「クライアント」VMが「サーバー」VMと比べて最適化が異なる(弱い)という点です。
もし可能なら VMによって生成されたマシンコードを逆アセンブルする 、それが何をするかを推測しようとするよりもむしろそれをしなさい!
私は物事を改善するかもしれないという考えを持っていたが、ここでの答えは良いです。
2つの分岐とそれに関連する分岐予測が原因となる可能性が高いため、ロジックをまったく変更せずに分岐を1つの分岐に減らすことができます。
bool aNotZero = (nums[0][i] != 0);
bool bNotZero = (nums[1][i] != 0);
if (aNotZero && bNotZero) { /* Some code */ }
それはまたするために働くかもしれません
int a = nums[0][i];
int b = nums[1][i];
if (a != 0 && b != 0) { /* Some code */ }
その理由は、短絡の規則により、最初のブール値がfalseの場合、2番目のブール値は評価されないはずです。 nums[1][i]
がfalseの場合、nums[0][i]
の評価を避けるために追加の分岐を実行する必要があります。今、あなたはnums[1][i]
が評価されることを気にしないかもしれません、しかし、あなたがそうするとき、それは範囲外またはnull refを投げないということをコンパイラは確信できません。 ifブロックを単純なブール値に減らすことで、コンパイラは、2番目のブール値を不必要に評価しても悪影響がないことを認識するのに十分賢いかもしれません。
乗算をすると、1つの数が0であっても積は0になります。
(a*b != 0)
それは積の結果を評価し、それによって0から始まる反復の最初の数回の出現を排除します。
(a != 0 && b != 0)
すべての要素が0と比較されて評価される場所。それ故に必要な時間はより少ないです。しかし、私は2番目の条件があなたにもっと正確な解決策を与えるかもしれないと思います。
ランダム化された入力データを使用しているため、分岐が予測不能になります。実際には分岐はしばしば(〜90%)予測可能なので、実際のコードでは分岐フルコードのほうが速いでしょう。
それは言った。 a*b != 0
が(a|b) != 0
よりも速くなることができるかどうかわかりません。一般に整数乗算はビットごとのORよりも高価です。しかし、このようなことは時折奇妙になります。たとえば、 Gallery of Processor Cache Effects の「例7:ハードウェアの複雑さ」の例を参照してください。