Programming

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

procodes 2020. 3. 6. 07:53
반응형

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

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

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


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

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

대부분의 최신 프로세서에는 이제 일종의 분기 예측이 있지만 이전에 분기를 겪었을 때만 지원하며 분기는 여전히 분기 예측 캐시에 있습니다.

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


컴파일러가 하드웨어에서 지원하는 적절한 분기 힌트를 생성하도록합니다. 이것은 일반적으로 명령 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


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

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

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

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


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. 고온 경로 이외의 장소에서 사용할 이유가 없습니다. 컴파일러가 한 아키텍처에서 수행하는 작업은 다른 아키텍처와 완전히 다를 수 있습니다.


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

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

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

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

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

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


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

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


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

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

참고 : https://stackoverflow.com/questions/109710/how-do-the-likely-unlikely-macros-in-the-linux-kernel-work-and-what-is-their-ben

반응형