Sono suggerimenti per il compilatore per GCC. Sono usati nei condizionali per dire al compilatore se è probabile che un ramo venga preso o meno. Può aiutare il compilatore a definire il codice in modo ottimale per il risultato più frequente.
Sono usati in questo modo:
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();
}
Dovrebbe essere usato con molta attenzione (cioè basato sui risultati effettivi della profilazione della filiale). Un suggerimento sbagliato può degradare le prestazioni (ovviamente).
Alcuni esempi di come il codice può essere ottimizzato si trovano facilmente cercando GCC __builtin_expect
. Questo post di blog sull'ottimizzazione di gcc:__builtin_expect, ad esempio, descrive in dettaglio un disassemblaggio con esso.
Il tipo di ottimizzazioni che è possibile eseguire è molto specifico del processore. L'idea generale è che spesso i processori eseguiranno il codice più velocemente se non si ramifica/salta dappertutto. Più è lineare e più prevedibili sono i rami, più velocemente funzionerà. (Ciò è particolarmente vero per i processori con pipeline profonde, ad esempio.)
Quindi il compilatore emetterà il codice in modo tale che il ramo più probabile non comporti un salto se questo è ciò che preferisce la CPU di destinazione, per esempio.
Decompiliamo per vedere cosa ci fa GCC 4.8
Senza aspettarti
#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;
}
Compila e decompila con GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Uscita:
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
L'ordine delle istruzioni in memoria era invariato:prima il printf
e poi puts
e il retq
ritorno.
Con attesa
Ora sostituisci if (i)
con:
if (__builtin_expect(i, 0))
e otteniamo:
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>
Il printf
(compilato in __printf_chk
) è stato spostato alla fine della funzione, dopo puts
e il ritorno per migliorare la previsione del ramo come menzionato da altre risposte.
Quindi è praticamente uguale a:
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
Questa ottimizzazione non è stata eseguita con -O0
.
Ma buona fortuna per la scrittura di un esempio che funzioni più velocemente con __builtin_expect
che senza, le CPU sono davvero intelligenti in quei giorni. I miei ingenui tentativi sono qui.
C++20 [[likely]]
e [[unlikely]]
C++20 ha standardizzato questi built-in C++:https://stackoverflow.com/questions/51797959/how-to-use-c20s-likely-unlikely-attribute-in-if-else-statement Probabilmente (un gioco di parole!) fare la stessa cosa.