GNU/Linux >> Linux Esercitazione >  >> Linux

Qual è la differenza tra chiamate probabili e improbabili nel kernel?

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.


Linux
  1. Qual è la differenza tra InnoDB e MyISAM?

  2. Qual è la differenza tra strtok_r e strtok_s in C?

  3. Qual è la differenza tra fsck ed e2fsck?

  4. Qual è la differenza tra adduser e useradd?

  5. Qual è la differenza tra ls e l?

Qual ​​è la differenza tra i kernel macOS e Linux

Qual è la differenza tra Linux e Unix?

Che cos'è un hypervisor? Qual è la differenza tra il tipo 1 e 2?

Qual è la differenza tra curl e Wget?

Qual è la differenza tra unlink e rm?

Qual è la differenza tra route e ip route?