Java에서 &&보다 빠를 수 있습니까?
이 코드에서 :
if (value >= x && value <= y) {
때 value >= x
와 value <= 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
( &&
) 메소드는 생성 이 개 예상대로 조건부 점프 :
- 스택에 로드
value
되고 더 낮 으면x
L1로 점프합니다value
. 그렇지 않으면 다음 줄을 계속 실행합니다. - 스택에 로드
value
되고 더 큰y
경우 L1로도 점프합니다value
. 그렇지 않으면 다음 줄을 계속 실행합니다. return true
두 점프 중 어느 것도 수행되지 않은 경우에 발생합니다 .- 그리고 L1로 표시된 라인이 있습니다
return false
.
그러나 AndNonSC
( &
) 메서드는 세 가지 조건부 점프를 생성 합니다!
- 스택에 로드
value
되고 더 낮 으면x
L1로 점프합니다value
. 이제 AND의 다른 부분과 비교하기 위해 결과를 저장해야하므로 "savetrue
"또는 "savefalse
" 를 실행 해야하므로 동일한 명령으로 둘 다 수행 할 수 없습니다. - 스택에 로드
value
되고 더 큰y
경우 L1로 점프합니다value
. 다시 한 번 저장해야true
하거나false
비교 결과에 따라 두 줄이 다릅니다. - 이제 두 비교가 모두 완료되었으므로 코드는 실제로 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 개의 어셈블리 점프 명령으로 변환 되는 메서드의 경우 더 길다 .
AndSC
IF_ICMP*
- 한편,
AndNonSC
메서드의 경우 컴파일러는보다 간단한 코드를 생성합니다. 여기서 각 바이트 코드IF_ICMP*
는 3 개의 조건부 점프의 원래 개수를 유지하면서 하나의 어셈블리 점프 명령으로 만 변환됩니다.
- ASM 코드는 실제로 총 4 개의 조건부 점프에 대해 모든 바이트 코드 가 2 개의 어셈블리 점프 명령으로 변환 되는 메서드의 경우 더 길다 .
- Intel 구문 사용 :
- 에 대한 ASM 코드
AndSC
는 더 짧아서 조건부 점프가 2 번뿐입니다 (jmp
끝에 비 조건부 를 계산하지 않음 ). 실제로 결과에 따라 CMP 2 개, JL / E 2 개 및 XOR / MOV 1 개입니다. - 의 ASM 코드
AndNonSC
가 이제 코드 보다 길어졌습니다AndSC
! 그러나 첫 번째 비교를위한 조건부 점프는 1 개뿐입니다. 레지스터를 사용하여 더 이상 점프없이 첫 번째 결과를 두 번째 결과와 직접 비교합니다.
- 에 대한 ASM 코드
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
'Development Tip' 카테고리의 다른 글
sed의 '홀드 스페이스'와 '패턴 스페이스'의 개념 (0) | 2020.11.01 |
---|---|
Objective C 클래스에서 Swift 함수 호출 (0) | 2020.11.01 |
개체가 설정되어 있는지 VBA 확인 (0) | 2020.11.01 |
IP 주소로 사용자 위치 가져 오기 (0) | 2020.11.01 |
이벤트 로그에서 앱 풀 재활용을 찾는 방법 (0) | 2020.11.01 |