else 문에서 GCC의 __builtin_expect의 장점은 무엇입니까?


144

나는 #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? 의 복잡한 구문 대신



3
직접 코드가 if ( x == 0) {} else foo();.. if ( x != 0 ) foo();GCC 문서의 코드와 동일 해야 한다고 생각합니다 .
Nawaz

답변:


187

다음에서 생성되는 어셈블리 코드를 상상해보십시오.

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경우는 거의 없기 때문에 점프도 거의 불가능하므로 파이프 라인을 막을 가능성은 거의 없습니다.


1
정말 그렇게 작동합니까? 왜 foo 정의가 먼저 올 수 없습니까? 함수 정의의 순서는 프로토 타입이있는 한 관련이 없습니다.
kingsmasher1

63
이것은 함수 정의에 관한 것이 아닙니다. CPU가 실행되지 않을 명령을 가져올 가능성이 더 적은 방식으로 머신 코드를 재배 열하는 것입니다.
Blagovest Buyukliev

4
오 이해합니다. 따라서 확률이 높 x = 0으므로 막대가 먼저 주어집니다. 그리고 foo는 (확률이 아닌) 확률이 적기 때문에 나중에 정의됩니다.
kingsmasher1

1
고마워 그게 가장 좋은 설명입니다. 어셈블리 코드는 실제로 트릭을 만들었습니다 :)
kingsmasher1

5
이것은 또한 CPU 브랜치 예측자를 위한 힌트를 포함시켜 파이프 라이닝을 향상시킵니다
Hasturkun

50

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

메모리 명령 순서는 불변 : 먼저 putsretq리턴한다.

__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의 가능성 / 불확실한 속성을 사용하는 방법 동일한 기능을 수행 할 것입니다.


1
실제 최적화를 위해 __builtin_expect를 사용하는 libdispatch의 dispatch_once 함수를 확인하십시오. 느린 경로는 한 번만 실행되며 __builtin_expect를 활용하여 분기 경로에 빠른 경로를 사용해야 함을 암시합니다. 빠른 경로는 잠금을 전혀 사용하지 않고 실행됩니다! mikeash.com/pyblog/…
Adam Kaplan

GCC 9.2에서 차이를 보이지 않는 것으로 보입니다 : gcc.godbolt.org/z/GzP6cx (실제로 이미 8.1)
Ruslan

40

아이디어는 __builtin_expect컴파일러에게 일반적으로 표현식이 c로 평가되어 컴파일러가 해당 경우에 맞게 최적화 될 수 있음을 알게된다는 것입니다.

나는 누군가가 그들이 영리하다고 생각하고 이것을함으로써 일을 가속화하고 있다고 생각합니다.

불행히도, 상황이 잘 이해 되지 않으면 (그들이 그런 일을하지 않았을 가능성이 높다), 상황을 악화시킬 수 있습니다. 문서에는 다음과 같이 말합니다.

일반적 -fprofile-arcs으로 프로그래머가 프로그램의 실제 성능을 예측하는 데 악명이 높기 때문에 실제 프로파일 피드백을 사용하는 것이 좋습니다 ( ). 그러나이 데이터를 수집하기 어려운 응용 프로그램이 있습니다.

일반적으로 다음과 같은 경우를 __builtin_expect제외하고는 사용 하지 않아야합니다.

  • 실제 성능 문제가 있습니다
  • 이미 시스템의 알고리즘을 적절하게 최적화했습니다
  • 특정 사례가 가장 가능성이 높다는 주장을 백업 할 성능 데이터가 있습니다.

7
@ Michael : 그것은 실제로 분기 예측에 대한 설명이 아닙니다.
Oliver Charlesworth

3
"대부분의 프로그래머는 BAD입니다"또는 어쨌든 컴파일러보다 낫지 않습니다. 모든 바보는 for 루프에서 연속 조건이 참일 가능성이 있지만 컴파일러는 그것을 알고 있다는 이점이 없다는 것을 알고 있습니다. 어떤 이유로 경우에 당신은 거의 항상 즉시 휴식 것이 루프를 작성, 당신은 PGO에 대한 컴파일러에 프로파일 데이터를 제공 할 수없는 경우, 다음 아마 프로그래머는 컴파일러가하지 않는 무언가를 알고있다.
Steve Jessop

15
어떤 상황에서는 어느 지점이 더 가능성이 큰지가 아니라 어느 지점이 중요한지 중요합니다. 예기치 않은 분기가 abort ()로 연결되면 가능성은 중요하지 않으며 최적화 할 때 예상 분기에 성능 우선 순위를 부여해야합니다.
Neowizard 2019

1
청구의 문제점은 최적화 CPU가 가지 가능성에 대해 꽤 많이 하나에 제한하여 수행 할 수 있다는 것입니다 : 분기 예측 및 사용 여부를이 최적화 발생 __builtin_expect여부 . 반면에 컴파일러는 핫 경로가 연속되도록 코드를 구성하고 코드를 더 멀리 최적화하거나 크기를 줄이지 않고 벡터화 할 브랜치에 대한 결정을 내리는 등의 코드 구성과 같은 브랜치 확률을 기반으로 많은 최적화를 수행 할 수 있습니다. 핫 경로 예약 등이 향상되었습니다.
BeeOnRope

1
... 개발자의 정보가 없으면 맹인이며 중립 전략을 선택합니다. 개발자가 확률에 대해 옳은 경우 (및 대부분의 경우 분기가 일반적으로 수행 / 취득되지 않음을 이해하는 것은 사소한 일임) 이러한 이점을 얻을 수 있습니다. 당신이 약간의 패널티를 얻지 못하더라도, 그것은 이점보다 훨씬 크지 않으며, 가장 중요한 것은 CPU 분기 예측을 무시 하지 않습니다 .
BeeOnRope

13

설명에서 알 수 있듯이 첫 번째 버전은 예측 요소를 구성에 추가하여 x == 0분기가 더 가능성이 높다는 것을 컴파일러에게 알려줍니다. 즉, 분기가 프로그램에서 더 자주 사용하게됩니다.

이를 염두에두고, 컴파일러는 예상치 못한 조건이 발생할 경우 더 많은 작업을 수행해야하는 대가로 예상 조건이 유지 될 때 최소한의 작업 만 요구하도록 조건부를 최적화 할 수 있습니다.

컴파일 단계 및 결과 어셈블리에서 조건이 구현되는 방식을 살펴보고 한 분기가 다른 분기보다 덜 작동하는 방법을 살펴보십시오.

그러나 결과 코드의 차이가 상대적으로 작기 때문에 문제의 조건이 많은 내부 루프의 일부인 경우에만이 최적화가 눈에 띄는 효과를 기대합니다 . 잘못된 방향으로 최적화하면 성능이 저하 될 수 있습니다.


그러나 결국 컴파일러가 조건을 확인하는 것입니다. 컴파일러가 항상이 분기를 가정하고 진행한다고 말하고 나중에 일치하는 것이 없으면 말입니까? 무슨 일이야? 컴파일러 디자인 에서이 분기 예측 항목과 작동 방식에 대해 더 많은 것이 있다고 생각합니다.
kingsmasher1

2
이것은 실제로 미세 최적화입니다. 조건이 어떻게 구현되는지 찾아보십시오. 하나의 브랜치에 대한 작은 편견이 있습니다. 가상의 예로서, 조건부 (conditional)가 어셈블리에서 테스트와 점프가된다고 가정하자. 그런 다음 점프 분기가 비 점프 분기보다 느리므로 예상 분기를 비 점프 분기로 만드는 것이 좋습니다.
Kerrek SB

고맙습니다, 당신과 마이클 나는 비슷한 견해를 가지고 있다고 생각하지만 다른 말로 표현합니다 :-) 테스트 및 분기에 대한 정확한 컴파일러 내부가 여기에서 설명 할 수 없다는 것을 이해합니다 :)
kingsmasher1

그들은 또한 인터넷을 검색함으로써 매우 쉽게 배울 수 있습니다 :-)
Kerrek SB

나는 나의 대학 서적으로 되돌아가는 것이 더 나을 것이다 compiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

나는 당신이 묻고 있다고 생각하는 질문에 대한 답을 보지 못했습니다.

컴파일러에 분기 예측을 암시하는보다 이식 가능한 방법이 있습니까?

귀하의 질문 제목에 따라 다음과 같이 생각했습니다.

if ( !x ) {} else foo();

컴파일러에서 'true'가 더 가능성이 높다고 가정하면를 호출하지 않도록 최적화 할 수 foo()있습니다.

여기서 문제는 일반적으로 컴파일러가 무엇을 가정할지 알지 못한다는 것입니다. 따라서 이러한 종류의 기술을 사용하는 모든 코드는 신중하게 측정해야합니다 (컨텍스트가 변경되면 시간이 지남에 따라 모니터링 될 수 있음).


이것은 실제로 OP가 원래 의도 한 것과 똑같을 수도 있지만 (제목에 표시된대로) 어떤 이유로 든 else게시물 본문 에서 사용되지 않았습니다.
브렌트 브래드 번

1

@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는 마지막 경우에 작동합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.