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

나는 #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

메모리 명령 순서는 불변 : 먼저 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의 가능성 / 불확실한 속성을 사용하는 방법 동일한 기능을 수행 할 것입니다.


답변

아이디어는 __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는 마지막 경우에 작동합니다.


답변