programing tip

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

itbloger 2020. 7. 3. 20:22
반응형

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


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

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

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

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

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

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

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는 그 당시에는 정말 똑똑했습니다 . 나의 순진한 시도 가 여기에 있습니다 .


Well, as it says in the description, the first version adds a predictive element to the construction, telling the compiler that the x == 0 branch is the more likely one - that is, it's the branch that will be taken more often by your program.

With that in mind, the compiler can optimize the conditional so that it requires the least amount of work when the expected condition holds, at the expense of maybe having to do more work in case of the unexpected condition.

Take a look at how conditionals are implemented during the compilation phase, and also in the resulting assembly, to see how one branch may be less work than the other.

However, I would only expect this optimization to have noticeable effect if the conditional in question is part of a tight inner loop that gets called a lot, since the difference in the resulting code is relatively small. And if you optimize it the wrong way round, you may well decrease your performance.


I don't see any of the answers addressing the question that I think you were asking, paraphrased:

Is there a more portable way of hinting branch prediction to the compiler.

The title of your question made me think of doing it this way:

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

If the compiler assumes that 'true' is more likely, it could optimize for not calling foo().

The problem here is just that you don't, in general, know what the compiler will assume -- so any code that uses this kind of technique would need to be carefully measured (and possibly monitored over time if the context changes).


I test it on Mac according @Blagovest Buyukliev and @Ciro. The assembles look clear and I add comments;

Commands are gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

When I use -O3 , it looks the same no matter the __builtin_expect(i, 0) exist or not.

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

When compile with -O2 , it looks different with and without __builtin_expect(i, 0)

First without

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

Now with __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

To summarize, __builtin_expect works in the last case.

참고URL : https://stackoverflow.com/questions/7346929/what-is-the-advantage-of-gccs-builtin-expect-in-if-else-statements

반응형