programing

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

yoursource 2022. 7. 10. 10:35
반응형

다른 문장에서 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 ();

에서는, 「이러다」라고 부르는 하고 있지 .foo예상대로라면x0 이 0 。

그렇다면 직접 사용해 보는 것은 어떨까요?

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(CC'R'MONT CHANGE: (C'RONT CHANGE: C'MONT CHANGE: C)점프는 이미 가져온 명령을 thrash하기 때문에 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 수의 was was was was was was was was was was was was was was was was was was was 로 이동했습니다.retq★★★★★★★★★★★★★★★★★★!

새로운 코드는 기본적으로 다음과 같습니다.

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

이 최적화는 다음과 같이 수행되지 않았습니다.-O0.

보다 를 잘 바랍니다.__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는 마지막 케이스에서 작동합니다.

대부분의 경우 지점 예측을 그대로 두고 걱정할 필요가 없습니다.

브런치가 많은 CPU 집약적인 알고리즘이 도움이 되는 경우가 있습니다.경우에 따라서는 점프로 인해가 현재의 CPU 프로그램캐시를 초과하여 CPU가 소프트웨어의 다음 부분을 실행할 때까지 대기할 수 있습니다.마지막에 있을 것 같지 않은 브랜치를 밀어넣으면 메모리를 닫아 두고 있을 것 같지 않은 경우에만 점프합니다.

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

반응형