GCC에 대한 컴파일러 힌트가 분기 예측이 항상 특정 방식으로 진행되도록 강제합니까? 하드웨어는 어떻습니까? 빨리

인텔 아키텍처의 경우 GCC 컴파일러가 항상 내 코드에서 특정 방식으로 분기 예측을 강제하는 코드를 생성하도록 지시하는 방법이 있습니까? 인텔 하드웨어가이를 지원합니까? 다른 컴파일러 나 하드웨어는 어떻습니까?

빨리 실행하고 싶은 경우를 알고 있고 최근에 해당 분기를 가져 갔더라도 다른 분기를 가져와야 할 때 속도가 느려지는 것에 대해 신경 쓰지 않는 C ++ 코드에서 이것을 사용합니다.

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

Evdzhan Mustafa에 대한 질문에 대한 후속 질문으로 힌트는 프로세서가 명령을 처음 만났을 때 힌트를 지정할 수 있습니까? 모든 후속 분기 예측은 정상적으로 작동합니까?



답변

C ++ 20에서 가능성과 가능성이없는 속성 은 표준화되어야하며 이미 g ++ 9에서 지원 됩니다 . 따라서 여기 에서 논의한대로 다음과 같이 작성할 수 있습니다.

if (a>b) {
  /* code you expect to run often */
  [[likely]] /* last statement */
}

예를 들어 다음 코드에서 else 블록은 [[unlikely]]if 블록의 덕분에 인라인됩니다.

int oftendone( int a, int b );
int rarelydone( int a, int b );
int finaltrafo( int );

int divides( int number, int prime ) {
  int almostreturnvalue;
  if ( ( number % prime ) == 0 ) {
    auto k                         = rarelydone( number, prime );
    auto l                         = rarelydone( number, k );
    [[unlikely]] almostreturnvalue = rarelydone( k, l );
  } else {
    auto a            = oftendone( number, prime );
    almostreturnvalue = oftendone( a, a );
  }
  return finaltrafo( almostreturnvalue );
}

속성의 존재 / 부재를 비교하는 godbolt 링크


답변

GCC는 이러한 기능 __builtin_expect(long exp, long c)을 제공 하는 기능 을 지원합니다 . 여기 에서 설명서를 확인할 수 있습니다 .

exp사용 된 조건은 어디 이며 c예상 값입니다. 예를 들어 당신이 원하는 경우

if (__builtin_expect(normal, 1))

어색한 구문 때문에 일반적으로 다음과 같은 두 개의 사용자 정의 매크로를 정의하여 사용됩니다.

#define likely(x)    __builtin_expect (!!(x), 1)
#define unlikely(x)  __builtin_expect (!!(x), 0)

작업을 쉽게하기 위해서입니다.

다음 사항에 유의하십시오.

  1. 이것은 표준이 아닙니다
  2. 컴파일러 / cpu 분기 예측자는 그러한 결정에있어서 당신보다 더 능숙 할 가능성이 높으므로 이것은 조기 마이크로 최적화가 될 수 있습니다.

답변

gcc에는 긴 __builtin_expect (long exp, long c)가 있습니다 ( 강조 내 ) :

__builtin_expect를 사용하여 컴파일러에 분기 예측 정보를 제공 할 수 있습니다. 일반적 으로 프로그래머는 자신의 프로그램이 실제로 어떻게 수행되는지 예측하는 데 악명이 높기 때문에이 (-fprofile-arcs)에 대한 실제 프로필 피드백을 사용하는 것이 좋습니다 . 그러나이 데이터를 수집하기 어려운 애플리케이션이 있습니다.

반환 값은 정수 표현식이어야하는 exp의 값입니다. 내장의 의미는 exp == c가 예상된다는 것입니다. 예를 들면 :

if (__builtin_expect (x, 0))
   foo ();

x가 0이 될 것으로 예상하기 때문에 foo를 호출하지 않을 것임을 나타냅니다. exp에 대한 적분 표현식으로 제한되므로 다음과 같은 구성을 사용해야합니다.

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

포인터 또는 부동 소수점 값을 테스트 할 때.

문서에서 설명하는대로 실제 프로필 피드백을 사용하는 것을 선호해야하며이 문서는 이에 대한 실제 예__builtin_expect. 또한 g ++에서 프로파일 기반 최적화를 사용하는 방법을 참조하십시오 . .

또한 이 기능을 사용 하는 kernal macros like () 및 likely ()에 대한 Linux 커널 초보자 기사를 찾을 수 있습니다 .

#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)

!!매크로 에서 사용 된 것에 대한 설명은 (조건) 대신 !! (조건)을 사용 하는 이유 에서 찾을 수 있습니다 . .

이 기술이 Linux 커널에서 사용된다고해서 항상 사용하는 것이 합리적이라는 의미는 아닙니다. 이 질문에서 내가 최근에 컴파일 시간 상수 또는 변수로 매개 변수를 전달할 때 함수 성능의 차이에 대해 대답 했음을 알 수 있습니다. 이는 많은 수작업 최적화 기술이 일반적인 경우에 작동하지 않습니다. 기술이 효과적인지 이해하려면 코드를 신중하게 프로파일 링해야합니다. 많은 오래된 기술은 최신 컴파일러 최적화와 관련이 없을 수도 있습니다.

내장 기능은 이식 가능하지 않지만 clang도 __builtin_expect를 지원합니다 .

또한 일부 아키텍처에서는 차이가 없을 수 있습니다 .


답변

아니 없어. (적어도 최신 x86 프로세서에서는.)

__builtin_expect다른 답변에서 언급 한 것은 gcc가 어셈블리 코드를 정렬하는 방식에 영향을 미칩니다. CPU의 분기 예측기에 직접적인 영향을 주지 않습니다 . 물론 코드 순서를 변경하면 분기 예측에 간접적 인 영향이있을 것입니다. 그러나 최신 x86 프로세서에는 CPU에 “이 분기가 사용 / 사용되지 않는다고 가정”하는 명령이 없습니다.

자세한 내용은이 질문을 참조하십시오. 인텔 x86 0x2E / 0x3E 접두사 분기 예측이 실제로 사용 되었습니까?

명확하게하기 위해 __builtin_expect및 / 또는을 사용 하면 코드 레이아웃을 통해 분기 예측 자에 힌트를 제공하고 ( x86-64 어셈블리의 성능 최적화-정렬 및 분기 예측 참조 ) 캐시 동작을 개선 하여 코드 성능을 향상시킬 -fprofile-arcs 있습니다. “가능성이없는”코드를 “가능성이 높은”코드에서 멀리 유지합니다.


답변

C ++ 11에서 가능성이 있거나 가능성이없는 매크로를 정의하는 올바른 방법은 다음과 같습니다.

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

이 메서드는와 달리 모든 C ++ 버전과 호환 [[likely]]되지만 비표준 확장에 의존합니다 __builtin_expect.


이러한 매크로가 이렇게 정의 된 경우 :

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

이것은 if문장 의 의미를 바꾸고 코드를 깨뜨릴 수 있습니다. 다음 코드를 고려하십시오.

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

그리고 출력 :

if(a) is true
if(LIKELY(a)) is false

보시다시피 LIKELY를 !!캐스트로 사용하는 정의 boolif.

여기서 요점은하지 operator int()operator bool()관련되어야한다. 좋은 습관입니다.

오히려 그 사용하는 !!(x)대신 static_cast<bool>(x)에 대한 컨텍스트 손실 C ++ (11) 문맥 변환을 .


답변

다른 답변이 모두 적절하게 제안했듯이 __builtin_expect어셈블리 코드를 정렬하는 방법에 대한 힌트를 컴파일러에 제공하는 데 사용할 수 있습니다 . 으로 공식 문서가 지적, 대부분의 경우, 어셈블러는 GCC 팀에 의해 제작 된 것과 같은 좋은으로하지 않습니다 당신의 두뇌에 내장. 추측보다는 실제 프로필 데이터를 사용하여 코드를 최적화하는 것이 가장 좋습니다.

유사한 줄에 있지만 아직 언급되지 않은 것은 컴파일러가 “콜드”경로에서 코드를 생성하도록 강제하는 GCC 특정 방법입니다. 여기에는 noinlinecold속성 의 사용이 포함되며 , 이는 그들이하는 것처럼 들리는 것과 정확히 일치합니다. 이러한 속성은 함수에만 적용 할 수 있지만 C ++ 11에서는 인라인 람다 함수를 선언 할 수 있으며이 두 속성은 람다 함수에도 적용 할 수 있습니다.

이것은 여전히 ​​마이크로 최적화의 일반적인 범주에 속하므로 표준 조언이 적용됩니다. 테스트는 추측하지 마십시오 __builtin_expect.. x86 프로세서의 어떤 세대도 분기 예측 힌트 ( 참조 )를 사용하지 않으므로 어쨌든 영향을 미칠 수있는 유일한 것은 어셈블리 코드의 순서입니다. 오류 처리 또는 “에지 케이스”코드가 무엇인지 알고 있으므로이 주석을 사용하여 컴파일러가 분기를 예측하지 않고 크기를 최적화 할 때 “핫”코드에서 멀리 링크하도록 할 수 있습니다.

샘플 사용법 :

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    
}

더 좋은 점은 GCC가 사용 가능한 경우 (예 :로 컴파일 할 때) 프로파일 피드백을 위해 자동으로이를 무시합니다 -fprofile-use.

공식 문서는 https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes를 참조 하십시오.


답변

__builtin_expect는 분기가 예상되는 방식을 컴파일러에 알리는 데 사용할 수 있습니다. 이는 코드 생성 방법에 영향을 줄 수 있습니다. 일반적인 프로세서는 코드를 순차적으로 더 빠르게 실행합니다. 그래서 당신이 쓰면

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

컴파일러는 다음과 같은 코드를 생성합니다.

if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

힌트가 정확하면 실제로 분기를 수행하지 않고 코드를 실행합니다. 각 if 문이 조건부 코드를 중심으로 분기되고 세 개의 분기를 실행하는 일반 시퀀스보다 빠르게 실행됩니다.

최신 x86 프로세서에는 가져갈 것으로 예상되는 분기 또는 가져 오지 않을 것으로 예상되는 분기에 대한 지침이 있습니다 (명령 접두사가 있습니다. 세부 사항에 대해서는 확실하지 않음). 프로세서가 그것을 사용하는지 확실하지 않습니다. 분기 예측이 이것을 잘 처리하기 때문에별로 유용하지 않습니다. 따라서 분기 예측에 실제로 영향을 미칠 수 있다고 생각하지 않습니다 .