GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Wie funktionieren die Wahrscheinlich/Unwahrscheinlich-Makros im Linux-Kernel und welchen Nutzen haben sie?

Sie weisen den Compiler darauf hin, Anweisungen auszugeben, die bewirken, dass die Verzweigungsvorhersage die "wahrscheinliche" Seite einer Sprunganweisung bevorzugt. Dies kann ein großer Gewinn sein, wenn die Vorhersage korrekt ist, bedeutet dies, dass der Sprungbefehl im Grunde kostenlos ist und null Zyklen dauert. Wenn die Vorhersage andererseits falsch ist, bedeutet dies, dass die Prozessorpipeline geleert werden muss, was mehrere Zyklen kosten kann. Solange die Vorhersage die meiste Zeit richtig ist, ist dies tendenziell gut für die Leistung.

Wie bei allen solchen Leistungsoptimierungen sollten Sie dies nur nach einer umfassenden Profilerstellung tun, um sicherzustellen, dass sich der Code wirklich in einem Engpass befindet, und wahrscheinlich aufgrund der Mikronatur, dass er in einer engen Schleife ausgeführt wird. Im Allgemeinen sind die Linux-Entwickler ziemlich erfahren, also würde ich mir vorstellen, dass sie das getan hätten. Sie kümmern sich nicht wirklich um Portabilität, da sie nur auf gcc abzielen, und sie haben eine sehr genaue Vorstellung von der Assembly, die sie generieren möchten.


Lassen Sie uns dekompilieren, um zu sehen, was GCC 4.8 damit macht

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

Kompilieren und dekompilieren Sie mit GCC 4.8.2 x86_64 Linux:

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

Ausgabe:

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

Die Befehlsreihenfolge im Speicher blieb unverändert:zuerst die printf und dann puts und die retq zurück.

Mit __builtin_expect

Ersetzen Sie nun if (i) mit:

if (__builtin_expect(i, 0))

und wir erhalten:

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>

Die printf (kompiliert zu __printf_chk ) wurde an das Ende der Funktion nach puts verschoben und die Rückkehr zur Verbesserung der Verzweigungsvorhersage, wie in anderen Antworten erwähnt.

Es ist also im Grunde dasselbe wie:

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

Diese Optimierung wurde nicht mit -O0 durchgeführt .

Aber viel Glück beim Schreiben eines Beispiels, das mit __builtin_expect schneller läuft als ohne, CPUs sind heutzutage wirklich schlau. Meine naiven Versuche sind hier.

C++20 [[likely]] und [[unlikely]]

C++20 hat diese eingebauten C++-Elemente standardisiert:How to use C++20's wahrscheinlich/unwahrscheinlich Attribut in if-else Statement Sie werden wahrscheinlich (ein Wortspiel!) dasselbe tun.


Dies sind Makros, die dem Compiler Hinweise geben, in welche Richtung eine Verzweigung gehen kann. Die Makros werden auf GCC-spezifische Erweiterungen erweitert, sofern verfügbar.

GCC verwendet diese, um die Verzweigungsvorhersage zu optimieren. Zum Beispiel, wenn Sie etwas wie das Folgende haben

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

return x;

Dann kann es diesen Code so umstrukturieren, dass er eher wie folgt aussieht:

if (!x) {
  return x;
}

dosomething();
return x;

Der Vorteil davon besteht darin, dass, wenn der Prozessor zum ersten Mal eine Verzweigung durchführt, ein erheblicher Overhead entsteht, da er Code möglicherweise weiter vorne spekulativ geladen und ausgeführt hat. Wenn es feststellt, dass es die Verzweigung nehmen wird, muss es diese ungültig machen und beim Verzweigungsziel beginnen.

Die meisten modernen Prozessoren verfügen jetzt über eine Art Verzweigungsvorhersage, aber das hilft nur, wenn Sie die Verzweigung zuvor durchlaufen haben und sich die Verzweigung noch im Verzweigungsvorhersage-Cache befindet.

Es gibt eine Reihe anderer Strategien, die der Compiler und der Prozessor in diesen Szenarios verwenden können. Weitere Einzelheiten zur Funktionsweise von Verzweigungsprädiktoren finden Sie auf Wikipedia:http://en.wikipedia.org/wiki/Branch_predictor


Linux
  1. Was ist ein Makefile und wie funktioniert es?

  2. Was ist der Unterschied zwischen module_init und init_module in einem Linux-Kernel-Modul?

  3. Wie debuggt man den Linux-Kernel mit GDB und QEMU?

  4. Wie wird dem Linux-Kernel Unterstützung für neue Hardware hinzugefügt?

  5. Was bewirken die Kernel-Parameter acpi_osi=linux und acpi_backlight=vendor?

Linux-Tail-Befehl:Was es ist und wie man es verwendet

So überprüfen Sie die Kernel-Version in Linux

Was ist der Unterschied zwischen macOS- und Linux-Kernels

Linux Kernel 5.9:Was ist neu und wie man aktualisiert

Was ist Source Command in Linux und wie funktioniert es?

Was ist der Grep-Befehl unter Linux? Warum wird es verwendet und wie funktioniert es?