Linux 커널의 가능성/가능성 없음 매크로는 어떻게 작동하며 그 이점은 무엇입니까?

StackOverflow https://stackoverflow.com/questions/109710

문제

저는 Linux 커널의 일부를 조사해 본 결과 다음과 같은 호출을 발견했습니다.

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)

최적화를 위한 것이라는 것은 알고 있지만 어떻게 작동합니까?그리고 이를 사용하면 얼마나 많은 성능/크기 감소가 예상됩니까?그리고 적어도 병목 현상 코드(물론 사용자 공간에서)에서는 번거로움을 감수할 가치가 있습니까(아마도 이식성을 잃을 수도 있음).

도움이 되었습니까?

해결책

이는 분기 예측이 점프 명령의 "가능성 있는" 측면을 선호하도록 하는 명령을 내보내도록 컴파일러에 대한 힌트입니다.이것은 큰 승리가 될 수 있습니다. 예측이 정확하다면 점프 명령은 기본적으로 무료이며 제로 사이클을 사용한다는 의미입니다.반면에 예측이 잘못된 경우 프로세서 파이프라인을 플러시해야 하며 여러 사이클이 소요될 수 있음을 의미합니다.예측이 대부분 정확하다면 이는 성능에 좋은 경향이 있습니다.

이러한 모든 성능 최적화와 마찬가지로 코드가 실제로 병목 현상에 있는지 확인하고 아마도 미시적 특성을 고려하여 긴밀한 루프에서 실행되고 있는지 확인하기 위해 광범위한 프로파일링 후에만 수행해야 합니다.일반적으로 Linux 개발자는 경험이 풍부하므로 그렇게했을 것이라고 생각합니다.그들은 gcc만을 대상으로 하기 때문에 이식성에 크게 신경 쓰지 않으며 생성하려는 어셈블리에 대해 매우 긴밀한 아이디어를 가지고 있습니다.

다른 팁

이는 분기가 어느 방향으로 갈 수 있는지에 대한 힌트를 컴파일러에 제공하는 매크로입니다.매크로는 사용 가능한 경우 GCC 특정 확장으로 확장됩니다.

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

대부분의 최신 프로세서에는 이제 일종의 분기 예측 기능이 있지만 이전에 분기를 거쳐본 적이 있고 분기가 여전히 분기 예측 캐시에 있는 경우에만 도움이 됩니다.

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

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 그런 다음 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 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 i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

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

하지만 다음과 같이 더 빠르게 실행되는 예제를 작성하는 데 행운을 빕니다. __builtin_expect 없는 것보다, 요즘 CPU가 정말 똑똑하네요.나의 순진한 시도 여기 있어요.

이로 인해 컴파일러는 하드웨어가 지원하는 적절한 분기 힌트를 내보냅니다.이는 일반적으로 명령어 opcode에서 몇 비트를 조작하는 것을 의미하므로 코드 크기는 변경되지 않습니다.CPU는 예측된 위치에서 명령을 가져오기 시작하고 파이프라인을 플러시하고 분기에 도달했을 때 잘못된 것으로 판명되면 다시 시작합니다.힌트가 올바른 경우 분기가 훨씬 빨라집니다. 정확히 얼마나 빨라지는지는 하드웨어에 따라 다릅니다.이것이 코드 성능에 얼마나 영향을 미치는지는 시간 힌트의 올바른 비율에 따라 달라집니다.

예를 들어, PowerPC CPU에서 힌트가 없는 분기에는 16주기가 걸릴 수 있으며, 올바르게 힌트된 분기에는 8주기, 잘못 힌트된 분기에는 24주기가 소요될 수 있습니다.가장 안쪽 루프에서는 좋은 힌트가 엄청난 차이를 만들 수 있습니다.

이식성은 실제로 문제가 되지 않습니다. 아마도 정의는 플랫폼별 헤더에 있을 것입니다.정적 분기 힌트를 지원하지 않는 플랫폼의 경우 "가능성 있음" 및 "가능성 없음"을 아무것도 정의하지 않을 수 있습니다.

long __builtin_expect(long EXP, long C);

이 구성은 컴파일러에게 표현식 Exp가 값 C를 가질 가능성이 높다고 말합니다.반환 값은 EXP입니다.__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

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

이를 사용함으로써 이식성을 잃을 이유가 없습니다.

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

다른 플랫폼을 사용하는 경우 최적화의 이점을 얻을 수 없습니다.

님의 의견에 따르면 코디, 이는 Linux와 관련이 없지만 컴파일러에 대한 힌트입니다.무슨 일이 일어나는지는 아키텍처와 컴파일러 버전에 따라 다릅니다.

Linux의 이 특정 기능은 드라이버에서 다소 잘못 사용됩니다.처럼 osgx 에서 지적한다 핫 속성의 의미, 어느 hot 또는 cold 블록에서 호출된 함수는 조건이 가능성이 있는지 여부를 자동으로 암시할 수 있습니다.예를 들어, dump_stack() 표시되어 있다 cold 그래서 이것은 중복됩니다.

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

향후 버전 gcc 이러한 힌트를 기반으로 함수를 선택적으로 인라인할 수 있습니다.그렇지 않다는 제안도 나왔다. boolean, 그러나 다음과 같은 점수 아마도, 등.일반적으로 다음과 같은 대체 메커니즘을 사용하는 것이 좋습니다. cold.핫패스 외에는 어떤 곳에서도 사용할 이유가 없습니다.한 아키텍처에서 컴파일러가 수행하는 작업은 다른 아키텍처에서 완전히 다를 수 있습니다.

많은 Linux 릴리스에서는 /usr/linux/에서 complier.h를 찾을 수 있으며 간단히 포함하여 사용할 수 있습니다.그리고 또 다른 의견으로는 아마도()보다 아마도()가 더 유용할 것입니다.

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

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

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

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

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

이는 분기에 힌트 접두사를 생성하기 위한 컴파일러에 대한 힌트입니다.x86/x64에서는 1바이트를 차지하므로 각 분기마다 최대 1바이트씩 증가합니다.성능은 전적으로 애플리케이션에 따라 달라집니다. 대부분의 경우 프로세서의 분기 예측기는 요즘에는 이를 무시합니다.

편집하다:그들이 실제로 도움을 줄 수 있는 한 곳을 잊어버렸습니다.이를 통해 컴파일러는 제어 흐름 그래프의 순서를 변경하여 '가능한' 경로에 대해 취해진 분기 수를 줄일 수 있습니다.이는 여러 종료 사례를 확인하는 루프에서 현저한 개선을 가져올 수 있습니다.

이는 프로그래머가 주어진 표현식에서 가장 가능성이 높은 분기 조건이 무엇인지에 대한 힌트를 컴파일러에 제공하는 GCC 함수입니다.이를 통해 컴파일러는 가장 일반적인 경우 실행하는 데 가장 적은 수의 명령어가 사용되도록 분기 명령어를 작성할 수 있습니다.

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

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top