Linux 커널에서 가능성이 높거나 가능성이 낮은 매크로는 어떻게 작동하며 그 이점은 무엇입니까?


348

나는 리눅스 커널의 일부를 파헤 쳐서 다음과 같은 호출을 발견했다.

if (unlikely(fd < 0))
{
    /* Do something */
}

또는

if (likely(!err))
{
    /* Do something */
}

나는 그들의 정의를 찾았다.

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

나는 그들이 최적화를위한 것임을 알고 있지만 어떻게 작동합니까? 그리고 그것들을 사용함으로써 얼마나 많은 성능 / 크기 감소가 예상 될 수 있습니까? 그리고 최소한 병목 코드 (사용자 공간에서)에서 번거롭고 이식성이 떨어질 가치가 있습니다.


7
이것은 실제로 Linux 커널이나 매크로에만 국한된 것이 아니라 컴파일러 최적화입니다. 이를 반영하기 위해 다시 태그를 다시 작성해야합니까?
Cody Brocious

11
용지 모든 프로그래머가 메모리에 대해 알아야 할 사항 (p. 57)에 대한 깊이있는 설명이 포함되어 있습니다.
Torsten Marek

2
또한 참조BOOST_LIKELY
루 제로 Turra에게


13
이식성 문제가 없습니다. 당신은 인간든지 등의 작업을 수행 할 수 있습니다 #define likely(x) (x)#define unlikely(x) (x)힌트의이 종류를 지원하지 않는 플랫폼에.
David Schwartz

답변:


328

분기 예측이 점프 명령의 "유사한"측면을 선호하게하는 명령을 생성하도록 컴파일러에 대한 힌트입니다. 예측이 정확하다면 점프 명령이 기본적으로 자유롭고 제로 사이클이 걸린다는 것을 의미합니다. 반면에 예측이 잘못되면 프로세서 파이프 라인을 비워야하며 몇 번의 비용이 소요될 수 있습니다. 예측이 대부분 정확하다면 성능에 좋은 경향이 있습니다.

이러한 모든 성능 최적화와 마찬가지로 코드가 실제로 병목 상태에 있고 미세한 특성으로 인해 엄격한 루프로 실행되고 있는지 확인하기 위해 광범위한 프로파일 링 후에 만 ​​수행해야합니다. 일반적으로 Linux 개발자는 경험이 풍부하므로 그렇게했을 것입니다. gcc만을 대상으로하므로 이식성에 대해 크게 신경 쓰지 않으며 생성하려는 어셈블리에 대해 매우 가깝습니다.


3
이 매크로는 주로 오류 검사에 사용되었습니다. 오류는 정상적인 작동보다 적습니다. 몇몇 사람들은 가장 많이 사용되는 잎을 결정하기 위해 프로파일 링이나 계산을합니다 ...
gavenkoa

51
프래그먼트 "[...]that it is being run in a tight loop"와 관련하여 많은 CPU에는 분기 예측 변수 가 있으므로 이러한 매크로를 사용하면 첫 번째 시간 코드가 실행되거나 히스토리 테이블이 분기 테이블에 동일한 색인을 가진 다른 분기에 의해 겹쳐 쓰기 될 때만 도움이됩니다. 긴밀한 루프에서 분기가 대부분의 시간 동안 진행된다고 가정하면 분기 예측기는 올바른 분기를 매우 빨리 추측하기 시작할 것입니다. -당신의 친구는 pedantry.
로스 로저스

8
@RossRogers : 실제로 발생하는 것은 컴파일러가 가지를 정렬하므로 일반적인 경우는 그렇지 않습니다. 분기 예측이 작동하는 경우에도 더 빠릅니다. 취해진 분기는 완벽하게 예측 된 경우에도 명령어 가져 오기 및 디코딩에 문제가 있습니다. 일부 CPU는 히스토리 테이블에없는 분기를 정적으로 예측하며 일반적으로 포워드 분기에는 사용하지 않는 것으로 가정합니다. 인텔 CPU는 그런 식으로 작동하지 않습니다. 예측 변수 테이블 항목 이이 분기에 대한 것인지 확인하지 않고 어쨌든 사용합니다. 핫 지점과 동일한 항목 별명 감기 지점의 힘 ...
피터 코르

12
이 답변은 브랜치 예측을 돕고 @PeterCordes가 지적한 것처럼 대부분의 최신 하드웨어에는 암시 적 또는 명시 적 정적 브랜치 예측이 없기 때문에 주로 사용되지 않습니다. 실제로 힌트는 정적 분기 힌트 또는 다른 유형의 최적화와 관계없이 컴파일러가 코드를 최적화하는 데 사용합니다. 오늘날 대부분의 아키텍처의 경우 핫 패스를 연속적으로 만들고 핫 패스를 더 잘 예약하고 느린 패스의 크기를 최소화하고 예상 경로 만 벡터화하는 등 중요한 "다른 최적화"입니다.
BeeOnRope

3
캐시 프리 페치 및 워드 크기로 인해 @BeeOnRope는 여전히 프로그램을 선형으로 실행하는 이점이 있습니다. 다음 메모리 위치는 이미 페치되어 캐시에 있으며, 분기 대상은 아닐 수도 있습니다. 64 비트 CPU를 사용하면 한 번에 64 비트 이상을 가져옵니다. DRAM 인터리브에 따라 2x 3x 이상의 비트가 잡힐 수 있습니다.
브라이스

88

GCC 4.8이 수행하는 작업을 확인하기 위해 디 컴파일하자

없이 __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)
        printf("%d\n", 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 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

메모리 명령 순서는 불변 : 먼저 printf다음 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 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  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
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf(컴파일 된 __printf_chk후), 함수의 끝으로 이동 한 puts다른 응답하여 한 바와 같이 분기 예측을 향상시키고 복귀.

따라서 기본적으로 다음과 같습니다.

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

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

그러나 CPU가__builtin_expect 없는 것보다 더 빠르게 실행되는 예제를 작성하면 행운을 빕니다 . 요즘 CPU는 정말 똑똑합니다 . 나의 순진한 시도 가 여기에 있습니다 .

C ++ 20 [[likely]][[unlikely]]

C ++ 20은 C ++ 내장 기능을 표준화 했습니다. if-else 문에서 C ++ 20의 가능성 / 불확실한 속성을 사용하는 방법 동일한 기능을 수행 할 것입니다.


71

이들은 분기가 진행될 수있는 방법에 대한 힌트를 컴파일러에 제공하는 매크로입니다. 매크로는 사용 가능한 경우 GCC 특정 확장으로 확장됩니다.

GCC는이를 사용하여 분기 예측을 최적화합니다. 예를 들어 다음과 같은 것이 있다면

if (unlikely(x)) {
  dosomething();
}

return x;

그런 다음이 코드를 다음과 같이 재구성 할 수 있습니다.

if (!x) {
  return x;
}

dosomething();
return x;

이것의 이점은 프로세서가 처음으로 분기를 수행 할 때 추론 적으로 코드를 더 많이로드하고 실행했기 때문에 상당한 오버 헤드가 있다는 것입니다. 그것이 지점을 취할 것이라고 결정하면, 그것을 무효화하고 지점 대상에서 시작해야합니다.

대부분의 최신 프로세서에는 이제 일종의 분기 예측이 있지만 이전에 분기를 통과 한 경우에만 지원하며 분기는 여전히 분기 예측 캐시에 있습니다.

컴파일러와 프로세서가이 시나리오에서 사용할 수있는 여러 가지 다른 전략이 있습니다. 분기 예측기의 작동 방식에 대한 자세한 내용은 Wikipedia에서 확인할 수 있습니다. http://en.wikipedia.org/wiki/Branch_predictor


3
또한 핫 경로에서 코드 조각을 유지하지 않기 때문에 icache 풋 프린트에 영향을 미칩니다.
fche

2
더 정확하게, 그것은 그것을 할 수있는 goto반복하지 않고 S return x: stackoverflow.com/a/31133787/895245을
치로 틸리가郝海东冠状病六四事件法轮功

7

컴파일러가 하드웨어에서 지원하는 적절한 분기 힌트를 생성하도록합니다. 이것은 보통 명령 opcode에서 몇 비트를 돌리는 것을 의미하므로 코드 크기는 변경되지 않습니다. CPU는 예측 된 위치에서 명령어 가져 오기를 시작하고 파이프 라인을 플러시하고 분기에 도달했을 때 잘못된 것으로 판명되면 다시 시작합니다. 힌트가 맞다면 분기가 훨씬 빨라질 것이다. 정확히 얼마나 빨리 하드웨어에 의존 할 것인가; 이것이 코드 성능에 얼마나 영향을 미치는지는 정확한 시간 힌트의 비율에 달려 있습니다.

예를 들어, PowerPC CPU에서 힌트를 얻지 않은 분기는 16주기, 올바르게 힌트를주는 하나 8 및 잘못 힌트를 얻은 것 한 24 개를 취할 수 있습니다. 가장 안쪽 루프에서 좋은 힌트는 큰 차이를 만들 수 있습니다.

이식성은 실제로 문제가되지 않습니다. 아마도 정의는 플랫폼 별 헤더에 있습니다. 정적 분기 힌트를 지원하지 않는 플랫폼에 대해서는 "가능성"및 "가능성 없음"을 간단하게 정의 할 수 있습니다.


3
레코드의 경우 x86은 분기 힌트를위한 추가 공간을 사용합니다. 적절한 힌트를 지정하려면 분기에 1 바이트 접두사가 있어야합니다. 그러나 힌트는 Good Thing (TM)이라는 데 동의했습니다.
Cody Brocious

2
Dang CISC CPU 및 가변 길이 명령어;)
moonshadow

3
Dang RISC CPUs – 15 바이트 명령을 피하십시오;)
Cody Brocious

7
@CodyBrocious : P4에서는 분기 힌트가 도입되었지만 P4와 함께 포기되었습니다. 다른 모든 x86 CPU는 해당 접두사를 무시합니다 (접두사가 의미가없는 컨텍스트에서는 항상 무시되기 때문에). 이 매크로 gcc로 하여금 x86에서 실제로 브랜치 힌트 접두사를 방출하게 하지는 않습니다 . gcc가 빠른 경로에서 더 적은 수의 가지를 사용하여 기능을 배치하도록 도와줍니다.
Peter Cordes

5
long __builtin_expect(long EXP, long C);

이 구조는 컴파일러에게 EXP 표현식이 C 값을 가질 가능성이 가장 높다는 것을 알려줍니다. __builtin_expect 는 조건식에 사용됩니다. 거의 모든 경우에 부울 표현식의 컨텍스트에서 사용되며,이 경우 두 개의 도우미 매크로를 정의하는 것이 훨씬 편리합니다.

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

이 매크로는 다음과 같이 사용할 수 있습니다

if (likely(a > 1))

참조 : https://www.akkadia.org/drepper/cpumemory.pdf


1
매크로에서 이중 반전의 이유는 무엇입니까 (즉, 왜 __builtin_expect(!!(expr),0)대신에 사용 __builtin_expect((expr),0)합니까?
Michael Firth

1
@MichaelFirth "이중 반전" !!은 무언가를에 캐스팅하는 것과 같습니다 bool. 어떤 사람들은 이런 식으로 쓰는 것을 좋아합니다.
Ben XO

2

(일반적인 의견-다른 답변은 세부 사항을 다룹니다)

그것들을 사용하여 이식성을 잃어 버릴 이유가 없습니다.

항상 다른 컴파일러를 사용하여 다른 플랫폼에서 컴파일 할 수있는 간단한 무효과 "인라인"또는 매크로를 생성 할 수 있습니다.

다른 플랫폼을 사용하는 경우 최적화의 이점을 얻지 못합니다.


1
이식성을 사용하지 마십시오. 지원하지 않는 플랫폼은 빈 문자열로 확장하도록 정의하기 만합니다.
sharptooth

2
두 분이 실제로 서로 동의하고 있다고 생각합니다. 혼란 스럽습니다. (그것의 모습에서, 앤드류의 코멘트 "는 휴대 성을 잃지 않고 사용할 수 있습니다"라고되어 있지만 sharptooth은 그와 반대 "그들은 휴대용 아니에요로 사용하지 마십시오"라고 생각했다.)
밀알

2

Cody 의 의견에 따르면 이것은 Linux와 관련이 없지만 컴파일러에 대한 힌트입니다. 아키텍처와 컴파일러 버전에 따라 달라집니다.

Linux에서이 특정 기능은 드라이버에서 다소 잘못 사용됩니다. osgx 가 hot attribute의 의미론 에서 지적한 것처럼 블록에서 호출 된 모든 hot또는 cold함수는 조건이 가능하거나 그렇지 않다는 것을 자동으로 암시 할 수 있습니다. 예를 들어, dump_stack()이 표시 cold가 중복되도록 표시 되어 있습니다.

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

향후 버전에서는 gcc이러한 힌트를 기반으로 함수를 선택적으로 인라인 할 수 있습니다. 또한 그렇지 않다는 제안도 boolean있었지만, 가장 가능성이 높은 점수 등입니다. 일반적으로와 같은 대체 메커니즘을 사용하는 것이 좋습니다 cold. 고온 경로 이외의 장소에서 사용할 이유가 없습니다. 컴파일러가 한 아키텍처에서 수행하는 작업은 다른 아키텍처와 완전히 다를 수 있습니다.


2

많은 Linux 릴리스에서 / usr / linux /에 complier.h를 찾을 수 있으며 간단하게 사용할 수 있습니다. 또 다른 의견으로는, like ()가 like ()보다 더 유용합니다.

if ( likely( ... ) ) {
     doSomething();
}

많은 컴파일러에서도 최적화 할 수 있습니다.

그런데 코드의 세부 동작을 관찰하려면 다음과 같이 간단하게 수행 할 수 있습니다.

gcc -c test.c objdump -d test.o> obj.s

그런 다음 obj.s를 열면 답을 찾을 수 있습니다.


1

이들은 분기에 힌트 접두사를 생성하는 컴파일러에 대한 힌트입니다. x86 / x64에서는 1 바이트를 차지하므로 각 분기마다 최대 1 바이트가 증가합니다. 성능에 관해서는 전적으로 응용 프로그램에 달려 있습니다. 대부분의 경우 프로세서의 분기 예측기는 요즘 무시합니다.

편집 : 실제로 실제로 도울 수있는 곳을 잊었습니다. 컴파일러는 제어 흐름 그래프를 재정렬하여 '유사한'경로에 사용되는 분기 수를 줄일 수 있습니다. 여러 개의 이탈 사례를 확인하는 루프가 크게 개선 될 수 있습니다.


10
gcc는 x86 분기 힌트를 생성하지 않습니다. 적어도 모든 인텔 CPU는 무시합니다. 그래도 인라인 및 루프 언 롤링을 피함으로써 가능성이 적은 지역에서 코드 크기를 제한하려고 시도합니다.
alex strange

1

이들은 프로그래머가 주어진 표현식에서 가장 가능성있는 분기 조건에 대해 힌트를 제공하는 GCC 함수입니다. 이를 통해 컴파일러는 분기 명령어를 빌드 할 수 있으므로 가장 일반적인 경우에는 실행하는 데 가장 적은 수의 명령어가 사용됩니다.

분기 명령어가 작성되는 방법은 프로세서 아키텍처에 따라 다릅니다.

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