Development Tip

Java에서 &&보다 빠를 수 있습니까?

yourdevel 2020. 11. 1. 18:48
반응형

Java에서 &&보다 빠를 수 있습니까?


이 코드에서 :

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

value >= xvalue <= y특별한 패턴 등의 가능성이 허위 사실로하고, 사용하는 것 &운영자는 빨리 사용하여보다&& ?

특히, &&조건을 의미하는 반면, &이 컨텍스트 에서 Java 에서는 두 (부울) 하위 표현식에 대한 엄격한 평가를 보장 하는 반면 오른쪽 표현식 (즉, LHS가 true 인 경우에만)을 얼마나 느리게 평가 하는지에 대해 생각 하고 있습니다 . 값 결과는 어느 쪽이든 동일합니다.

그러나 >=또는 <=연산자는 간단한 비교 명령어를 사용 하지만 &&분기를 포함해야하며 해당 분기는 분기 예측 실패에 취약합니다. 이 매우 유명한 질문 : 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 것이 더 빠른 이유는 무엇입니까?

따라서 식에 게으른 구성 요소가 없도록 강제하는 것은 확실히 더 결정적이며 예측 실패에 취약하지 않습니다. 권리?

메모:

  • 분명히 내 질문에 대한 대답 은 코드가 다음과 같으면 아니오if(value >= x && verySlowFunction()) 입니다. 저는 "충분히 단순한"RHS 표현에 초점을 맞추고 있습니다.
  • 어쨌든 거기에 조건부 분기가 있습니다 ( if문). 나는 그것이 무관하다는 것을 스스로 증명할 수 없으며 대체 공식이 더 나은 예가 될 수 있습니다.boolean b = value >= x && value <= y;
  • 이 모든 것이 끔찍한 마이크로 최적화의 세계에 속합니다. 그래, 알아 :-) ...하지만 흥미 롭다?

업데이트 내가 관심이있는 이유를 설명하기 위해 : 나는 Martin Thompson이 Aeron에 대해 이야기를 한 후 Mechanical Sympathy 블로그 에서 작성하고있는 시스템을보고 있습니다. 핵심 메시지 중 하나는 우리의 하드웨어에이 모든 마법 같은 것들이 있고 우리 소프트웨어 개발자들은 비극적으로 그것을 활용하지 못한다는 것입니다. 걱정하지 마세요. 내 모든 코드에 대해 s / && / \ & /로 이동하지 않을 것입니다. :-) ...하지만이 사이트에는 분기를 제거하여 분기 예측을 개선하는 방법에 대한 많은 질문이 있습니다. 나에게 조건부 부울 연산자는 테스트 조건 의 핵심 입니다.

물론 @StephenC는 코드를 이상한 모양으로 구부리면 JIT가 일반적인 최적화를 쉽게 찾을 수 없다는 환상적인 점을 보여줍니다. 그리고 위에서 언급 한 매우 유명한 질문은 예측의 복잡성을 실제 최적화를 훨씬 뛰어 넘기 때문에 특별합니다.

나는 대부분의 (또는 거의 모든 ) 상황 &&에서 가장 명확하고, 가장 간단하고, 가장 빠르고, 최선의 방법이라는 것을 잘 알고 있습니다. 그러나 이것을 증명하는 답변을 게시 한 사람들에게 매우 감사합니다! 실제로 누군가의 경험에서 " &더 빠를 수 있습니까 ?"에 대한 답이있는 경우가 있는지 정말 궁금합니다. 수 있습니다 ...

업데이트 2 : (질문이 지나치게 광범위하다는 충고를 처리합니다. 아래 답변 중 일부가 타협 할 수 있으므로이 질문을 크게 변경하고 싶지 않습니다. 이는 뛰어난 품질입니다!) 아마도 야생의 예가 호출 될 것입니다. 에 대한; 이것은 Guava LongMath 클래스에서 가져온 것입니다 (이것을 찾은 @maaartinus에게 대단히 감사합니다) :

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

먼저 보 &시겠습니까? 링크를 확인하면 다음 메서드가 호출 lessThanBranchFree(...)되어 분기 회피 영역에 있음을 암시합니다. 구아바는 실제로 널리 사용됩니다. 저장된 모든 사이클은 해수면을 눈에 띄게 떨어 뜨립니다. 그래서 질문이 방법을 만들어 보자 : 인이 사용 &(여기서 &&더 일반적인 것) 진짜 최적화?


좋아, 그래서 당신은 그것이 낮은 수준에서 어떻게 동작하는지 알고 싶다 ... 그럼 바이트 코드를 살펴 보자!

편집 : 끝에 AMD64에 대해 생성 된 어셈블리 코드를 추가했습니다. 흥미로운 메모를 찾아보세요.
EDIT 2 (re : OP의 "Update 2") : Guava의 isPowerOfTwo방법에 대한 asm 코드도 추가되었습니다 .

자바 소스

이 두 가지 빠른 방법을 작성했습니다.

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 연산자 유형을 제외하고 정확히 동일합니다.

자바 바이트 코드

그리고 이것은 생성 된 바이트 코드입니다.

  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( &&) 메소드는 생성 이 개 예상대로 조건부 점프 :

  1. 스택에 로드 value되고 더 낮 으면 xL1로 점프합니다 value. 그렇지 않으면 다음 줄을 계속 실행합니다.
  2. 스택에 로드 value되고 더 큰 y경우 L1로도 점프합니다 value. 그렇지 않으면 다음 줄을 계속 실행합니다.
  3. return true두 점프 중 어느 것도 수행되지 않은 경우에 발생합니다 .
  4. 그리고 L1로 표시된 라인이 있습니다 return false.

그러나 AndNonSC( &) 메서드는 세 가지 조건부 점프를 생성 합니다!

  1. 스택에 로드 value되고 더 낮 으면 xL1로 점프합니다 value. 이제 AND의 다른 부분과 비교하기 위해 결과를 저장해야하므로 "save true"또는 "save false" 를 실행 해야하므로 동일한 명령으로 둘 다 수행 할 수 없습니다.
  2. 스택에 로드 value되고 더 큰 y경우 L1로 점프합니다 value. 다시 한 번 저장해야 true하거나 false비교 결과에 따라 두 줄이 다릅니다.
  3. 이제 비교가 모두 완료되었으므로 코드는 실제로 AND 연산을 실행하고 둘 다 참이면 점프 (세 번째)로 참을 반환합니다. 그렇지 않으면 false를 반환하기 위해 다음 줄로 계속 실행됩니다.

(예비) 결론

Java 바이트 코드에 대해 그다지 경험이 많지 않고 간과했을 수도 있지만 &실제로 모든 경우 보다 성능이 더 나빠질&&같습니다. .

다른 누군가가 제안한 것처럼 비교를 산술 연산으로 대체하기 위해 코드를 다시 작성 &하는 것이 더 나은 옵션 을 만드는 방법 일 수 있지만 코드를 훨씬 덜 명확하게 만드는 대가가 있습니다.
IMHO는 시나리오의 99 %에 대해 번거로울 가치가 없습니다.

편집 : AMD64 어셈블리

주석에서 언급했듯이 동일한 Java 바이트 코드는 다른 시스템에서 다른 기계 코드로 이어질 수 있으므로 Java 바이트 코드는 어떤 AND 버전이 더 나은 성능을 발휘하는지에 대한 힌트를 제공 할 수 있지만 컴파일러가 생성 한 실제 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-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-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 구문을 선택하는지 인텔 구문을 선택하는지에 따라 다릅니다.
  • AT & T 구문 사용 :
    • ASM 코드는 실제로 총 4 개의 조건부 점프에 대해 모든 바이트 코드 가 2 개의 어셈블리 점프 명령으로 변환 되는 메서드의 경우 더 길다 .AndSCIF_ICMP*
    • 한편, AndNonSC메서드의 경우 컴파일러는보다 간단한 코드를 생성합니다. 여기서 각 바이트 코드 IF_ICMP*는 3 개의 조건부 점프의 원래 개수를 유지하면서 하나의 어셈블리 점프 명령으로 만 변환됩니다.
  • Intel 구문 사용 :
    • 에 대한 ASM 코드 AndSC는 더 짧아서 조건부 점프가 2 번뿐입니다 ( jmp끝에 비 조건부 계산하지 않음 ). 실제로 결과에 따라 CMP 2 개, JL / E 2 개 및 XOR / MOV 1 개입니다.
    • 의 ASM 코드 AndNonSC가 이제 코드 보다 길어졌습니다 AndSC! 그러나 첫 번째 비교를위한 조건부 점프는 1 개뿐입니다. 레지스터를 사용하여 더 이상 점프없이 첫 번째 결과를 두 번째 결과와 직접 비교합니다.

ASM 코드 분석 후 결론

  • AMD64 기계 언어 수준에서 &운영자는 더 적은 조건부 점프로 ASM 코드를 생성하는 것으로 보이며, 이는 높은 예측 실패율 ( value: 임의)에 더 좋을 수 있습니다 .
  • 반면에 &&연산자는 더 적은 명령 ( -XX:PrintAssemblyOptions=intel어쨌든 옵션 포함)으로 ASM 코드를 생성하는 것으로 보입니다 . 이는 예측 친화적 인 입력 을 사용하는 정말 긴 루프에 더 좋을 수 있습니다. 각 비교에 대한 CPU주기 수가 적 으면 차이를 만들 수 있습니다. 장기적으로는.

일부 주석에서 언급했듯이 이것은 시스템마다 크게 다를 것이므로 분기 예측 최적화에 대해 이야기하는 경우 유일한 실제 대답은 JVM 구현, 컴파일러, CPU 및 귀하의 입력 데이터 .


부록 : 구아바의 isPowerOfTwo방법

여기에서 Guava의 개발자는 주어진 숫자가 2의 거듭 제곱인지 계산하는 깔끔한 방법을 고안했습니다.

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

OP 인용 :

의 사용이다 &(여기서 &&더 일반적인 것) 진짜 최적화?

이것이 맞는지 알아보기 위해 테스트 클래스에 두 가지 유사한 메서드를 추가했습니다.

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

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

Guava 버전에 대한 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    

&&버전에 대한 인텔의 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 컴파일러는 Guava 버전보다 버전에 대한 어셈블리 코드를 훨씬 적게 생성합니다 (그리고 어제의 결과 이후 솔직히 놀랐습니다). Guava와 비교할 때이 버전은 JIT가 컴파일 할 바이트 코드를 25 %, 어셈블리 명령을 50 % 줄이며, 조건부 점프가 2 개뿐입니다 ( 버전에는 4 개가 있음).&&&
&&&

그래서 모든 것은 Guava의 &방법이보다 "자연스러운" &&버전 보다 덜 효율적이라는 것을 지적합니다 .

... 아니면?

앞서 언급했듯이 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 컴파일러에 의해 메소드 용으로 생성 된 어셈블리 코드는 현재 하나의 조건부 점프 만 가지고 있으며 훨씬 더 짧습니다! 반면 &&방법 (당신은! 내가 결말을 복잡하게하고 싶지 않아,이 날 믿어해야합니다)의 두 가지 조건 점프 몇 덜 지침, 정상으로 거의 동일 남아있다.
결국 Guava의 엔지니어들은 그들이 무엇을하고 있는지 알고 있었던 것 같습니다! (Java 7 실행 시간을 최적화하려는 경우 ;-)

그래서 OP의 최근 질문으로 돌아갑니다.

의 사용이다 &(여기서 &&더 일반적인 것) 진짜 최적화?

IMHO에 대한 대답은 동일 합니다.이 (매우!) 특정 시나리오에서도 JVM 구현, 컴파일러, CPU 및 입력 데이터에 따라 다릅니다 .


이러한 종류의 질문에 대해서는 마이크로 벤치 마크를 실행해야합니다. 이 테스트에 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 피연산자 값으로는


나는 이것을 다른 각도에서 올 것입니다.

이 두 코드 조각을 고려하십시오.

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

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

우리가 가정하는 경우 value, x, y다음 두 (부분) 문은 가능한 모든 입력 값에 대해 동일한 결과를 제공합니다 기본 유형을 가지고있다. (래퍼 유형이 포함 된 경우 암시 적 null테스트 y&버전이 아닌 버전 에서 실패 할 수 있으므로 정확히 동일 하지 않습니다 &&.)

JIT 컴파일러가 좋은 일을하고 있다면 그 최적화 프로그램은이 두 문장이 같은 일을한다는 것을 추론 할 수있을 것입니다.

  • 하나가 다른 것보다 예측 가능하게 빠르다면 JIT 컴파일 코드에서 더 빠른 버전을 사용할 수 있어야 합니다 .

  • 그렇지 않은 경우 소스 코드 수준에서 사용되는 버전은 중요하지 않습니다.

  • JIT 컴파일러는 컴파일하기 전에 경로 통계를 수집하므로 프로그래머 (!)가 실행하는 특성에 대한 더 많은 정보를 잠재적으로 가질 수 있습니다.

  • 현재 세대의 JIT 컴파일러 (주어진 플랫폼에서)가이를 처리하기에 충분히 최적화되지 않으면, 경험적 증거가 이것이 최적화 가치있는 패턴 임을 지적하는지 여부에 따라 다음 세대가 잘 할 수 있습니다 .

  • 당신이 방법으로 당신에게 자바 코드를 작성하는 경우 실제로,이 최적화되었으며이 있다는 것을 기회 코드의 더 "모호한"버전을 선택하여, 당신은 수 있음을 억제 최적화 현재 또는 미래의 JIT 컴파일러의 능력.

간단히 말해서 소스 코드 수준에서 이러한 종류의 마이크로 최적화를 수행해서는 안된다고 생각합니다. 그리고이 주장 1 을 받아들이고 논리적 인 결론에 도달한다면 어떤 버전이 더 빠른지에 대한 질문은 ... moot 2 입니다.

1-나는 이것이 거의 증거라고 주장하지 않습니다.

2-실제로 Java JIT 컴파일러를 작성하는 작은 커뮤니티 중 하나가 아니라면 ...


"매우 유명한 질문"은 두 가지 측면에서 흥미 롭습니다.

  • 한편으로는 차이를 만드는 데 필요한 최적화 유형이 JIT 컴파일러의 기능을 뛰어 넘는 예입니다.

  • 반면에 정렬 된 배열이 더 빨리 처리 될 수 있기 때문에 배열을 정렬하는 것이 반드시 올바른 것은 아닙니다. 어레이 정렬 비용은 절약하는 것보다 훨씬 더 클 수 있습니다.


중 하나를 사용 &하거나 &&계속하는 것은 그것이 어떤 처리 시간을 절약 할 수 없을 그래서 조건이 평가 될 필요 - 그것도 하나만을 평가해야 할 때 두 표현식을 평가하고 고려 여기에 추가 할 수 있습니다.

사용 &을 통해 &&매우 드문 상황에서 무의미한 것을, 당신은 이미 당신이 사용하는 저장 한 것보다 차이 고민 더 많은 시간을 허비 한 경우 나노초을 저장 &이상을 &&.

편집하다

호기심이 생겨 벤치 마크를하기로 결정했습니다.

이 수업을 만들었습니다.

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. 둘 다 .

첫 번째 테스트 :

첫 번째 프로파일 링 테스트

두 번째 테스트 :

두 번째 프로파일 링 테스트

세 번째 테스트 :

세 번째 프로파일 링 테스트

프로파일 링 테스트에서 알 수 있듯이 하나만 사용하면 &실제로 두를 사용하는 것보다 실행하는 데 2-3 배 더 오래 걸립니다 &&. 이것은 내가 단 하나에서 더 나은 성능을 기대했던 것처럼 어떤 이상하게 공격 &합니다.

왜 그런지 100 % 확실하지 않습니다. 두 경우 모두 true이므로 두 식을 모두 평가해야합니다. JVM이 속도를 높이기 위해 배후에서 특별한 최적화를 수행한다고 생각합니다.

이야기의 도덕 : 관습은 좋고 조기 최적화는 나쁘다.


편집 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 :

여기에 이미지 설명 입력

테스트 2 :

여기에 이미지 설명 입력

테스트 3 :

여기에 이미지 설명 입력

이것은 다른 값과 다른 조건도 고려합니다.

하나를 사용 &하면 두 조건이 모두 참일 때 실행하는 데 더 많은 시간이 걸리며 약 60 % 또는 2 밀리 초 더 많은 시간이 걸립니다. 하나 또는 두 조건이 모두 거짓이면 하나 &가 더 빠르게 실행되지만 약 0.30-0.50 밀리 초 만 더 빠르게 실행됩니다. 따라서 대부분의 경우 &보다 빠르게 실행 &&되지만 성능 차이는 여전히 미미합니다.


당신이 추구하는 것은 다음과 같습니다.

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

흥미롭게도, 거의 바이트 코드를보고 싶을 것입니다. 하지만 말하기 어렵습니다. 이것이 C 질문 이었으면합니다.


나는 대답도 궁금해서 이것에 대해 다음과 같은 (간단한) 테스트를 작성했습니다.

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가 지적했듯이 정수를 얻는 데 Random이 걸리는 시간도 측정했습니다. 미리 채워진 난수 배열을 사용하도록 변경하여 단일 피연산자 테스트 기간이 크게 변동했습니다. 여러 실행 간의 차이는 최대 6-7ms였습니다.


이것이 나에게 설명 된 방식은 시리즈의 첫 번째 검사가 거짓이면 &&가 거짓을 반환하는 반면 &는 거짓인지에 관계없이 시리즈의 모든 항목을 확인한다는 것입니다. IE

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

보다 빠르게 실행됩니다

만약 (x> 0 & x <= 10 & x

x가 10보다 크면 단일 앰퍼샌드가 나머지 조건을 계속 확인하는 반면 이중 앰퍼샌드는 첫 번째 비참 조건 이후에 중단되기 때문입니다.

참고 URL : https://stackoverflow.com/questions/39588251/in-java-can-be-faster-than

반응형