Wie die wahrscheinlich / unwahrscheinlich Makros in der Kernel-Arbeit Linux und was ist ihr Nutzen?

StackOverflow https://stackoverflow.com/questions/109710

Frage

Ich habe durch einige Teile des Linux-Kernels zu graben und fand Anrufe wie folgt aus:

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öße Abnahme kann durch deren Nutzung zu erwarten? Und ist es die Mühe wert (und verliert die Portabilität wahrscheinlich) zumindest im Code Engpass (im Userspace, natürlich).

War es hilfreich?

Lösung

Sie sind Hinweis an den Compiler Anweisungen zu emittieren, die Verzweigungsvorhersage führen die „wahrscheinlich“ Seite eines Sprungbefehl zu begünstigen. Dies kann ein großer Gewinn sein, wenn die Vorhersage korrekt ist, bedeutet dies, dass der Sprungbefehl grundsätzlich frei ist und Null-Zyklen dauern. Auf der anderen Seite, wenn die Vorhersage falsch ist, dann bedeutet es, die Prozessor-Pipeline ausgespült werden muss, und es kann mehrere Zyklen kosten. Solange die Vorhersage der meiste Zeit richtig ist, dies wird dazu neigen, für die Leistung gut.

Wie alle solchen Leistung Optimierungen Sie es nur nach umfangreicher Profilierung tun sollten der Code ist wirklich in einem Engpass zu gewährleisten, und wahrscheinlich die Mikro Natur gegeben, dass sie in einer engen Schleife ausgeführt werden. Im Allgemeinen werden die Linux-Entwickler ziemlich erfahren, damit ich vorstellen, dass sie das getan hätte. Sie haben wirklich nicht zu viel kümmern sich um die Portabilität, da sie nur gcc gerichtet ist, und sie haben eine sehr enge Vorstellung von der Versammlung sie es will erzeugen.

Andere Tipps

Dies sind Makros, die Hinweise auf den Compiler geben, über die Art und Weise eine Filiale gehen. Die Makros erweitern spezifische Erweiterungen auf GCC, wenn sie verfügbar sind.

GCC verwendet diese für Verzweigungsvorhersage zu optimieren. Zum Beispiel, wenn Sie so etwas wie die folgenden haben

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

return x;

Dann kann es diesen Code neu strukturieren zu sein, etwas mehr wie:

if (!x) {
  return x;
}

dosomething();
return x;

Der Vorteil davon ist, dass, wenn der Prozessor einen Zweig die erste Zeit in Anspruch nimmt, gibt erheblichen Aufwand, weil es spekulativ worden seinen Laden und Ausführen von Code weiter voran. Wenn es bestimmt wird es den Zweig nehmen, dann muss es das ungültig, und in dem Zweigziel starten.

Die meisten modernen Prozessoren jetzt irgendeine Art von Verzweigungsvorhersage, aber das nur unterstützt, wenn Sie schon vor der Verzweigung durchgemacht, und der Zweig noch in der Verzweigungsvorhersage-Cache.

Es gibt eine Reihe von anderen Strategien, die der Compiler und Prozessor in diesen Szenarien verwenden können. Sie können weitere Informationen, wie Zweig Prädiktoren Arbeit bei Wikipedia finden: http://en.wikipedia.org/wiki / Branch_predictor

Lassen Sie uns decompile zu sehen, was GCC 4.8 tut damit

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 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

Der Befehl, um im Speicher war unverändert: zuerst die printf und dann puts und die retq Rückkehr

.

Mit __builtin_expect

Jetzt ersetzen if (i) mit:

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>

Die printf (kompiliert __printf_chk) wurde bis zum Ende der Funktion bewegt, nach puts und der Rückkehr Verzweigungsvorhersage zu verbessern, wie sie von anderen Antworten erwähnt.

Es ist also im Grunde das gleiche 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 getan.

Aber viel Glück auf ein Beispiel zu schreiben, die mit __builtin_expect läuft schneller als ohne, CPUs sind wirklich jene Tage smart. Meine naiven Versuche sind hier .

Sie bewirken, dass der Compiler die entsprechenden Zweig Hinweise emittieren, wo die Hardware unterstützt werden. Diese Regel bedeutet nur ein paar Bits in dem Befehlsopcode twiddling, so Codegröße wird sich nicht ändern. Die CPU startet Anweisungen von der vorhergesagten Position zu holen, und die Pipeline spülen und neu beginnen, wenn das falsch erweist, wenn die Verzweigung erreicht ist; in dem Fall, in dem der Hinweis korrekt ist, wird dies den Zweig viel schneller machen - genau, wie viel schneller auf der Hardware abhängen wird; und wie viel dies die Leistung des Codes beeinflusst wird davon abhängen, wie hoch der Anteil der Zeit Hinweis korrekt ist.

Zum Beispiel auf einem PowerPC-CPU ein nicht anpassbare Zweig 16 Zyklen dauern kann, ein angedeutet richtig eine 8 und eine angedeutet falsch ein 24 innersten gut Hinting Schleifen kann einen enormen Unterschied machen.

Portabilität ist nicht wirklich ein Problem - vermutlich ist die Definition in einem Pro-Plattform-Header; Sie können einfach „wahrscheinlich“ und „unwahrscheinlich“ nichts für Plattformen definieren, die nicht statisch Zweig Hinweise unterstützen.

long __builtin_expect(long EXP, long C);

Dieses Konstrukt weist den Compiler an, dass der Ausdruck EXP höchstwahrscheinlich den Wert C. Der Rückgabewert EXP haben. __ builtin_expect soll in einer Bedingung verwendet werden Ausdruck. In fast allen Fällen wird es in die verwendet werden, Kontext von Booleschen Ausdrücken in diesem Fall ist es viel bequeme zwei Helfer Makros 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

(allgemeine Bemerkung - andere Antworten auf Details)

Es gibt keinen Grund, dass Sie Portabilität durch ihre Verwendung verlieren sollten.

Sie haben immer die Möglichkeit, einen einfachen Null-Effekt „inline“ oder Makro erstellen, die Sie erlaubt, auf anderen Plattformen mit anderen Compilern zu kompilieren.

Sie werden nicht nur den Vorteil der Optimierung erhalten, wenn Sie auf anderen Plattformen sind.

Gemäß dem Kommentar von Cody , hat dies nichts mit Linux zu tun, sondern ist ein Hinweis auf die Compiler. Was passiert auf der Architektur und Compiler-Version abhängen.

Dieses besondere Merkmal in Linux ist etwas falsch verwendet in drivers. Wie osgx weist darauf hin, in Semantik von hot Attribut jede hot oder cold Funktion mit in einem Block kann automatisch aufgerufen andeuten, dass der Zustand wahrscheinlich ist oder nicht. Zum Beispiel ist dump_stack() deutlicher cold so ist dies überflüssig,

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

Zukünftige Versionen von gcc kann wahlweise eine Funktion auf diese Hinweise basieren Inline. Es gibt auch Vorschläge, dass es nicht boolean, sondern eine Partitur, wie in wahrscheinlich , usw. Im Allgemeinen sollte es bevorzugt sein, einen alternativen Mechanismus wie cold zu verwenden. Es gibt keinen Grund, es in jedem Ort zu verwenden, aber heiß Pfade. Was ist ein Compiler auf einer Architektur tun kann auf einem anderen ganz anders sein.

In vielen Linux-Version können Sie in / usr / linux / finden complier.h, können Sie es für den Einsatz einfach aufnehmen können. Und eine andere Meinung nach unwahrscheinlich () ist nützlicher, eher als wahrscheinlich (), weil

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

kann es in vielen Compiler als auch optimiert werden.

Und übrigens, wenn Sie das Detail Verhalten des Codes beobachten möchten, können Sie einfach wie folgt tun:

  

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

Dann öffnen obj.s, können Sie die Antwort finden.

Sie sind Hinweise an den Compiler den Hinweis Präfixe auf Zweigen zu erzeugen. Auf x86 / x64, nehmen sie ein Byte, so dass Sie höchstens eine Ein-Byte-Erhöhung für jeden Zweig erhalten werden. Was der Leistung, es hängt ganz von der Anwendung -. In den meisten Fällen ignoriert der Verzweigungsprädiktor auf dem Prozessor sie, in diesen Tagen

Edit: Vergessen über einen Ort, den sie mit eigentlich wirklich helfen. Es kann der Compiler ermöglicht die Steuerung-Flußgraphen neu zu ordnen, die Anzahl der Zweige für den ‚wahrscheinlich‘ Weg genommen zu reduzieren. Dies kann eine deutliche Verbesserung in Schleifen, in dem Sie mehr Exit-Fälle sind zu überprüfen.

Dies sind GCC Funktionen für die Programmierer einen Hinweis an den Compiler darüber zu geben, was die wahrscheinlichste Verzweigungsbedingung in einem bestimmten Ausdruck sein wird. Dadurch kann der Compiler die Verzweigungsbefehle bauen, damit der häufigste Fall die geringste Anzahl von Anweisungen ausführen auszuführen.

Wie die Verzweigungsbefehle gebaut werden, sind abhängig von der Prozessorarchitektur.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top