Java에서 (a * b! = 0)이 (a! = 0 && b! = 0)보다 빠른 이유는 무엇입니까?


412

어느 시점에서 프로그램의 흐름이 두 개의 int 변수 "a"와 "b"가 0이 아닌지 여부에 의해 결정되는 Java로 코드를 작성하고 있습니다 (참고 : 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"가 ~ 3 % 이상의 0과 같을 것으로 예상하면 다음보다 a*b != 0빠릅니다 a!=0 && b!=0.

0이 아닌 AND b의 결과 그래픽 그래프

왜 그런지 궁금합니다. 누구나 빛을 비출 수 있습니까? 컴파일러입니까, 아니면 하드웨어 수준입니까?

편집 : 호기심에서 ... 이제 분기 예측에 대해 배웠으므로 OR b에 대한 아날로그 비교가 0 이 아닌지 궁금 합니다.

0이 아닌 a 또는 b의 그래프

우리는 예상대로 분기 예측의 동일한 효과를 보았습니다. 흥미롭게도 그래프는 X 축을 따라 다소 뒤집 힙니다.

최신 정보

1- 나는 !(a==0 || b==0)어떤 일이 일어나는지보기 위해 분석에 추가 했다.

2 나는 또한 포함 a != 0 || b != 0, (a+b) != 0그리고 (a|b) != 0호기심, 분기 예측에 대한 학습 후. 그러나 OR b 만 true를 리턴하기 위해 0이 아니어도되므로 처리 효율성과 비교할 수 없기 때문에 다른 표현식과 논리적으로 동일 하지 않습니다.

3- 또한 분석에 사용한 실제 벤치 마크를 추가했는데, 이는 임의의 int 변수를 반복하는 것입니다.

4- 일부 사람들은에 대한 a != 0 & b != 0반대 의견을 제안 했지만 분기 예측 효과를 제거 a != 0 && b != 0하기 a*b != 0때문에 더 밀접하게 작동 할 것이라는 예측이있었습니다 . &부울 변수와 함께 사용할 수 있다는 것을 몰랐 습니다. 정수를 사용하는 이진 연산에만 사용되었다고 생각했습니다.

참고 :이 모든 것을 고려하고있는 맥락에서 int overflow는 문제가되지 않지만 일반적인 상황에서는 확실히 중요한 고려 사항입니다.

CPU : 2.3GHz에서 Intel Core i7-3610QM

Java 버전 : 1.8.0_45
Java (TM) SE 런타임 환경 (빌드 1.8.0_45-b14)
Java HotSpot ™ 64 비트 서버 VM (빌드 25.45-b02, 혼합 모드)


11
무엇에 대해 if (!(a == 0 || b == 0))? Microbenchmarks는 믿을 수 없을 정도로 악명 높으며, 이것은 실제로 측정 할 수 없을 것입니다 (~ 3 %는 나에게 오차 한계처럼 들립니다).
Elliott Frisch 1

9
또는 a != 0 & b != 0.
Louis Wasserman

16
예측 분기가 잘못되면 분기가 느려집니다. a*b!=0한 적은 지점이
어윈 Bolwidt

19
(1<<16) * (1<<16) == 0그러나 둘 다 0과 다릅니다.
코드 InChaos

13
@Gene : 제안 된 최적화가 유효하지 않습니다. 심지어 오버 플로우 무시 a*b하면 제로 하나a과가 b제로; a|b둘 다인 경우에만 0입니다.
hmakholm

답변:


240

벤치마킹에 결함이 있을 수있는 문제를 무시하고 결과를 액면가로 취합니다.

컴파일러입니까, 아니면 하드웨어 수준입니까?

후자는 생각합니다.

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

2 개의 메모리로드와 2 개의 조건부 분기로 컴파일됩니다.

  if (a * b != 0)

2 개의 메모리로드, 곱하기 및 하나의 조건부 분기로 컴파일합니다.

하드웨어 수준 분기 예측이 효과적이지 않으면 곱하기가 두 번째 조건부 분기보다 빠를 가능성이 높습니다. 비율을 높이면 분기 예측이 덜 효과적이됩니다.

조건부 분기가 느려지는 이유는 명령 실행 파이프 라인이 정지하기 때문입니다. 브랜치 예측은 브랜치가 어떤 방식으로 진행 될지 예측하고 추론에 따라 다음 명령을 선택하여 실속을 피하는 것입니다. 예측이 실패하면 다른 방향에 대한 명령이로드되는 동안 지연이 발생합니다.

(참고 : 위의 설명은 지나치게 단순화되었습니다.보다 정확한 설명을 위해서는 어셈블리 언어 코더 및 컴파일러 작성자를 위해 CPU 제조업체에서 제공 한 문헌을 참조해야합니다. 분기 예측기 의 Wikipedia 페이지 는 배경 지식이 좋습니다.)


그러나이 최적화와 관련하여주의해야 할 사항이 있습니다. a * b != 0틀린 답을 줄 수 있는 가치가 있습니까? 제품을 계산할 때 정수 오버플로가 발생하는 경우를 고려하십시오.


최신 정보

당신의 그래프는 내가 말한 것을 확인하는 경향이 있습니다.

  • 조건부 분기 a * b != 0경우 에도 "분기 예측"효과가 있으며 이는 그래프에서 나타납니다.

  • X 축에서 0.9를 초과하여 곡선을 투영하면 1) 약 1.0에서 만나고 2) 미팅 포인트는 X = 0.0과 거의 같은 Y 값이됩니다.


업데이트 2

곡선이 다릅니다 왜 이해가 안 a + b != 0a | b != 0사례. 분기 예측기 논리에 영리한 것이 있을 수 있습니다 . 또는 다른 것을 나타낼 수 있습니다.

(이 종류는 특정 칩 모델 번호 또는 버전에 따라 달라질 수 있습니다. 벤치 마크 결과는 다른 시스템에서 다를 수 있습니다.)

그러나, 그들은 모두 모든 음이 아닌 값에 대한 작업의 장점이 ab.


1
@DebosmitRay-1) SW가 없어야합니다. 중간 결과는 레지스터에 보관됩니다. 2) 두 번째 경우에는 사용 가능한 두 가지가 있습니다. 하나는 "일부 코드"를 실행하고 다른 하나는 다음의 다음 문으로 건너 뜁니다 if.
Stephen C

1
@StephenC는 a + b와 a | b에 대해 혼란 스러울 수 있습니다. 곡선 동일 하기 때문에 색상이 실제로 가깝다고 생각합니다. 시각 장애인을위한 사과드립니다!
Maljam

3
확률의 관점에서 @ njzk2는 50 %에서 축에 따라 대칭이어야합니다 (0의 확률 a&ba|b). 그것들은 퍼즐이지만 완벽하지는 않습니다.
Antonín Lejsek

3
@StephenC 이유 a*b != 0a+b != 0벤치 마크가 다른 이유 a+b != 0는 전혀 동일하지 않으며 벤치마킹되어서 는 안되기 때문 입니다. 예를 a = 1, b = 0들어을 사용하면 첫 번째 표현식은 false로 평가되고 두 ​​번째 표현식은 true로 평가됩니다. 곱하기는 일종의 and 연산자 처럼 작동하는 반면 add는 일종의 or 연산자 처럼 작동합니다.
JS1

2
@ AntonínLejsek 나는 확률이 다를 것이라고 생각합니다. 당신이있는 경우 n다음 두 가능성 0을 a하고 b제로 증가되고 n. 에서 AND동작 이상으로 n그들 중 하나는 확률 비제 인 증가 및 조건을 충족한다. 이것은 OR연산 과 반대입니다 (둘 중 하나 가 0 일 가능성은 n). 이것은 수학적 관점에 근거합니다. 그것이 하드웨어가 작동하는 방식인지 확실하지 않습니다.
WYSIWYG

70

귀하의 벤치 마크에는 약간의 결함이 있으며 실제 프로그램에 대해 유추하는 데 유용하지 않을 수 있습니다. 내 생각은 다음과 같습니다.

  • (a|b)!=0(a+b)!=0시험 경우 어느 반면 값은 비 제로 a != 0 && b != 0(a*b)!=0하는지 테스트 모두 비 - 제로이다. 따라서 당신은 단지 산술의 타이밍을 비교하지 않습니다. 조건이 더 자주 참이면 더 많은 if본문이 실행되어 더 많은 시간이 걸립니다.

  • (a+b)!=0 양수와 음수 값이 0으로 잘못되면 잘못된 일을하므로 일반적인 경우에는 사용할 수 없습니다.

  • 마찬가지로 (a*b)!=0오버플로되는 값에 대해 잘못된 작업을 수행합니다. (임의의 예 : 196608 * 327680은 0이므로 실제 결과는 2 32 로 나눌 수 있으므로 낮은 32 비트는 0이며, 해당 비트가 int연산 이면 모든 비트를 얻게됩니다 .)

  • VM은 분기가 거의 수행되지 않을 때 0 일 때 외부 ( fraction) 루프 의 처음 몇 번의 실행 동안 식을 최적화합니다 fraction. fraction0.5에서 시작하면 최적화 프로그램이 다른 작업을 수행 할 수 있습니다 .

  • VM이 여기에서 배열 범위 검사 중 일부를 제거 할 수 없다면, 범위 검사로 인해 식에 4 개의 다른 분기가 있으며, 이는 저수준에서 발생하는 상황을 파악하려고 할 때 복잡한 요소입니다. 2 차원 배열을 2 개의 평평한 배열로 분할하고 nums[0][i]and nums[1][i]nums0[i]and로 변경하면 결과가 달라질 수 있습니다 nums1[i].

  • CPU 분기 예측 변수는 데이터에서 짧은 패턴 또는 수행중인 모든 분기의 실행을 감지합니다. 임의로 생성 된 벤치 마크 데이터는 분기 예측기최악의 시나리오입니다 . 실제 데이터가 예측 가능한 패턴을 갖거나 0이 아닌 0이 아닌 값을 길게 실행하면 분기 비용이 훨씬 낮아질 수 있습니다.

  • 조건이 충족 된 후 실행되는 특정 코드는 조건을 평가하는 성능에 영향을 줄 수 있습니다. 이는 루프를 언 롤링 할 수 있는지 여부, 사용 가능한 CPU 레지스터 및 가져온 nums값 중 하나가 필요한 경우에 영향을주기 때문입니다. 조건을 평가 한 후 재사용하십시오. 벤치 마크에서 카운터를 증가시키는 것은 실제 코드가 수행하는 작업에 대한 완벽한 자리 표시자가 아닙니다.

  • System.currentTimeMillis()대부분의 시스템에서 +/- 10ms보다 정확하지 않습니다. System.nanoTime()일반적으로 더 정확합니다.

많은 불확실성이 있으며 한 VM 또는 CPU에서 더 빠른 트릭이 다른 VM에서 더 느릴 수 있기 때문에 이러한 종류의 마이크로 최적화로 명확한 것을 말하기는 항상 어렵습니다. 64 비트 버전이 아닌 32 비트 HotSpot JVM을 실행하는 경우 "서버"VM과 "클라이언트"VM의 최적화가 다른 "클라이언트"VM의 두 가지 특징이 있습니다.

VM에 의해 생성 된 기계어 코드를 분해 할 수 있다면 , 그것이 무엇을하는지 추측하지 말고 그렇게하십시오!


24

여기에 대한 답변은 훌륭하지만 개선 할 수있는 아이디어가있었습니다.

두 개의 브랜치 및 관련 브랜치 예측이 범인 일 수 있으므로 로직을 전혀 변경하지 않고도 브랜칭을 단일 브랜치로 줄일 수 있습니다.

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 */ }

이유는 단락 규칙에 따라 첫 번째 부울이 거짓이면 두 번째 부울이 평가되지 않아야합니다. 거짓 nums[1][i]인지 평가하지 않으려면 추가 분기를 수행해야합니다 nums[0][i]. 이제는 nums[1][i]평가를 신경 쓰지 않아도 되지만 컴파일러는 범위를 벗어나거나 null 참조를 throw하지 않을 것이라고 확신 할 수 없습니다. if 블록을 간단한 부울로 줄임으로써, 컴파일러는 불필요하게 두 번째 부울을 평가할 때 부작용이 없다는 것을 깨닫기에 충분히 영리 할 수 ​​있습니다.


3
나는 느낌이 있지만 Upvoted이되지 않습니다 질문에 대답.
Pierre Arlaud

3
그것은 (당신이 얻은 방법 경우 비 분기에서 논리를 변경하지 않고 분기를 소개하는 방법 ab부작용을 가지고 당신이 그들을 유지하는 것). 당신은 여전히 ​​가지고 &&있으므로 여전히 지점이 있습니다.
Jon Hanna

11

곱셈을하면 하나의 숫자가 0이더라도 곱은 0입니다.

    (a*b != 0)

제품의 결과를 평가하여 0부터 시작하여 반복의 처음 몇 번의 발생을 제거합니다. 결과적으로 조건이

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

모든 요소가 0과 비교되고 평가되는 곳. 따라서 필요한 시간이 줄어 듭니다. 그러나 나는 두 번째 조건이 더 정확한 해결책을 줄 수 있다고 생각합니다.


4
두 번째 표현식에서 a0이면 b전체 표현식이 이미 거짓이므로 평가할 필요가 없습니다. 따라서 모든 요소가 비교되는 것은 아닙니다.
Kuba Wyrostek

9

분기를 예측할 수없는 무작위 입력 데이터를 사용하고 있습니다. 실제로 분기는 종종 (~ 90 %) 예측 가능하므로 실제 코드에서는 분기 코드가 더 빠를 수 있습니다.

그렇습니다. a*b != 0보다 빠를 수있는 방법을 모르겠습니다 (a|b) != 0. 일반적으로 정수 곱셈은 비트 단위 OR보다 비쌉니다. 그러나 이런 것들이 때때로 이상해집니다. 예를 들어, 프로세서 캐시 효과 갤러리의 "예 7 : 하드웨어 복잡성"예를 참조하십시오 .


2
&(이 경우) "비트 OR"하지만없는 "논리적 AND"두 피연산자는 논리 값이며이 아니기 때문에 |;-)
siegi

1
@siegi TIL Java '&'는 실제로 단락이없는 논리적 AND입니다.
StackedCrooked 16:24에
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.