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

어느 시점에서 프로그램의 흐름이 두 개의 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, 혼합 모드)



답변

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

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

후자는 생각합니다.

  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.


답변

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

  • (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에 의해 생성 된 기계어 코드를 분해 할 수 있다면 , 그것이 무엇을하는지 추측하지 말고 그렇게하십시오!


답변

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

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

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 블록을 간단한 부울로 줄임으로써, 컴파일러는 불필요하게 두 번째 부울을 평가할 때 부작용이 없다는 것을 깨닫기에 충분히 영리 할 수 ​​있습니다.


답변

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

    (a*b != 0)

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

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

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


답변

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

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


답변