논리 AND 연산자 ( &&
)는 단락 평가를 사용합니다. 즉, 두 번째 테스트는 첫 번째 비교가 true로 평가되는 경우에만 수행됩니다. 이것은 종종 당신이 요구하는 의미론입니다. 예를 들어 다음 코드를 고려하십시오.
if ((p != nullptr) && (p->first > 0))
역 참조하기 전에 포인터가 널이 아닌지 확인해야합니다. 이것이 단락 평가 가 아닌 경우 널 포인터를 역 참조하기 때문에 동작이 정의되지 않은 것입니다.
조건 평가가 비싼 프로세스 인 경우 단락 평가로 성능이 향상 될 수도 있습니다. 예를 들면 다음과 같습니다.
if ((DoLengthyCheck1(p) && (DoLengthyCheck2(p))
경우 DoLengthyCheck1
에 실패, 전화 아무 문제가 없다 DoLengthyCheck2
.
그러나 결과 바이너리에서는 단락 연산으로 인해 종종 두 가지 분기가 발생하는데, 이는 컴파일러가 이러한 의미를 보존하는 가장 쉬운 방법이기 때문입니다. (어느 왜 동전의 반대편에, 단락 회로 평가 때로는 수 있습니다 금지 . 최적화 가능성이) 당신은 당신을 위해 생성 된 오브젝트 코드의 관련 부분을 보면이 볼 수 if
GCC 5.4에 의해 문 :
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13w, 478 ; (curr[i] < 479)
ja .L5
cmp ax, 478 ; (l[i + shift] < 479)
ja .L5
add r8d, 1 ; nontopOverlap++
여기에 두 가지 비교 ( cmp
명령)가 있으며 각각 별도의 조건부 점프 / 분기 ( ja
또는 위의 경우 점프)가 이어집니다.
일반적으로 분기가 느리기 때문에 빡빡한 고리에서는 피해야합니다. 이것은 거의 모든 x86 프로세서에서 겸손한 8088 (매우 느린 페치 시간 및 매우 작은 프리 페치 큐 (명령 캐시와 비교할 수 있음), 분기 예측이 완전히 결여되어 분기에서 캐시를 덤프해야 함을 의미 함) )를 현대적인 구현 (긴 파이프 라인으로 잘못 예측 한 지점을 비슷하게 비싸게 만드는)에 적용합니다. 내가 미끄러운 작은 경고에 주목하십시오. Pentium Pro 이후의 최신 프로세서에는 지점 비용을 최소화하도록 설계된 고급 지점 예측 엔진이 있습니다. 지점의 방향을 올바르게 예측할 수 있으면 비용이 최소화됩니다. 대부분의 경우, 이것은 잘 작동하지만 분기 예측 변수가 귀하의 편이 아닌 병리학 적 사례에 들어가면,코드가 매우 느려질 수 있습니다 . 배열이 정렬되지 않았다고 말했기 때문에 아마도 여기에있을 것입니다.
당신은 벤치 마크는 대체 있음을 확인 말 &&
로모그래퍼 것은 *
눈에 띄게 더 빠른 코드를 만든다. 그 이유는 객체 코드의 관련 부분을 비교할 때 분명합니다.
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
xor r15d, r15d ; (curr[i] < 479)
cmp r13w, 478
setbe r15b
xor r14d, r14d ; (l[i + shift] < 479)
cmp ax, 478
setbe r14b
imul r14d, r15d ; meld results of the two comparisons
cmp r14d, 1 ; nontopOverlap++
sbb r8d, -1
여기에 더 많은 지침 이 있기 때문에 이것이 더 빠를 수는 약간 반 직관적 이지만 최적화가 때때로 작동하는 방식입니다. cmp
여기 에서 동일한 비교 ( )가 수행되는 것을 볼 수 있지만 이제 각각 앞에 xor
a와 뒤에을 붙 setbe
입니다. XOR은 레지스터를 지우는 표준 방법입니다. 이 setbe
플래그의 값에 기초하여 비트를 설정하고, 종종 지점없는 코드를 구현하기 위해 사용되는 x86 명령어이다. 여기에 setbe
의 반대가 ja
있습니다. 비교가 이하이거나 같으면 (레지스터가 사전 영 점화되었으므로 0이 됨) 대상 레지스터를 1로 설정하고 ja
비교가 위의 경우 분기됩니다. 이 두 값이 얻어되고 나면 r15b
및r14b
레지스터는을 사용하여 함께 곱해집니다 imul
. 곱셈은 전통적으로 비교적 느린 연산이지만 현대 프로세서에서는 빠르며 특히 2 바이트 크기의 값만 곱하기 때문에 빠릅니다.
곱셈을 비트 단위 AND 연산자 ( &
) 로 쉽게 대체 할 수 있었으며 단락 평가를 수행하지 않습니다. 이것은 코드를 훨씬 명확하게하며 컴파일러가 일반적으로 인식하는 패턴입니다. 그러나 코드 로이 작업을 수행하고 GCC 5.4로 컴파일하면 첫 번째 분기가 계속 발생합니다.
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13w, 478 ; (curr[i] < 479)
ja .L4
cmp ax, 478 ; (l[i + shift] < 479)
setbe r14b
cmp r14d, 1 ; nontopOverlap++
sbb r8d, -1
이런 식으로 코드를 생성해야하는 기술적 이유는 없지만 어떤 이유로 든 내부 휴리스틱이이를 통해 더 빠르다고 말합니다. 그것은 것입니다 분기 예측이 옆에 있었다면 아마 더 빠를 수 있지만, 분기 예측이 더 자주 성공보다 실패 할 경우 가능성이 느려집니다.
최신 세대의 컴파일러 (및 Clang과 같은 다른 컴파일러)는이 규칙을 알고 있으며이를 수동 최적화로 찾은 것과 동일한 코드를 생성하는 데 사용합니다. 나는 Clang이 &&
표현식을 사용했을 때 방출되었을 것과 동일한 코드로 변환 하는 것을 정기적으로 본다 &
. 다음은 일반 &&
연산자를 사용하여 코드와 함께 GCC 6.2의 관련 출력입니다 .
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13d, 478 ; (curr[i] < 479)
jg .L7
xor r14d, r14d ; (l[i + shift] < 479)
cmp eax, 478
setle r14b
add esi, r14d ; nontopOverlap++
이것이 얼마나 영리한지 주목하십시오 ! 그것은 (서명 조건을 사용 jg
하고 setle
) 부호없는 조건 (반대 ja
하고 setbe
), 그러나 이것은 중요하지 않습니다. 여전히 이전 버전과 같은 첫 번째 조건에 대해 비교 및 분기를 수행하고 동일한 setCC
명령을 사용 하여 두 번째 조건에 대한 분기 없는 코드를 생성하지만 증가를 수행하는 방법에 훨씬 효율적입니다. . sbb
연산에 대한 플래그를 설정하기 위해 두 번째 중복 비교를 수행하는 대신 r14d
이 값을 무조건 추가하기 위해 1 또는 0 의 지식을 사용합니다 nontopOverlap
. 경우 r14d
0, 다음 추가는 어떤 조합입니다; 그렇지 않으면 정확히 1과 동일합니다.
GCC 6.2는 실제로 비트 연산자 보다 단락 연산자 를 사용할 때 더 효율적인 코드를 생성합니다 .&&
&
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13d, 478 ; (curr[i] < 479)
jg .L6
cmp eax, 478 ; (l[i + shift] < 479)
setle r14b
cmp r14b, 1 ; nontopOverlap++
sbb esi, -1
분기와 조건부 집합은 여전히 존재하지만 이제는 덜 영리한 방식으로 되돌아갑니다 nontopOverlap
. 이것은 컴파일러를 능숙하게 만들려고 할 때주의해야 할 중요한 교훈입니다!
그러나 벤치마킹으로 분기 코드가 실제로 느리다는 것을 증명할 수 있다면 컴파일러를 시도하고 능가하는 데 비용이들 수 있습니다. 디스 어셈블리를주의 깊게 검사해야하며 이후 버전의 컴파일러로 업그레이드 할 때 결정을 다시 평가할 준비가되어 있어야합니다. 예를 들어, 작성한 코드는 다음과 같이 다시 작성할 수 있습니다.
nontopOverlap += ((curr[i] < 479) & (l[i + shift] < 479));
여기에는 if
진술 이 전혀 없으며 대다수의 컴파일러는이를 위해 분기 코드를 생성하는 것에 대해 결코 생각하지 않을 것입니다. GCC도 예외는 아닙니다. 모든 버전은 다음과 유사한 것을 생성합니다.
movzx r14d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r14d, 478 ; (curr[i] < 479)
setle r15b
xor r13d, r13d ; (l[i + shift] < 479)
cmp eax, 478
setle r13b
and r13d, r15d ; meld results of the two comparisons
add esi, r13d ; nontopOverlap++
이전 예제를 따라 본 적이 있다면 이것은 매우 친숙 할 것입니다. 두 비교는 모두 분기없는 방식으로 수행되고 중간 결과는 and
함께 계산 된 다음이 결과 (0 또는 1 임)는로 계산 add
됩니다 nontopOverlap
. 분기없는 코드를 원한다면 실제로 얻을 수 있습니다.
GCC 7은 더욱 똑똑해졌습니다. 위 코드는 원래 코드와 거의 동일한 코드를 생성합니다 (일부 명령어 재 배열 제외). 그래서 당신의 질문에 대한 대답은 "왜 컴파일러는 이런 식으로 행동합니까?" 아마도 완벽하지 않기 때문일 것입니다. 휴리스틱을 사용하여 가능한 가장 최적의 코드를 생성하려고하지만 항상 최선의 결정을 내리지는 않습니다. 그러나 적어도 그들은 시간이 지남에 따라 더 똑똑해질 수 있습니다!
이 상황을 보는 한 가지 방법은 분기 코드의 성능이 가장 우수 하다는 것 입니다. 분기 예측이 성공하면 불필요한 작업을 건너 뛰면 실행 시간이 약간 빨라집니다. 그러나 분기없는 코드는 최악의 경우 성능 이 더 좋습니다 . 분기 예측이 실패하면 분기를 피하기 위해 필요에 따라 몇 가지 추가 명령을 실행하는 것이 잘못 예측 된 분기보다 확실히 빠릅니다. 가장 똑똑하고 똑똑한 컴파일러 조차도이 선택을하기가 어려울 것입니다.
그리고 이것이 프로그래머가 조심해야 할 것인지에 대한 질문에 대해서는 마이크로 최적화를 통해 속도를 높이려는 특정 핫 루프를 제외하고는 대답이 거의 없습니다. 그런 다음 분해와 함께 앉아서 조정할 방법을 찾으십시오. 이전에 말했듯이 최신 버전의 컴파일러로 업데이트하면 까다로운 코드로 어리석은 짓을하거나 다시 돌아갈 수 있도록 최적화 휴리스틱을 변경했을 수 있으므로 이러한 결정을 다시 확인할 준비를하십시오. 원래 코드를 사용합니다. 철저하게 의견을 말하십시오!