web-dev-qa-db-ja.com

Javaでは、&&よりも高速にできますか?

このコードでは:

if (value >= x && value <= y) {

value >= xvalue <= yが特定のパターンなしでfalseと同じくらい本当である場合、&演算子を使用すると、&&を使用するよりも高速になりますか?

具体的には、&&が右辺式を遅延評価する方法(つまり、LHSがtrueの場合のみ)を考えています。これは条件を意味しますが、このコンテキストではJava &は両方の厳密な評価を保証します(ブール)部分式。値の結果はどちらの方法でも同じです。

しかし、>=または<=演算子は単純な比較命令を使用しますが、&&は分岐を含む必要があり、この分岐は分岐予測エラーの影響を受けやすい-この非常に有名な質問に従って: なぜソートされていない配列よりもソートされた配列を処理する方が高速ですか?

したがって、遅延コンポーネントを持たないように式を強制すると、確実に決定性が高まり、予測の失敗に対して脆弱になりません。右?

ノート:

  • 明らかに、私の質問に対する答えは、コードがif(value >= x && verySlowFunction())のような場合、Noになります。私は「十分に単純な」RHS表現に焦点を当てています。
  • とにかく条件分岐があります(ifステートメント)。それが無関係であること、そしてboolean b = value >= x && value <= y;のような代替の定式化がより良い例であるかもしれないことを自分自身に証明することはできません。
  • これはすべて、恐ろしいマイクロ最適化の世界に分類されます。ええ、私は知っています:-) ...しかし、面白いですか?

Update興味がある理由を説明するために:Martin Thompsonが彼について書いているシステムをじっと見つめていました Mechanical同情のブログ 、彼が来てから 話をした アーロンについて。重要なメッセージの1つは、ハードウェアにこのような魔法のようなものがすべて含まれており、ソフトウェア開発者が悲劇的にそれを利用できないことです。心配しないで、私はすべてのコードでs/&&/\&/に行くつもりはありません:-) ...しかし、このサイトには、分岐を削除して分岐予測を改善するための質問がたくさんあり、それが発生しました私にとって、条件付きブール演算子はコアでテスト条件です。

もちろん、@ StephenCは、コードを奇妙な形に曲げると、JITが一般的な最適化を見つけるのが簡単ではなくなるという素晴らしい点を示しています。そして、上記の非常に有名な質問は、予測の複雑さを実際の最適化をはるかに超えて推し進めるため、特別です。

ほとんどの(またはalmost all)状況では、&&が最も明確で、シンプルで、最速で、最善のことであることをよく知っています-回答を投稿してくれた人にはとても感謝していますこれを実証します! 「&を高速化できますか?」に対する回答が実際に誰かの経験にあるケースがあるかどうかを確認することに興味があります。 はい ...

Update 2(質問が広すぎるというアドバイスに対処します。この質問に大きな変更を加えることはできません。以下の回答のいくつかを妥協してください。これらは並外れた品質です!)おそらく実例が求められています。これは Guava LongMath クラスからのものです(これを見つけてくれた@maaartinusに感謝します):

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

最初の&をご覧ください。リンクを確認すると、nextメソッドはlessThanBranchFree(...)と呼ばれ、分岐回避領域にいることを示唆します。そして、グアバは実際に広く使用されています。 -目に見えるようにドロップするレベル。それでは、このように質問をしましょう:この&の使用(&&の方が普通です)は本当の最適化ですか?

69
SusanW

わかりましたので、下位レベルでの動作を知りたいと思います...では、バイトコードを見てみましょう!

編集:AMD64用に生成されたアセンブリコードを最後に追加しました。興味深いメモをご覧ください。
EDIT 2(re:OPの「Update 2」): GuavaのisPowerOfTwo method にもasmコードを追加しました。

Javaソース

次の2つの簡単な方法を作成しました。

public boolean AndSC(int x, int value, int y) {
    return value >= x && value <= y;
}

public boolean AndNonSC(int x, int value, int y) {
    return value >= x & value <= y;
}

ご覧のとおり、これらはまったく同じであり、AND演算子のタイプを除きます。

Javaバイトコード

そして、これは生成されたバイトコードです:

  public AndSC(III)Z
   L0
    LINENUMBER 8 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 9 L2
    ICONST_1
    IRETURN
   L1
    LINENUMBER 11 L1
   FRAME SAME
    ICONST_0
    IRETURN
   L3
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
    LOCALVARIABLE x I L0 L3 1
    LOCALVARIABLE value I L0 L3 2
    LOCALVARIABLE y I L0 L3 3
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1
  public AndNonSC(III)Z
   L0
    LINENUMBER 15 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ICONST_1
    GOTO L2
   L1
   FRAME SAME
    ICONST_0
   L2
   FRAME SAME1 I
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L3
    ICONST_1
    GOTO L4
   L3
   FRAME SAME1 I
    ICONST_0
   L4
   FRAME FULL [test/lsoto/AndTest I I I] [I I]
    IAND
    IFEQ L5
   L6
    LINENUMBER 16 L6
    ICONST_1
    IRETURN
   L5
    LINENUMBER 18 L5
   FRAME SAME
    ICONST_0
    IRETURN
   L7
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
    LOCALVARIABLE x I L0 L7 1
    LOCALVARIABLE value I L0 L7 2
    LOCALVARIABLE y I L0 L7 3
    MAXSTACK = 3
    MAXLOCALS = 4

AndSC&&)メソッドは、期待どおりtwo条件付きジャンプを生成します。

  1. valuexをスタックにロードし、valueが小さい場合はL1にジャンプします。それ以外の場合は、次の行を実行し続けます。
  2. valueyをスタックにロードし、valueが大きい場合はL1にもジャンプします。それ以外の場合は、次の行を実行し続けます。
  3. 2つのジャンプのいずれも行われなかった場合は、return trueになります。
  4. そして、return falseであるL1としてマークされた行があります。

ただし、AndNonSC&)メソッドは、three条件付きジャンプを生成します!

  1. valuexをスタックにロードし、valueが小さい場合はL1にジャンプします。結果を保存してANDの他の部分と比較する必要があるため、「save true」または「save false」のいずれかを実行する必要があるため、できません。両方とも同じ命令で。
  2. valueyをスタックにロードし、valueが大きい場合はL1にジャンプします。もう一度trueまたはfalseを保存する必要があります。これは比較結果に応じて2つの異なる行です。
  3. bothの比較が行われたので、コードは実際にAND演算を実行し、両方がtrueの場合、(3回目)ジャンプしてtrueを返します。または、次の行まで実行を続けてfalseを返します。

(予備)結論

私はJavaバイトコードをあまり経験しておらず、何かを見落としているかもしれませんが、&は実際に実行されますさらに悪いよりも&&すべての場合:予測する、または失敗する可能性のある条件ジャンプを含む、実行する命令がより多く生成されます。

他の誰かが提案したように、算術演算で比較を置き換えるためにコードを書き直すことは、&をより良いオプションにする方法かもしれませんが、コードをはるかにわかりにくくします。
私見では、99%のシナリオで手間をかける価値はありません(ただし、非常に最適化する必要がある1%のループでは非常に価値があります)。

編集:AMD64アセンブリ

コメントに記載されているように、同じJavaバイトコードは異なるシステムで異なるマシンコードにつながる可能性があるため、Javaバイトコードは、また、バージョンのパフォーマンスが向上しているため、コンパイラで生成された実際のASMを取得することが唯一の方法です。
両方の方法のAMD64 ASMの手順を印刷しました。以下は関連する行です(エントリポイントの削除など)。

注:特に指定のない限り、Java 1.8.0_91でコンパイルされたすべてのメソッド

メソッドAndSCにデフォルトのオプションを指定

  # {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923e3e: cmp    %r8d,%r9d
  0x0000000002923e41: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e4b: movabs $0x108,%rsi
  0x0000000002923e55: jl     0x0000000002923e65
  0x0000000002923e5b: movabs $0x118,%rsi
  0x0000000002923e65: mov    (%rax,%rsi,1),%rbx
  0x0000000002923e69: lea    0x1(%rbx),%rbx
  0x0000000002923e6d: mov    %rbx,(%rax,%rsi,1)
  0x0000000002923e71: jl     0x0000000002923eb0  ;*if_icmplt
                                                ; - AndTest::AndSC@2 (line 22)

  0x0000000002923e77: cmp    %edi,%r9d
  0x0000000002923e7a: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e84: movabs $0x128,%rsi
  0x0000000002923e8e: jg     0x0000000002923e9e
  0x0000000002923e94: movabs $0x138,%rsi
  0x0000000002923e9e: mov    (%rax,%rsi,1),%rdi
  0x0000000002923ea2: lea    0x1(%rdi),%rdi
  0x0000000002923ea6: mov    %rdi,(%rax,%rsi,1)
  0x0000000002923eaa: jle    0x0000000002923ec1  ;*if_icmpgt
                                                ; - AndTest::AndSC@7 (line 22)

  0x0000000002923eb0: mov    $0x0,%eax
  0x0000000002923eb5: add    $0x30,%rsp
  0x0000000002923eb9: pop    %rbp
  0x0000000002923eba: test   %eax,-0x1c73dc0(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ec0: retq                      ;*ireturn
                                                ; - AndTest::AndSC@13 (line 25)

  0x0000000002923ec1: mov    $0x1,%eax
  0x0000000002923ec6: add    $0x30,%rsp
  0x0000000002923eca: pop    %rbp
  0x0000000002923ecb: test   %eax,-0x1c73dd1(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ed1: retq   

メソッドAndSC with -XX:PrintAssemblyOptions=intelオプション

  # {method} {0x00000000170a0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c26e2c: cmp    r9d,r8d
  0x0000000002c26e2f: jl     0x0000000002c26e36  ;*if_icmplt
  0x0000000002c26e31: cmp    r9d,edi
  0x0000000002c26e34: jle    0x0000000002c26e44  ;*iconst_0
  0x0000000002c26e36: xor    eax,eax            ;*synchronization entry
  0x0000000002c26e38: add    rsp,0x10
  0x0000000002c26e3c: pop    rbp
  0x0000000002c26e3d: test   DWORD PTR [rip+0xffffffffffce91bd],eax        # 0x0000000002910000
  0x0000000002c26e43: ret    
  0x0000000002c26e44: mov    eax,0x1
  0x0000000002c26e49: jmp    0x0000000002c26e38

メソッドAndNonSCにデフォルトのオプションを指定

  # {method} {0x0000000016da0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923a78: cmp    %r8d,%r9d
  0x0000000002923a7b: mov    $0x0,%eax
  0x0000000002923a80: jl     0x0000000002923a8b
  0x0000000002923a86: mov    $0x1,%eax
  0x0000000002923a8b: cmp    %edi,%r9d
  0x0000000002923a8e: mov    $0x0,%esi
  0x0000000002923a93: jg     0x0000000002923a9e
  0x0000000002923a99: mov    $0x1,%esi
  0x0000000002923a9e: and    %rsi,%rax
  0x0000000002923aa1: cmp    $0x0,%eax
  0x0000000002923aa4: je     0x0000000002923abb  ;*ifeq
                                                ; - AndTest::AndNonSC@21 (line 29)

  0x0000000002923aaa: mov    $0x1,%eax
  0x0000000002923aaf: add    $0x30,%rsp
  0x0000000002923ab3: pop    %rbp
  0x0000000002923ab4: test   %eax,-0x1c739ba(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923aba: retq                      ;*ireturn
                                                ; - AndTest::AndNonSC@25 (line 30)

  0x0000000002923abb: mov    $0x0,%eax
  0x0000000002923ac0: add    $0x30,%rsp
  0x0000000002923ac4: pop    %rbp
  0x0000000002923ac5: test   %eax,-0x1c739cb(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923acb: retq   

メソッドAndNonSC with -XX:PrintAssemblyOptions=intelオプション

  # {method} {0x00000000170a0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c270b5: cmp    r9d,r8d
  0x0000000002c270b8: jl     0x0000000002c270df  ;*if_icmplt
  0x0000000002c270ba: mov    r8d,0x1            ;*iload_2
  0x0000000002c270c0: cmp    r9d,edi
  0x0000000002c270c3: cmovg  r11d,r10d
  0x0000000002c270c7: and    r8d,r11d
  0x0000000002c270ca: test   r8d,r8d
  0x0000000002c270cd: setne  al
  0x0000000002c270d0: movzx  eax,al
  0x0000000002c270d3: add    rsp,0x10
  0x0000000002c270d7: pop    rbp
  0x0000000002c270d8: test   DWORD PTR [rip+0xffffffffffce8f22],eax        # 0x0000000002910000
  0x0000000002c270de: ret    
  0x0000000002c270df: xor    r8d,r8d
  0x0000000002c270e2: jmp    0x0000000002c270c0
  • まず、生成されるASMコードは、デフォルトのAT&T構文を選択するか、Intel構文を選択するかによって異なります。
  • AT&T構文の場合:
    • ASMコードは、実際にはAndSCメソッドではlongerであり、すべてのバイトコードIF_ICMP*が2つのアセンブリジャンプ命令に変換され、合計4つの条件付きジャンプになります。
    • 一方、AndNonSCメソッドの場合、コンパイラはより単純なコードを生成します。各バイトコードIF_ICMP*は、3つの条件付きジャンプの元のカウントを維持しながら、1つのアセンブリジャンプ命令のみに変換されます。
  • Intel構文の場合:
    • AndSCのASMコードは短く、条件付きジャンプが2つだけです(最後に無条件のjmpをカウントしません)。実際には、結果に応じて2つのCMP、2つのJL/E、1つのXOR/MOVのみです。
    • AndNonSCのASMコードはAndSCのものよりも長くなりました! ただし、、1つだけの条件付きジャンプ(最初の比較用)があり、レジスタを使用して、最初の結果と2番目の結果を直接ジャンプして比較します。 。

ASMコード分析後の結論

  • AMD64マシン言語レベルでは、&演算子は、条件付きジャンプが少ないASMコードを生成するようです。これは、高い予測失敗率(ランダムvaluesなど)に適している場合があります。
  • 一方、&&演算子は、より少ない命令で(とにかく-XX:PrintAssemblyOptions=intelオプションを使用して)ASMコードを生成するようです。これは、本当に長い予測付きループに適している可能性があります。各比較のCPUサイクル数が少ないほど、長期的には違いが生じる可能性がある、使いやすい入力。

いくつかのコメントで述べたように、これはシステム間で大きく異なるため、分岐予測の最適化について話している場合、唯一の本当の答えは次のとおりです: JVM実装、コンパイラ、CPU、入力データ


補遺:グアバのisPowerOfTwoメソッド

ここで、Guavaの開発者は、指定された数値が2の累乗であるかどうかを計算するきちんとした方法を考え出しました。

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

OPの引用:

これは&を使用していますか(&&の方がより一般的でしょう)実際の最適化ですか?

そうであるかどうかを確認するために、テストクラスに2つの同様のメソッドを追加しました。

public boolean isPowerOfTwoAND(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

public boolean isPowerOfTwoANDAND(long x) {
    return x > 0 && (x & (x - 1)) == 0;
}

グアバのバージョン用のIntelのASMコード

  # {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103bbe: movabs rax,0x0
  0x0000000003103bc8: cmp    rax,r8
  0x0000000003103bcb: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103bd5: movabs rsi,0x108
  0x0000000003103bdf: jge    0x0000000003103bef
  0x0000000003103be5: movabs rsi,0x118
  0x0000000003103bef: mov    rdi,QWORD PTR [rax+rsi*1]
  0x0000000003103bf3: lea    rdi,[rdi+0x1]
  0x0000000003103bf7: mov    QWORD PTR [rax+rsi*1],rdi
  0x0000000003103bfb: jge    0x0000000003103c1b  ;*lcmp
  0x0000000003103c01: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c0b: inc    DWORD PTR [rax+0x128]
  0x0000000003103c11: mov    eax,0x1
  0x0000000003103c16: jmp    0x0000000003103c20  ;*goto
  0x0000000003103c1b: mov    eax,0x0            ;*lload_1
  0x0000000003103c20: mov    rsi,r8
  0x0000000003103c23: movabs r10,0x1
  0x0000000003103c2d: sub    rsi,r10
  0x0000000003103c30: and    rsi,r8
  0x0000000003103c33: movabs rdi,0x0
  0x0000000003103c3d: cmp    rsi,rdi
  0x0000000003103c40: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c4a: movabs rdi,0x140
  0x0000000003103c54: jne    0x0000000003103c64
  0x0000000003103c5a: movabs rdi,0x150
  0x0000000003103c64: mov    rbx,QWORD PTR [rsi+rdi*1]
  0x0000000003103c68: lea    rbx,[rbx+0x1]
  0x0000000003103c6c: mov    QWORD PTR [rsi+rdi*1],rbx
  0x0000000003103c70: jne    0x0000000003103c90  ;*lcmp
  0x0000000003103c76: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c80: inc    DWORD PTR [rsi+0x160]
  0x0000000003103c86: mov    esi,0x1
  0x0000000003103c8b: jmp    0x0000000003103c95  ;*goto
  0x0000000003103c90: mov    esi,0x0            ;*iand
  0x0000000003103c95: and    rsi,rax
  0x0000000003103c98: and    esi,0x1
  0x0000000003103c9b: mov    rax,rsi
  0x0000000003103c9e: add    rsp,0x50
  0x0000000003103ca2: pop    rbp
  0x0000000003103ca3: test   DWORD PTR [rip+0xfffffffffe44c457],eax        # 0x0000000001550100
  0x0000000003103ca9: ret    

&&バージョン用のIntelのasmコード

  # {method} {0x0000000017580bd0} 'isPowerOfTwoANDAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103438: movabs rax,0x0
  0x0000000003103442: cmp    rax,r8
  0x0000000003103445: jge    0x0000000003103471  ;*lcmp
  0x000000000310344b: mov    rax,r8
  0x000000000310344e: movabs r10,0x1
  0x0000000003103458: sub    rax,r10
  0x000000000310345b: and    rax,r8
  0x000000000310345e: movabs rsi,0x0
  0x0000000003103468: cmp    rax,rsi
  0x000000000310346b: je     0x000000000310347b  ;*lcmp
  0x0000000003103471: mov    eax,0x0
  0x0000000003103476: jmp    0x0000000003103480  ;*ireturn
  0x000000000310347b: mov    eax,0x1            ;*goto
  0x0000000003103480: and    eax,0x1
  0x0000000003103483: add    rsp,0x40
  0x0000000003103487: pop    rbp
  0x0000000003103488: test   DWORD PTR [rip+0xfffffffffe44cc72],eax        # 0x0000000001550100
  0x000000000310348e: ret    

この特定の例では、JITコンパイラはfar Guavaの&&バージョンよりも&バージョンのアセンブリコードを生成します(そして、昨日の結果の後、正直に驚いた)。
グアバのものと比較すると、&&バージョンは、JITがコンパイルするためのバイトコードが25%削減され、アセンブリ命令が50%削減され、条件付きジャンプは2つだけです(&バージョンには)。

したがって、すべては、グアバの&メソッドが、より自然な&&バージョンよりも効率が低いことを示しています。

... またはそれは?

前述のように、上記の例をJava 8で実行しています:

C:\....>Java -version
Java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

しかし、Java 7)に切り替えるとどうなりますか?

C:\....>c:\jdk1.7.0_79\bin\Java -version
Java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
C:\....>c:\jdk1.7.0_79\bin\Java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain
  .....
  0x0000000002512bac: xor    r10d,r10d
  0x0000000002512baf: mov    r11d,0x1
  0x0000000002512bb5: test   r8,r8
  0x0000000002512bb8: jle    0x0000000002512bde  ;*ifle
  0x0000000002512bba: mov    eax,0x1            ;*lload_1
  0x0000000002512bbf: mov    r9,r8
  0x0000000002512bc2: dec    r9
  0x0000000002512bc5: and    r9,r8
  0x0000000002512bc8: test   r9,r9
  0x0000000002512bcb: cmovne r11d,r10d
  0x0000000002512bcf: and    eax,r11d           ;*iand
  0x0000000002512bd2: add    rsp,0x10
  0x0000000002512bd6: pop    rbp
  0x0000000002512bd7: test   DWORD PTR [rip+0xffffffffffc0d423],eax        # 0x0000000002120000
  0x0000000002512bdd: ret    
  0x0000000002512bde: xor    eax,eax
  0x0000000002512be0: jmp    0x0000000002512bbf
  .....

驚き! Java 7でJITコンパイラーによって&メソッド用に生成されたアセンブリコードには、one条件付きジャンプのみがあり、もっと短くなっています! &&メソッド(これを信頼する必要があります。エンディングを乱雑にしたくない!)は、2つの条件付きジャンプと2、3少ない命令、topsでほぼ同じままです。
グアバのエンジニアは、結局のところ、彼らが何をしていたかを知っていたようです。 (彼らがJava 7実行時間、つまり;-)を最適化しようとしていた場合

OPの最新の質問に戻ります。

これは&を使用していますか(&&の方がより一般的でしょう)実際の最適化ですか?

そして私見答えは同じです、この(非常に!)特定のシナリオでも:それはあなたのJVMに依存します実装、コンパイラ、CPU、入力データ

72
walen

これらの種類の質問については、マイクロベンチマークを実行する必要があります。このテストでは [〜#〜] jmh [〜#〜] を使用しました。

ベンチマークは次のように実装されます

// boolean logical AND
bh.consume(value >= x & y <= value);

そして

// conditional AND
bh.consume(value >= x && y <= value);

そして

// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)

ベンチマーク名に従ってvalue, x and yの値を使用します。

スループットベンチマークの結果(5回のウォームアップと10回の測定反復)は次のとおりです。

Benchmark                                 Mode  Cnt    Score    Error   Units
Benchmark.isBooleanANDBelowRange          thrpt   10  386.086 ▒ 17.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.240 ▒  7.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.847 ▒ 15.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.877 ▒ 11.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.743 ▒ 15.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.524 ▒ 16.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.190 ▒ 19.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.094 ▒ 15.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.913 ▒  5.537  ops/us

結果は、評価自体に関してそれほど違いはありません。そのコードにパフォーマンスへの影響が見られない限り、最適化を試みません。コード内の場所に応じて、ホットスポットコンパイラは最適化を決定する場合があります。上記のベンチマークではおそらくカバーされません。

いくつかの参照:

ブール論理AND -オペランド値が両方ともtrueの場合、結果値はtrueです。それ以外の場合、結果はfalse
条件付きAND -&に似ていますが、左側のオペランドの値がtrueである場合にのみ、右側のオペランドを評価します
ビット単位のOR -結果値は、ビット単位の包括的ORオペランド値の

23
SubOptimal

私はこれに別の角度から来ます。

これらの2つのコードフラグメントを検討してください。

  if (value >= x && value <= y) {

そして

  if (value >= x & value <= y) {

valuexyがプリミティブ型であると仮定すると、これらの2つの(部分)ステートメントは、すべての可能な入力値に対して同じ結果をもたらします。 (ラッパータイプが関係する場合、&バージョンではなく&&バージョンで失敗する可能性のあるnullの暗黙的なyテストのため、ラッパータイプは完全に同等ではありません。 )

JITコンパイラーが適切に機能している場合、オプティマイザーはこれら2つのステートメントが同じことを行うと推測できます。

  • 一方が他方よりも予想どおりに高速である場合、より高速なバージョンを使用できるはずです... JITコンパイル済みコード内

  • そうでない場合、ソースコードレベルで使用されるバージョンは関係ありません。

  • JITコンパイラーはコンパイルする前にパス統計を収集するため、プログラマー(!)が実行特性についてより多くの情報を持っている可能性があります。

  • 現在の世代のJITコンパイラー(特定のプラットフォーム)がこれを処理するのに十分に最適化されていない場合、経験的な証拠がこれを指すかどうかに応じて、次世代がうまくいく可能性がありますworthwhile =最適化するパターン。

  • 実際、このために最適化する方法でJavaコードを記述した場合、チャンスがあります。これは、より「あいまいな」バージョンのコードを選択することにより、 might inhibit現在または将来のJITコンパイラの最適化能力。

要するに、ソースコードレベルでこの種のマイクロ最適化を行うべきではないと思います。そして、あなたがこの議論を受け入れるなら1、そしてその論理的結論に従うと、どちらのバージョンが速いのかという疑問が...2

1-私はこれが証拠に近いと主張していません。

2-あなたが実際にJava JITコンパイラを記述する人々の小さなコミュニティの1つでない限り...


「非常に有名な質問」は、2つの点で興味深いものです。

  • 一方では、違いを生むために必要な種類の最適化がJITコンパイラの能力をはるかに超えている例です。

  • 一方で、配列を並べ替えるのは必ずしも正しいことではありません...並べ替えられた配列をより速く処理できるからです。配列をソートするコストは、節約よりも(はるかに)高くなる可能性があります。

12
Stephen C

&または&&のいずれかを使用するには、評価する条件が依然として必要であるため、処理時間を節約できない可能性があります。 。

& over &&を使用してナノ秒を節約することは、非常にまれな状況でそれが無意味な場合、& over &&

編集

私は興味を持ち、いくつかのベンチマークを実行することにしました。

私はこのクラスを作りました:

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        runWithOneAnd(30);
        runWithTwoAnds(30);
    }

    static void runWithOneAnd(int value){
        if(value >= x & value <= y){

        }
    }

    static void runWithTwoAnds(int value){
        if(value >= x && value <= y){

        }
    }
}

netBeansを使用してプロファイリングテストを実行しました。処理時間を節約するためにprintステートメントを使用しませんでした。両方がtrueに評価されることを知っているだけです。

最初のテスト:

The first profiling test

2番目のテスト:

The second profiling test

第3テスト:

The third profiling test

プロファイリングテストでわかるように、1つの&のみを使用すると、2つの&&を使用した場合と比較して、実際に実行に2〜3倍時間がかかります。これは、1つの&のみからより良いパフォーマンスを期待しているので、奇妙に思えます。

理由は100%わかりません。どちらの場合も、両方が真であるため、両方の式を評価する必要があります。 JVMは、バックグラウンドで特別な最適化を行って速度を向上させていると思われます。

ストーリーのモラル:慣習は良く、最適化は時期尚早です。


Edit 2

@SvetlinZarevのコメントを念頭に置いてベンチマークコードを再作成し、その他のいくつかの改善を加えました。変更されたベンチマークコードは次のとおりです。

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        oneAndBothTrue();
        oneAndOneTrue();
        oneAndBothFalse();
        twoAndsBothTrue();
        twoAndsOneTrue();
        twoAndsBothFalse();
        System.out.println(b);
    }

    static void oneAndBothTrue() {
        int value = 30;
        for (int i = 0; i < 2000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothTrue() {
        int value = 30;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    //I wanted to avoid print statements here as they can
    //affect the benchmark results. 
    static StringBuilder b = new StringBuilder();
    static int times = 0;

    static void doSomething(){
        times++;
        b.append("I have run ").append(times).append(" times \n");
    }
}

次に、パフォーマンステストを示します。

テスト1:

enter image description here

テスト2:

enter image description here

テスト3:

enter image description here

これには、異なる値と異なる条件も考慮されます。

1つの&を使用すると、両方の条件が真である場合の実行時間が長くなり、約60%または2ミリ秒長くなります。 1つまたは両方の条件がfalseの場合、1つの&はより高速に実行されますが、実行されるのは約0.30〜0.50ミリ秒です。したがって、ほとんどの場合、&&&よりも高速に実行されますが、パフォーマンスの違いはごくわずかです。

6
Luke Melaia

後は次のようになります。

x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0  // integer bit-or

興味深いことに、バイトコードをほとんど見たいと思うでしょう。しかし、言うのは難しい。これがCの質問だったらいいのに。

3
Joop Eggen

これが私に説明された方法は、&&はシリーズの最初のチェックがfalseの場合にfalseを返しますが、&はfalseの数に関係なくシリーズのすべてのアイテムをチェックします。 I.E.

if(x> 0 && x <= 10 && x

より速く実行されます

if(x> 0&x <= 10&x

Xが10より大きい場合、単一のアンパサンドは残りの条件を引き続きチェックするのに対し、二重のアンパサンドは最初の非真の条件の後に壊れるので。

0
milkman

私も答えに興味があったので、このために次の(簡単な)テストを書きました。

private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;

@Before
public void setUp() {
    this.random = new Random();
}

@After
public void tearDown() {
    random = null;
}

@Test
public void testSingleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of single operand: " + (end - start));
}

@Test
public void testDoubleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of double operand: " + (end - start));
}

最終結果として、&&との比較は常に速度の点で勝ち、&よりも約1.5/2ミリ秒速くなります。

編集: @SvetlinZarevが指摘したように、整数を取得するためにランダムにかかった時間も測定していました。事前に入力された乱数の配列を使用するように変更しました。これにより、単一オペランドテストの期間が大幅に変動しました。いくつかの実行の違いは最大6〜7ミリ秒でした。

0
Oromë