GNU/Linux >> Znalost Linux >  >> Linux

Jak fungují pravděpodobná/nepravděpodobná makra v linuxovém jádře a jaký je jejich přínos?

Jsou nápovědou kompilátoru, aby vyslal instrukce, které způsobí, že predikce větvení upřednostní "pravděpodobnou" stranu instrukce skoku. To může být velká výhra, pokud je předpověď správná, znamená to, že instrukce skoku je v podstatě zdarma a bude trvat nula cyklů. Na druhou stranu, pokud je předpověď špatná, znamená to, že je potřeba propláchnout potrubí procesoru a to může stát několik cyklů. Dokud je předpověď po většinu času správná, bude to pro výkon dobré.

Stejně jako všechny takové optimalizace výkonu byste to měli provést pouze po rozsáhlém profilování, abyste zajistili, že kód je skutečně v úzkém hrdle, a pravděpodobně vzhledem k mikropovaze, že je provozován v těsné smyčce. Obecně jsou vývojáři Linuxu dost zkušení, takže bych si představoval, že by to udělali. O přenositelnost se ve skutečnosti příliš nestarají, protože se zaměřují pouze na gcc a mají velmi blízkou představu o sestavě, kterou chtějí generovat.


Pojďme provést dekompilaci, abychom viděli, co s tím GCC 4.8 dělá

Bez __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;
}

Kompilace a dekompilace pomocí GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Výstup:

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

Pořadí instrukcí v paměti bylo nezměněno:nejprve printf a poté puts a retq návrat.

S __builtin_expect

Nyní nahraďte if (i) s:

if (__builtin_expect(i, 0))

a dostaneme:

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 (zkompilováno do __printf_chk ) byl přesunut na úplný konec funkce, za puts a návrat ke zlepšení predikce větvení, jak je zmíněno v jiných odpovědích.

Takže je to v podstatě stejné jako:

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

Tato optimalizace nebyla provedena s -O0 .

Ale hodně štěstí při psaní příkladu, který běží rychleji s __builtin_expect CPU jsou dnes opravdu chytré. Moje naivní pokusy jsou tady.

C++20 [[likely]] a [[unlikely]]

C++20 standardizovalo tyto vestavěné prvky C++:Jak použít atribut pravděpodobné/nepravděpodobné C++20 v příkazu if-else Pravděpodobně (slovní hříčka!) udělají totéž.


Jedná se o makra, která dávají kompilátoru rady, jakým směrem se může větev vydat. Makra se rozšiřují na konkrétní rozšíření GCC, pokud jsou k dispozici.

GCC je používá k optimalizaci pro predikci větví. Například, pokud máte něco jako následující

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

return x;

Pak může tento kód restrukturalizovat tak, aby byl něco podobného:

if (!x) {
  return x;
}

dosomething();
return x;

Výhodou toho je, že když procesor poprvé provede větev, vzniká značná režie, protože mohl spekulativně načítat a spouštět kód dále dopředu. Když se rozhodne, že bude mít větev, musí ji zrušit a začít u cíle větve.

Většina moderních procesorů má nyní nějaký druh predikce větvení, ale to pomáhá pouze tehdy, když jste větev prošli dříve a větev je stále v mezipaměti predikce větví.

Existuje řada dalších strategií, které může kompilátor a procesor v těchto scénářích použít. Další podrobnosti o tom, jak fungují prediktory větví, najdete na Wikipedii:http://en.wikipedia.org/wiki/Branch_predictor


Linux
  1. Co je příkaz Chown v Linuxu a jak jej používat

  2. Co je to Rolling Release Linux a jaký je skutečný přínos jeho používání

  3. Živé ladění linuxového jádra, jak se to dělá a jaké nástroje se používají?

  1. Co je Makefile a jak funguje?

  2. Jak ladit linuxové jádro pomocí GDB a QEMU?

  3. Jaká je výhoda kompilace vlastního linuxového jádra?

  1. Jaký je rozdíl mezi module_init a init_module v modulu jádra Linuxu?

  2. Jak interně funguje copy_from_user z jádra Linuxu?

  3. Jak je do linuxového jádra přidána nová hardwarová podpora?