커널에서 가능한 호출과 불가능한 호출의 차이점은 무엇입니까? 커널 소스 코드를 검색하는 동안 다음 문장을 발견했습니다.
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
누군가 이것을 설명할 수 있나요?
답변1
이는 GCC의 컴파일러 힌트입니다. 조건문에서 분기를 수행할 수 있는지 여부를 컴파일러에 알리는 데 사용됩니다. 이는 컴파일러가 가장 일반적인 결과에 가장 적합한 방식으로 코드를 작성하는 데 도움이 됩니다.
이들은 다음과 같이 사용됩니다:
if (likely(some_condition)) {
// the compiler will try and make the code layout optimal for the case
// where some_condition is true, i.e. where this block is run
most_likely_action();
} else {
// this block is less frequently used
corner_case();
}
주의해서 사용해야 합니다(즉, 실제 분기 분석 결과를 기반으로 함). 잘못된 프롬프트는 성능을 저하시킵니다(분명히).
를 검색하면 코드를 최적화하는 방법에 대한 몇 가지 예를 쉽게 찾을 수 있습니다 GCC __builtin_expect
. 이 블로그 게시물gcc 최적화: __builtin_expect예를 들어 분해가 자세히 설명되어 있습니다.
수행할 수 있는 최적화 유형은 프로세서별로 다릅니다. 일반적인 아이디어는 코드가 모든 곳에서 분기/점프하지 않으면 프로세서가 일반적으로 코드를 더 빠르게 실행한다는 것입니다. 선형성이 높고 분기의 예측 가능성이 높을수록 실행 속도가 빨라집니다. (예를 들어 깊은 파이프라인이 있는 프로세서의 경우 특히 그렇습니다.)
따라서 컴파일러는 가능성이 가장 높은 분기에 점프가 포함되지 않도록 코드를 내보냅니다(예: 대상 CPU가 점프를 선호하는 경우).
답변2
디컴파일하여 GCC 4.8이 무엇을 하는지 살펴보겠습니다.
예상치 못한
#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
return 입니다 retq
.
기대를 가지고
이제 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는 정말 똑똑했어요.. 나는 순진하게 노력했다여기 있어요.
C++20 [[likely]]
및[[unlikely]]
C++20은 다음과 같은 C++ 내장 함수를 표준화했습니다.https://stackoverflow.com/questions/51797959/how-to-use-c20s-likely-unlikely-attribute-in-if-else-statement그들은 아마도 (말장난 의도!) 같은 일을 할 것입니다.