GCC에 대한 컴파일러 힌트가 분기 예측이 항상 특정 방식으로 진행되도록 강제합니까?


118

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

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

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

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


(컴파일러와 무관 한) 비정상이 발생하면 예외를 던질 수도 있습니다.
Shep

2
밀접한 관련이 있음 : Linux 커널의
Michael Hampton

답변:


9

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 링크


[[unlikely]]in ifvs [[likely]]in else?
WilliamKF

아무 이유없이, 속성이 필요한 곳을 시도한 후에이 별자리에 도달했습니다.
pseyfert

정말 멋진. 너무 나쁜 방법은 이전 C ++ 버전에 적용 할 수 없습니다.
Maxim Egorushkin

환상적인 godbolt 링크
Lewis Kelsey

87

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 분기 예측자는 그러한 결정에있어서 당신보다 더 능숙 할 가능성이 높으므로 이것은 조기 마이크로 최적화가 될 수 있습니다.

3
constexpr함수가 아닌 매크로를 표시하는 이유가 있습니까?
Columbo

22
@Columbo : constexpr함수 가이 매크로를 대체 할 수 있다고 생각하지 않습니다 . if내가 믿는 성명서에 직접 있어야 합니다. 같은 이유 assert는 결코 constexpr함수가 될 수 없습니다 .
Mooing Duck

1
@MooingDuck 동의하는 데 더 많은 이유 가 있지만 동의합니다 .
Shafik Yaghmour

7
@Columbo 매크로를 사용하는 한 가지 이유는 이것이 C 또는 C ++에서 매크로가 함수 보다 의미 상 더 정확한 몇 안되는 위치 중 하나이기 때문 입니다. 이 기능은 때문에 최적화 작업에 나타납니다 (그것은 이다 최적화 : constexpr값의 의미, 어셈블리 구현 고유의 인라인하지 대한 단지 회담); 코드의 간단한 해석 (인라인 없음)은 의미가 없습니다. 이를 위해 함수를 사용할 이유가 전혀 없습니다.
Leushenko 2015 년

2
@Leushenko __builtin_expect자체가 최적화 힌트라고 생각하므로 사용을 단순화하는 방법이 최적화에 의존한다고 주장하는 것은 ... 설득력이 없습니다. 또한 constexpr처음에 작동하도록 지정자를 추가하지 않고 상수 표현식으로 작동하도록 만들었습니다. 그리고 예, 기능을 사용하는 이유가 있습니다. 예를 들어 .NET과 같은 귀여운 작은 이름으로 전체 네임 스페이스를 오염시키고 싶지 않습니다 likely. 나는 LIKELY그것이 매크로임을 강조하고 충돌을 피하기 위해 eg를 사용해야 할 것이지만 그것은 단순히 추악합니다.
Columbo

46

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를 지원합니다 .

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


Linux 커널에 적합한 것은 C ++ 11에 충분하지 않습니다.
Maxim Egorushkin

@MaximEgorushkin 참고, 실제로 사용을 권장하지 않습니다. 사실 내가 인용 한 gcc 문서는 그 기술을 사용하지도 않습니다. 내 대답의 주된 목적은이 경로를 따라 가기 전에 대안을 신중하게 고려하는 것입니다.
Shafik Yaghmour

44

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

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

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

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


9
이것은 올바르지 않습니다. 모든 최신 버전의 x86에서 기본 예측 알고리즘은 정방향 분기가 사용되지 않고 역방향 분기가 사용된다는 것을 예측하는 것입니다 ( software.intel.com/en-us/articles/… 참조 ). 따라서 코드를 재 배열 하면 CPU에 효과적으로 힌트를 줄 수 있습니다 . 이것이 바로 GCC가 __builtin_expect.
Nemo

6
@Nemo, 내 대답의 첫 문장을 지나서 읽었습니까? 당신이 말한 모든 것은 내 대답이나 주어진 링크에 포함됩니다. 질문은 "항상 특정 방식으로 가지를 예측하도록 강제"할 수 있는지 물었고, 대답은 "아니오"이며 다른 대답이 이것에 대해 충분히 명확하다고 생각하지 않았습니다.
Artelius

4
좋아요, 좀 더주의 깊게 읽어야 했어요. 질문자가 분명히 찾고 있기 때문에이 대답은 기술적으로 정확하지만 쓸모없는 것 같습니다 __builtin_expect. 그래서 이것은 단지 주석이어야합니다. 그러나 그것은 거짓이 아니므로 내 반대표를 제거했습니다.
Nemo

IMO 그것은 쓸모가 없습니다. 이는 CPU와 컴파일러가 실제로 어떻게 작동하는지에 대한 유용한 설명이며, 이러한 옵션이 있거나없는 성능 분석과 관련이있을 수 있습니다. 예를 들어, __builtin_expect측정 할 수있는 테스트 케이스를 사소하게 생성 perf stat하는 데는 일반적으로 사용할 수 없으며 분기 오류 예측률이 매우 높습니다. 분기 레이아웃 에만 영향을줍니다 . 그리고 BTW, Intel 이후 Sandybridge 또는 적어도 Haswell은 정적 예측을 많이 사용 하지 않습니다 . BHT에는 오래된 별칭이든 아니든 항상 예측이 있습니다. xania.org/201602/bpu-part-two
Peter Cordes

24

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) 문맥 변환을 .


참고 문맥 전환을 2012 년에 결함을 통해 들어왔다 심지어 2014 년 말에 아직 구현 차이가 있었다. 실제로 링크 한 케이스가 여전히 gcc에서 작동하지 않는 것 같습니다.
Shafik Yaghmour

@ShafikYaghmour에 관련된 문맥 변환과 관련하여 흥미로운 관찰입니다 switch. 감사합니다. 여기에 포함 된 문맥 변환은 유형 bool에 따라 다르며switch 문맥을 포함하지 않는 5 개의 특정 문맥에 나열되어 있습니다 .
Maxim Egorushkin

이것은 C ++에만 영향을 미칩니다. 따라서 (_Bool)(condition)C에는 연산자 오버로딩이 없기 때문에 기존 C 프로젝트를 사용하도록 변경할 이유 가 없습니다.
Peter Cordes

2
당신의 예에서, 당신은 사용 (condition)하지 !!(condition). 둘 다 true변경 한 후입니다 (g ++ 7.1로 테스트 됨). !!부울 화에 사용할 때 말하는 문제를 실제로 보여주는 예제를 구성 할 수 있습니까 ?
Peter Cordes

3
Peter Cordes가 지적했듯이 "이 매크로가 이렇게 정의 된 경우 :"라고 말한 다음 '!!'를 사용하여 매크로를 표시합니다. "if 문의 의미를 변경하고 코드를 손상시킬 수 있습니다. 다음 코드를 고려하십시오." ... 그리고 '!!'를 사용하지 않는 코드를 보여줍니다. 전혀-C ++ 11 이전에도 깨진 것으로 알려져 있습니다. 주어진 매크로 (!! 사용)가 잘못된 예를 보여주기 위해 대답을 변경하십시오.
Carlo Wood

18

다른 답변이 모두 적절하게 제안했듯이 __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를 참조 하십시오.


2
분기 예측 힌트 접두사는 필요하지 않기 때문에 무시됩니다. 코드 순서를 변경하는 것만으로도 똑같은 효과를 얻을 수 있습니다. (기본 분기 예측 알고리즘은 역방향 분기는 취하고 정방향 분기는 취하지 않는다고 추측하는 것입니다.) 따라서 실제로 CPU에 힌트를 제공 할 수 있습니다 __builtin_expect. 전혀 쓸모가 없습니다. 당신은 그 권리입니다 cold속성도 유용하지만 당신은 유틸리티를 과소 평가 __builtin_expect나는 생각한다.
Nemo

최신 Intel CPU는 정적 분기 예측을 사용하지 않습니다. 설명하는 알고리즘 인 @Nemo는 역방향 분기가 예측되고 정방향 분기는 이전 프로세서에서 사용되지 않은 것으로 예측되며 Pentium M 정도까지 사용되었지만 최신 디자인은 기본적으로 무작위로 추측하여 분기로 인덱싱합니다. 해당 분기에 대한 정보를 찾고 거기에있는 모든 정보를 사용할 것으로 예상 되는 테이블 (기본적으로 쓰레기 일 수 있음에도 불구하고). 따라서 분기 예측 힌트는 이론적으로는 유용하지만 실제로는 그렇지 않을 수 있으므로 인텔이 제거했습니다.
Cody Gray

명확하게 말하면, 분기 예측의 구현은 매우 복잡하고 주석의 공간 제약으로 인해 지나치게 단순화해야했습니다. 이것은 정말로 그 자체로 완전한 대답이 될 것입니다. Haswell과 같은 최신 마이크로 아키텍처에는 여전히 정적 분기 예측의 흔적이있을 수 있지만 예전만큼 간단하지는 않습니다.
Cody Gray

"최신 인텔 CPU는 정적 분기 예측을 사용하지 않습니다"에 대한 참조가 있습니까? 인텔의 자체 기사 ( software.intel.com/en-us/articles/… )는 그렇지 않다고 말합니다 ... 그러나 그것은 2011 년
Nemo

@Nemo라는 공식적인 참조는 없습니다. 인텔은 칩에 사용되는 분기 예측 알고리즘에 대해 극도로 입을 다물고 있으며이를 영업 비밀로 취급합니다. 알려진 것의 대부분은 경험적 테스트를 통해 밝혀졌습니다. 그 어느 때보 다 Agner Fog의 재료 는 최고의 자원이지만 그는 "분기 예측기가 Haswell에서 재 설계된 것으로 보이지만 그 구조에 대해 알려진 바가 거의 없습니다."라고 말합니다. 안타깝게도 정적 BP가 더 이상 사용되지 않음을 보여주는 벤치 마크를 처음 본 곳을 기억할 수 없습니다.
Cody Gray

5

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


2

OP와 관련하여 GCC에서는 프로세서에게 항상 분기가 취해 지거나 취해지지 않는다고 가정하도록 지시 할 방법이 없습니다. 당신이 가진 것은 __builtin_expect이며, 다른 사람들이 말하는 것을 수행합니다. 또한 분기가 항상 사용 되는지 여부를 프로세서에 알리고 싶지는 않습니다 . Intel 아키텍처와 같은 오늘날의 프로세서는 상당히 복잡한 패턴을 인식하고 효과적으로 적응할 수 있습니다.

그러나 기본적 으로 분기를 예측 할지 여부 제어하고 싶은 경우가 있습니다. 분기 통계와 관련하여 코드가 "콜드"라고 불릴 것임을 알고있는 경우.

한 가지 구체적인 예 : 예외 관리 코드. 정의에 따라 관리 코드는 예외적으로 발생하지만 최대 성능이 필요할 때 (가능한 한 빨리 처리해야하는 심각한 오류가있을 수 있음) 따라서 기본 예측을 제어 할 수 있습니다.

또 다른 예 : 입력을 분류하고 분류 결과를 처리하는 코드로 이동할 수 있습니다. 분류가 많은 경우 프로세서는 통계를 수집하지만 동일한 분류가 곧 발생하지 않고 예측 리소스가 최근 호출 된 코드에 할당되기 때문에 통계를 잃을 수 있습니다. 프로세서에 "이 코드에 예측 리소스를 할당하지 마십시오"라고 때때로 "캐시하지 마십시오"라고 말할 수있는 프리미티브가 있기를 바랍니다.

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