Javaで(a * b!= 0)が(a!= 0 && b!= 0)より速いのはなぜですか?


412

私はJavaでいくつかのコードを書いています。ある時点で、プログラムのフローは、2つのint変数「a」と「b」が非ゼロであるかどうかによって決定されます(注:aとbが負になることはありません。整数オーバーフローの範囲内には決してありません)。

私はそれを評価することができます

if (a != 0 && b != 0) { /* Some code */ }

または代わりに

if (a*b != 0) { /* Some code */ }

私はそのコードが実行ごとに数百万回実行されることを期待しているので、どちらがより高速になるのか疑問に思いました。ランダムに生成された巨大な配列でそれらを比較することによって実験を行い、配列のスパース性(データの割合= 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がゼロ以外の結果のグラフ

その理由を知りたいです。誰かが光を当てることができますか?それはコンパイラですか、それともハードウェアレベルですか?

編集: 好奇心から...分岐予測について学んだので、OR bのアナログ比較がゼロでないことを示すのではないかと思っていました。

ゼロ以外のaまたはbのグラフ

予想と同じように分岐予測の効果がわかります。興味深いことに、グラフはX軸に沿って多少反転しています。

更新

1- !(a==0 || b==0)何が起こるかを確認するために分析に追加しました。

2 -私も含めa != 0 || b != 0(a+b) != 0そして(a|b) != 0好奇心のうち、分岐予測についての学習の後。ただし、他の式と論理的に同等ではありません。trueを返すために必要なのはOR b だけであり、処理効率を比較するためのものではありません。

3-分析に使用した実際のベンチマークも追加しました。これは、任意のint変数を反復するだけです。

4-一部の人々は、とはa != 0 & b != 0対照的にを含めることを提案しa != 0 && b != 0ていましたa*b != 0。分岐予測効果を削除するため、より密接に動作すると予測されました。私は&それがブール変数で使用できることを知りませんでした、それは整数でのバイナリ演算にのみ使用されると思いました。

注:このすべてを検討していた状況では、intオーバーフローは問題ではありませんが、これは一般的な状況では間違いなく重要な考慮事項です。

CPU:Intel Core i7-3610QM @ 2.3 GHz

Javaバージョン:1.8.0_45
Java(TM)SEランタイム環境(ビルド1.8.0_45-b14)
Java HotSpot(TM)64ビットサーバーVM(ビルド25.45-b02、混合モード)


11
どうif (!(a == 0 || b == 0))ですか?マイクロベンチマークは信頼性が低いことで悪名高く、これが実際に測定可能である可能性は低いです(約3%は、私にとって誤差の範囲のように聞こえます)。
Elliott Frisch 2016

9
またはa != 0 & b != 0
Louis Wasserman

16
予測された分岐が間違っている場合、分岐は遅くなります。a*b!=0ブランチが1つ少ない
Erwin Bolwidt 2016

19
(1<<16) * (1<<16) == 0しかし、どちらもゼロとは異なります。
CodesInChaos

13
@Gene:提案された最適化は無効です。でもオーバーフローを無視し、a*bゼロならばである1a及びbゼロです。a|b両方の場合にのみゼロです。
hmakholmがモニカを去った

回答:


240

私はあなたのベンチマークに欠陥があるかもしれないという問題を無視し、結果を額面通りに受け止めています。

それはコンパイラですか、それともハードウェアレベルですか?

後者、私は思う:

  if (a != 0 && b != 0)

2つのメモリロードと2つの条件付きブランチにコンパイルされます

  if (a * b != 0)

2つのメモリロード、1つの乗算と1つの条件付きブランチにコンパイルされます。

ハードウェアレベルの分岐予測が効果的でない場合、乗算は2番目の条件付き分岐よりも高速になる可能性があります。比率を上げると...分岐予測の効果が低下します。

条件付き分岐が遅い理由は、それらが命令実行パイプラインを停止させるためです。分岐予測は、分岐が進む方向を予測し、それに基づいて次の命令を投機的に選択することにより、ストールを回避することです。予測が失敗した場合、他の方向の命令がロードされるまでに遅延があります。

(注:上記の説明は単純化しすぎています。より正確な説明を得るには、CPU製造元がアセンブリ言語コーダーとコンパイラライターに提供している資料を参照する必要があります。ブランチ予測に関するWikipediaのページは、適切な背景です。)


ただし、この最適化で注意する必要があることが1つあります。a * b != 0間違った答えを与える値はありますか?積を計算すると整数オーバーフローが発生する場合を考えてください。


更新

あなたのグラフは私が言ったことを確認する傾向があります。

  • 条件分岐のa * b != 0場合にも「分岐予測」の効果があり、これはグラフに現れます。

  • X軸上で0.9を超えるカーブを投影すると、1)約1.0で交わる2)交点がX = 0.0とほぼ同じY値になるようになります。


アップデート2

曲線が異なりますなぜ私は理解していないa + b != 0と、a | b != 0例。分岐予測ロジックには、何か賢いものがあるかもしれません。または、それは何か他のものを示している可能性があります。

(この種のことは、特定のチップモデル番号またはバージョンに固有である可能性があることに注意してください。ベンチマークの結果は、他のシステムでは異なる場合があります。)

ただし、どちらにもとのすべての負でない値に対して機能するという利点がaありbます。


1
@DebosmitRay-1)SWがあってはなりません。中間結果はレジスタに保持されます。2)2番目のケースでは、2つの使用可能なブランチがあります。1つは「いくつかのコード」を実行するブランチで、もう1つはの後の次のステートメントにスキップするブランチですif
スティーブンC

1
@StephenCあなたはa + bとa | bについて混乱するのは正しいです。曲線同じなので、色が本当に近いと思います。目の不自由な人に色を付けて謝罪!
Maljam 2016

3
確率の観点から見た@ njzk2これらのケースは、50%の軸に従って対称である必要があります(a&bおよびのゼロの確率a|b)。それらは完全ではありませんが、それがパズルです。
アントニン・Lejsek

3
@StephenC a*b != 0a+b != 0ベンチマークが異なる理由a+b != 0は、がまったく同等ではなく、ベンチマークされるべきではなかったためです。たとえば、a = 1, b = 0では、最初の式はfalseと評価されますが、2番目の式はtrueと評価されます。乗算は、and演算子のように機能しますが、addは、or演算子のように機能します。
JS1、2016

2
@AntonínLejsek確率は異なると思います。あなたが持っている場合はnゼロを、両方の可能性abゼロ増加していますn。でAND操作、より高いとn、それらの一方の確率が非ゼロである増加し、条件が満たされています。これは、OR演算の逆です(どちらかがゼロになる確率はで増加しますn)。これは数学的観点に基づいています。それがハードウェアの動作方法かどうかはわかりません。
WYSIWYG

70

あなたのベンチマークにはいくつかの欠陥があり、実際のプログラムを推論するのには役に立たないかもしれません。これが私の考えです。

  • (a|b)!=0そして(a+b)!=0場合、テストのいずれか一方の値は、非ゼロであるa != 0 && b != 0(a*b)!=0テスト場合の両方が非ゼロです。したがって、演算だけのタイミングを比較するのではなく、条件がより頻繁に真である場合、if本体の実行が多くなり、時間がかかります。

  • (a+b)!=0 合計がゼロになる正の値と負の値に対して誤った処理を行うため、ここで機能しても、一般的なケースでは使用できません。

  • 同様に、(a*b)!=0オーバーフローする値に対して誤った処理を行います。(ランダムな例:196608 * 327680は0です。これは、真の結果がたまたま2 32で割り切れるので、その下位32ビットは0であり、これらのビットは、int操作の場合に得られるすべてです。)

  • VMは、外側の(fraction)ループの最初の数回の実行中に式を最適化しますfractionfraction0.5 から始めると、オプティマイザーは異なる処理を実行する場合があります。

  • ここでVMが配列の境界チェックの一部を排除できない限り、境界チェックのために式には他に4つの分岐があり、それが低レベルで何が起こっているのかを理解しようとするときに複雑な要素になります。2次元配列を2つのフラット配列に分割し、nums[0][i]およびとnums[1][i]を変更すると、異なる結果が得られる場合がnums0[i]ありnums1[i]ます。

  • CPU分岐予測子は、データ内の短いパターン、または実行中または実行されないすべての分岐の実行を検出します。ランダムに生成されたベンチマークデータは、分岐予測の最悪のシナリオです。実際のデータに予測可能なパターンがある場合、またはすべてゼロの値とすべてゼロ以外の値の長いランがある場合、ブランチのコストははるかに低くなります。

  • 条件が満たされた後に実行される特定のコードは、ループを展開できるかどうか、使用可能なCPUレジスター、およびフェッチされたnums値のいずれかが必要かどうかなどに影響するため、条件自体の評価のパフォーマンスに影響を与える可能性があります状態を評価した後、再利用できます。ベンチマークでカウンターをインクリメントするだけでは、実際のコードが行うことの完全なプレースホルダーではありません。

  • System.currentTimeMillis()ほとんどのシステムでは+/- 10 msよりも正確ではありません。System.nanoTime()通常はより正確です。

多くの不確実性があり、ある種のVMまたはCPUで高速なトリックは別のVMまたはCPUで低速になる可能性があるため、この種のマイクロ最適化で明確なことを言うのは常に困難です。64ビットバージョンではなく32ビットHotSpot JVMを実行している場合は、2つの種類があることに注意してください。「クライアント」VMは「サーバー」VMとは異なる(弱い)最適化を持っています。

VMによって生成されたマシンコード逆アセンブルできる場合は、それが何をするかを推測しようとするのではなく、それを実行してください!


24

私は物事を改善するかもしれないという考えを持っていましたが、ここでの答えは良いです。

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番目のブール値は評価されないためです。がfalse nums[1][i]かどうかnums[0][i]を評価しないようにするには、追加の分岐を実行する必要があります。さて、あなたはそれnums[1][i]が評価されても気にしないかもしれませんが、コンパイラーはそれが範囲外またはnull参照をスローしないことを確信できません。ifブロックを単純なブール値に削減することにより、コンパイラーは、2番目のブール値を不必要に評価しても悪影響が生じないことを認識できるほど賢くなります。


3
私は気持ちを持っているがUpvotedこれはしません、かなりの質問に答えます。
Pierre Arlaud、2016

3
これは、ロジックを非分岐から変更せずに分岐を導入する方法です(取得abて副作用があった場合は、それらを維持していたはずです)。あなたはまだ持っている&&ので、あなたはまだブランチを持っています。
Jon Hanna

11

乗算を行うと、1つの数値が0であっても、積は0になります。

    (a*b != 0)

それは積の結果を評価し、それにより0から始まる反復の最初の数回の発生を排除します。その結果、比較は条件が

   (a != 0 && b != 0)

すべての要素が0と比較され、評価されます。したがって、必要な時間が短くなります。しかし、私は2番目の条件がより正確な解決策を与えると信じています。


4
2番目の式では、if aがゼロの場合、b式全体がすでにfalseであるため、評価する必要はありません。したがって、すべての要素が比較されるとは限りません。
Kuba Wyrostek、2016

9

ランダムな入力データを使用しているため、ブランチが予測不能になります。実際にはブランチは予測可能であることが多く(〜90%)、実際のコードではブランチフルコードの方が高速になる可能性があります。

それは言った。どのa*b != 0ように高速化できるかわかりません(a|b) != 0。通常、整数の乗算はビット単位のORよりもコストがかかります。しかし、このようなことが時々奇妙になる。Gallery of Processor Cache Effectsの「例7:ハードウェアの複雑さ」の例を参照してください。


2
&「ビット単位のOR」ではなく、(この場合は)「論理AND」です。両方のオペランドがブール値であり、そうではないためです|;-)
siegi

1
@siegi TIL Java '&'は、実際には短絡のない論理ANDです。
StackedCrooked 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.