나는 #define
그들이 사용하는 것을 발견했다 __builtin_expect
.
설명서 는 다음과 같이 말합니다.
내장 기능 :
long __builtin_expect (long exp, long c)
__builtin_expect
분기 예측 정보를 컴파일러에 제공하는 데 사용할 수 있습니다 . 일반적-fprofile-arcs
으로 프로그래머가 프로그램의 실제 성능을 예측하는 데 악명이 높기 때문에 실제 프로파일 피드백을 사용하는 것이 좋습니다 ( ). 그러나이 데이터를 수집하기 어려운 응용 프로그램이 있습니다.반환 값은의 값이며
exp
, 정수식이어야합니다. 내장의 의미는 다음과 같습니다
exp == c
. 예를 들면 다음과 같습니다.if (__builtin_expect (x, 0)) foo ();
우리는 0이 될
foo
것으로 기대x
하기 때문에 호출하지 않을 것임을 나타냅니다 .
직접 사용하지 않는 이유는 무엇입니까?
if (x)
foo ();
__builtin_expect
? 의 복잡한 구문 대신
답변
다음에서 생성되는 어셈블리 코드를 상상해보십시오.
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
나는 그것이 다음과 같아야한다고 생각합니다 :
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
명령어가 bar
케이스 앞에 있는 순서대로 정렬되어 있음을 알 수 있습니다 foo
(C 코드와 반대). 점프는 이미 가져온 명령어를 스 래시하므로 CPU 파이프 라인을 더 잘 활용할 수 있습니다.
점프가 실행되기 전에 그 아래의 명령어 ( bar
케이스)가 파이프 라인으로 푸시됩니다. 이 foo
경우는 거의 없기 때문에 점프도 거의 불가능하므로 파이프 라인을 막을 가능성은 거의 없습니다.
답변
GCC 4.8이 수행하는 작업을 확인하기 위해 디 컴파일하자
Blagovest는 파이프 라인을 개선하기 위해 분기 반전을 언급했지만 현재 컴파일러는 실제로 그렇게합니까? 알아 보자!
없이 __builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Use time to prevent it from being optimized away. */
int i = !time(NULL);
if (i)
puts("a");
return 0;
}
GCC 4.8.2 x86_64 Linux로 컴파일 및 디 컴파일 :
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
산출:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 75 0a jne 1a <main+0x1a>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1
15: e8 00 00 00 00 callq 1a <main+0x1a>
16: R_X86_64_PC32 puts-0x4
1a: 31 c0 xor %eax,%eax
1c: 48 83 c4 08 add $0x8,%rsp
20: c3 retq
메모리 명령 순서는 불변 : 먼저 puts
후 retq
리턴한다.
와 __builtin_expect
이제 다음으로 바꾸십시오 if (i)
:
if (__builtin_expect(i, 0))
그리고 우리는 :
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 74 07 je 17 <main+0x17>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
17: bf 00 00 00 00 mov $0x0,%edi
18: R_X86_64_32 .rodata.str1.1
1c: e8 00 00 00 00 callq 21 <main+0x21>
1d: R_X86_64_PC32 puts-0x4
21: eb ed jmp 10 <main+0x10>
는 puts
기능,의 맨 끝으로 이동 한 retq
수익!
새 코드는 기본적으로 다음과 같습니다.
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
이 최적화는로 수행되지 않았습니다 -O0
.
그러나 CPU가__builtin_expect
없는 것보다 더 빠르게 실행되는 예제를 작성하면 행운이 있습니다. CPU는 그 당시에는 정말 똑똑했습니다 . 나의 순진한 시도 가 여기에 있습니다 .
C ++ 20 [[likely]]
및[[unlikely]]
C ++ 20은 C ++ 내장 기능을 표준화 했습니다. if-else 문에서 C ++ 20의 가능성 / 불확실한 속성을 사용하는 방법 동일한 기능을 수행 할 것입니다.
답변
아이디어는 __builtin_expect
컴파일러에게 일반적으로 표현식이 c로 평가되어 컴파일러가 해당 경우에 맞게 최적화 될 수 있음을 알게된다는 것입니다.
나는 누군가가 그들이 영리하다고 생각하고 이것을함으로써 일을 가속화하고 있다고 생각합니다.
불행히도, 상황이 잘 이해 되지 않으면 (그들이 그런 일을하지 않았을 가능성이 높다), 상황을 악화시킬 수 있습니다. 문서에는 다음과 같이 말합니다.
일반적
-fprofile-arcs
으로 프로그래머가 프로그램의 실제 성능을 예측하는 데 악명이 높기 때문에 실제 프로파일 피드백을 사용하는 것이 좋습니다 ( ). 그러나이 데이터를 수집하기 어려운 응용 프로그램이 있습니다.
일반적으로 다음과 같은 경우를 __builtin_expect
제외하고는 사용 하지 않아야합니다.
- 실제 성능 문제가 있습니다
- 이미 시스템의 알고리즘을 적절하게 최적화했습니다
- 특정 사례가 가장 가능성이 높다는 주장을 백업 할 성능 데이터가 있습니다.
답변
설명에서 알 수 있듯이 첫 번째 버전은 예측 요소를 구성에 추가하여 x == 0
분기가 더 가능성이 높다는 것을 컴파일러에게 알려줍니다. 즉, 분기가 프로그램에서 더 자주 사용하게됩니다.
이를 염두에두고, 컴파일러는 예상치 못한 조건이 발생할 경우 더 많은 작업을 수행해야하는 대가로 예상 조건이 유지 될 때 최소한의 작업 만 요구하도록 조건부를 최적화 할 수 있습니다.
컴파일 단계 및 결과 어셈블리에서 조건이 구현되는 방식을 살펴보고 한 분기가 다른 분기보다 덜 작동하는 방법을 살펴보십시오.
그러나 결과 코드의 차이가 상대적으로 작기 때문에 문제의 조건이 많은 내부 루프의 일부인 경우에만이 최적화가 눈에 띄는 효과를 기대합니다 . 잘못된 방향으로 최적화하면 성능이 저하 될 수 있습니다.
답변
나는 당신이 묻고 있다고 생각하는 질문에 대한 답을 보지 못했습니다.
컴파일러에 분기 예측을 암시하는보다 이식 가능한 방법이 있습니까?
귀하의 질문 제목에 따라 다음과 같이 생각했습니다.
if ( !x ) {} else foo();
컴파일러에서 ‘true’가 더 가능성이 높다고 가정하면를 호출하지 않도록 최적화 할 수 foo()
있습니다.
여기서 문제는 일반적으로 컴파일러가 무엇을 가정할지 알지 못한다는 것입니다. 따라서 이러한 종류의 기술을 사용하는 모든 코드는 신중하게 측정해야합니다 (컨텍스트가 변경되면 시간이 지남에 따라 모니터링 될 수 있음).
답변
@Blagovest Buyukliev 및 @Ciro에 따라 Mac에서 테스트했습니다. 조립이 명확 해 보이고 의견을 추가합니다.
명령은
gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o
-O3을 사용하면 __builtin_expect (i, 0)의 존재 여부에 관계없이 동일하게 보입니다.
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp // open function stack
0000000000000004 xorl %edi, %edi // set time args 0 (NULL)
0000000000000006 callq _time // call time(NULL)
000000000000000b testq %rax, %rax // check time(NULL) result
000000000000000e je 0x14 // jump 0x14 if testq result = 0, namely jump to puts
0000000000000010 xorl %eax, %eax // return 0 , return appear first
0000000000000012 popq %rbp // return 0
0000000000000013 retq // return 0
0000000000000014 leaq 0x9(%rip), %rdi ## literal pool for: "a" // puts part, afterwards
000000000000001b callq _puts
0000000000000020 xorl %eax, %eax
0000000000000022 popq %rbp
0000000000000023 retq
-O2로 컴파일하면 __builtin_expect (i, 0)의 유무에 따라 다르게 보입니다.
없이
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e jne 0x1c // jump to 0x1c if not zero, then return
0000000000000010 leaq 0x9(%rip), %rdi ## literal pool for: "a" // put part appear first , following jne 0x1c
0000000000000017 callq _puts
000000000000001c xorl %eax, %eax // return part appear afterwards
000000000000001e popq %rbp
000000000000001f retq
이제 __builtin_expect (i, 0)
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e je 0x14 // jump to 0x14 if zero then put. otherwise return
0000000000000010 xorl %eax, %eax // return appear first
0000000000000012 popq %rbp
0000000000000013 retq
0000000000000014 leaq 0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b callq _puts
0000000000000020 jmp 0x10
요약하면 __builtin_expect는 마지막 경우에 작동합니다.