it-swarm.com.de

Wie funktionieren die wahrscheinlichen / unwahrscheinlichen Makros im Linux-Kernel und was ist ihr Nutzen?

Ich habe einige Teile des Linux-Kernels durchgearbeitet und folgende Aufrufe gefunden:

if (unlikely(fd < 0))
{
    /* Do something */
}

oder

if (likely(!err))
{
    /* Do something */
}

Ich habe die Definition von ihnen gefunden:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

Ich weiß, dass sie für die Optimierung sind, aber wie funktionieren sie? Und wie viel Leistung/Größenreduzierung ist mit ihrer Verwendung zu erwarten? Und ist es den Aufwand wert (und die Portabilität wahrscheinlich zu verlieren), zumindest im Engpass-Code (natürlich im User-Bereich)?.

318
terminus

Sie sind ein Hinweis für den Compiler, Anweisungen auszugeben, die eine Verzweigungsvorhersage veranlassen, die "wahrscheinliche" Seite einer Sprunganweisung zu bevorzugen. Dies kann ein großer Gewinn sein. Wenn die Vorhersage korrekt ist, bedeutet dies, dass der Sprungbefehl im Grunde frei ist und Null Zyklen dauert. Wenn die Vorhersage jedoch falsch ist, muss die Prozessor-Pipeline geleert werden, und es können mehrere Zyklen entstehen. Solange die Vorhersage die meiste Zeit korrekt ist, ist dies in der Regel gut für die Leistung.

Wie bei allen derartigen Leistungsoptimierungen sollten Sie dies erst nach einer umfassenden Profilerstellung tun, um sicherzustellen, dass sich der Code tatsächlich in einem Engpass befindet und wahrscheinlich aufgrund der Tatsache, dass er in einer engen Schleife ausgeführt wird. Generell sind die Linux-Entwickler ziemlich erfahren, daher würde ich mir vorstellen, dass sie das getan hätten. Die Portabilität ist ihnen eigentlich egal, da sie nur auf gcc abzielen, und sie haben eine sehr genaue Vorstellung von der Versammlung, die sie generieren möchten.

297

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 Anweisungsreihenfolge im Speicher war unverändert: Zuerst wird printf und dann puts und dann retq zurückgegeben.

Mit __builtin_expect

Ersetzen Sie nun if (i) durch:

if (__builtin_expect(i, 0))

und wir bekommen:

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>

Das printf (kompiliert zu __printf_chk) Wurde nach puts und der Rückkehr zum Ende der Funktion verschoben, um die Verzweigungsvorhersage zu verbessern, wie in anderen Antworten erwähnt.

Es ist also im Grunde dasselbe wie:

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 schla . Meine naiven Versuche sind hier .

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

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 dieser Code so umstrukturiert werden, dass er ungefähr so ​​aussieht:

if (!x) {
  return x;
}

dosomething();
return x;

Dies hat den Vorteil, dass beim erstmaligen Ausführen einer Verzweigung durch den Prozessor ein erheblicher Overhead entsteht, da der Code möglicherweise spekulativ geladen und weiter ausgeführt wurde. Wenn es festlegt, dass es die Verzweigung übernehmen wird, muss es diese ungültig machen und am Verzweigungsziel beginnen.

Die meisten modernen Prozessoren verfügen jetzt über eine Art Verzweigungsvorhersage, die jedoch nur hilft, wenn Sie die Verzweigung bereits durchlaufen haben und die Verzweigung sich noch im Cache für die Verzweigungsvorhersage befindet.

Es gibt eine Reihe anderer Strategien, die der Compiler und der Prozessor in diesen Szenarien verwenden können. Weitere Informationen zur Funktionsweise von Branch Predictors finden Sie bei Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor

70
dvorak

Sie bewirken, dass der Compiler die entsprechenden Verzweigungshinweise ausgibt, sofern die Hardware sie unterstützt. Dies bedeutet normalerweise nur, dass ein paar Bits im Anweisungs-Opcode gedreht werden, sodass sich die Codegröße nicht ändert. Die CPU beginnt, Anweisungen von der vorhergesagten Stelle abzurufen, die Pipeline zu leeren und neu zu beginnen, wenn sich herausstellt, dass dies falsch ist, wenn die Verzweigung erreicht ist. In dem Fall, in dem der Hinweis richtig ist, wird die Verzweigung viel schneller - genau wie viel schneller wird von der Hardware abhängen; und wie sehr sich dies auf die Leistung des Codes auswirkt, hängt davon ab, welcher Anteil des Zeithinweises korrekt ist.

Auf einer PowerPC-CPU kann ein nicht gedrückter Zweig beispielsweise 16 Zyklen dauern, ein korrekt angedeuteter 8 und ein falsch angedeuteter 24. In den innersten Schleifen kann ein guter Hinweis einen enormen Unterschied ausmachen.

Portabilität ist eigentlich kein Problem - vermutlich liegt die Definition in einem plattformspezifischen Header. Sie können einfach "wahrscheinlich" und "unwahrscheinlich" für Plattformen definieren, die keine statischen Verzweigungshinweise unterstützen.

6
moonshadow
long __builtin_expect(long EXP, long C);

Dieses Konstrukt teilt dem Compiler mit, dass der Ausdruck EXP höchstwahrscheinlich den Wert C haben wird. Der Rückgabewert ist EXP. __ builtin_expect soll in einem bedingten Ausdruck verwendet werden. In fast allen Fällen wird es im Kontext von Booleschen Ausdrücken verwendet. In diesem Fall ist es wesentlich praktischer, zwei Hilfsmakros zu definieren:

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

Diese Makros können dann wie in verwendet werden

if (likely(a > 1))

Referenz: https://www.akkadia.org/drepper/cpumemory.pdf

5
Ashish Maurya

In vielen Linux-Versionen finden Sie complier.h unter/usr/linux /. Sie können es einfach zur Verwendung einbinden. Und eine andere Meinung, unwahrscheinlich () ist nützlicher als wahrscheinlich (), weil

if ( likely( ... ) ) {
     doSomething();
}

es kann auch in vielen Compilern optimiert werden.

Übrigens, wenn Sie das Detailverhalten des Codes beobachten möchten, können Sie einfach Folgendes tun:

gcc -c test.c objdump -d test.o> obj.s

Dann öffnen Sie obj.s, um die Antwort zu finden.

2
Finaldie

Gemäß dem Kommentar von Cody hat dies nichts mit Linux zu tun, sondern ist ein Hinweis für den Compiler. Was passiert, hängt von der Architektur und der Compilerversion ab.

Diese besondere Funktion in Linux wird in Treibern etwas missbraucht. Wie osgx in Semantik des heißen Attributs angibt, kann jede hot - oder cold -Funktion, die in einem Block aufgerufen wird, automatisch darauf hinweisen, dass die Bedingung erfüllt ist ist wahrscheinlich oder nicht. Beispielsweise ist dump_stack() mit cold markiert, sodass dies redundant ist.

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

Zukünftige Versionen von gcc können selektiv eine Funktion einbinden, die auf diesen Hinweisen basiert. Es hat auch Vorschläge gegeben, dass es nicht boolean ist, sondern eine Punktzahl wie in höchstwahrscheinlich usw. Im Allgemeinen sollte es bevorzugt werden um einen alternativen Mechanismus wie cold zu verwenden. Es gibt keinen Grund, es an einem anderen Ort als auf heißen Pfaden zu benutzen. Was ein Compiler auf einer Architektur macht, kann auf einer anderen völlig anders sein.

2
artless noise

(allgemeiner Kommentar - andere Antworten decken die Details ab)

Es gibt keinen Grund, warum Sie die Portabilität verlieren sollten, wenn Sie sie verwenden.

Sie haben immer die Möglichkeit, ein einfaches Inline- oder Makro mit Null-Effekt zu erstellen, mit dem Sie auf anderen Plattformen mit anderen Compilern kompilieren können.

Wenn Sie sich auf anderen Plattformen befinden, können Sie die Optimierung nicht nutzen.

2

Sie sind Hinweise für den Compiler, um die Hinweispräfixe für Zweige zu generieren. Bei x86/x64 belegen sie ein Byte, sodass Sie für jeden Zweig höchstens eine Erhöhung um ein Byte erhalten. Die Leistung hängt vollständig von der Anwendung ab. In den meisten Fällen werden sie heutzutage von der Verzweigungsvorhersage des Prozessors ignoriert.

Bearbeiten: Sie haben einen Ort vergessen, bei dem sie wirklich helfen können. Dadurch kann der Compiler das Kontrollflussdiagramm neu anordnen, um die Anzahl der für den wahrscheinlichen Pfad verwendeten Verzweigungen zu verringern. Dies kann zu einer deutlichen Verbesserung von Schleifen führen, in denen Sie mehrere Exit-Fälle prüfen.

1
Cody Brocious

Dies sind GCC-Funktionen, mit denen der Programmierer dem Compiler einen Hinweis darauf gibt, welche Verzweigungsbedingung in einem bestimmten Ausdruck am wahrscheinlichsten ist. Auf diese Weise kann der Compiler die Verzweigungsanweisungen so erstellen, dass der häufigste Fall die geringste Anzahl von Anweisungen zur Ausführung benötigt.

Wie die Verzweigungsbefehle aufgebaut werden, hängt von der Prozessorarchitektur ab.

1
dcgibbons