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

Was ist der Vorteil von GCCs __builtin_expect in if else-Anweisungen?

Stellen Sie sich den Assembler-Code vor, der generiert würde aus:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

Ich denke, es sollte so etwas wie:

sein
  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

Sie können sehen, dass die Anweisungen in einer solchen Reihenfolge angeordnet sind, dass der bar Groß-/Kleinschreibung steht vor dem foo Fall (im Gegensatz zum C-Code). Dadurch kann die CPU-Pipeline besser ausgelastet werden, da ein Sprung die bereits geholten Befehle zertrümmert.

Bevor der Sprung ausgeführt wird, werden die Anweisungen darunter (der bar Fall) werden in die Pipeline geschoben. Seit foo Fall ist unwahrscheinlich, auch Sprünge sind unwahrscheinlich, daher ist es unwahrscheinlich, dass die Pipeline zerstört wird.


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

Blagovest erwähnte die Verzweigungsinversion, um die Pipeline zu verbessern, aber machen aktuelle Compiler das wirklich? Finden wir es heraus!

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)
        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 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  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

Die Befehlsreihenfolge im Speicher blieb unverändert:zuerst die puts und dann 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 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

Die puts wurde an das Ende der Funktion verschoben, die retq zurück!

Der neue Code ist im Grunde derselbe wie:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

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.


Die Idee von __builtin_expect ist, dem Compiler mitzuteilen, dass der Ausdruck normalerweise zu c ausgewertet wird, damit der Compiler für diesen Fall optimieren kann.

Ich würde vermuten, dass jemand dachte, er sei schlau und würde die Dinge dadurch beschleunigen.

Leider, es sei denn, die Situation ist sehr gut verstanden (es ist wahrscheinlich, dass sie so etwas nicht getan haben), es könnte die Dinge noch schlimmer gemacht haben. Die Dokumentation sagt sogar:

Im Allgemeinen sollten Sie dafür lieber das tatsächliche Profilfeedback verwenden (-fprofile-arcs ), da Programmierer notorisch schlecht darin sind, die tatsächliche Leistung ihrer Programme vorherzusagen. Es gibt jedoch Anwendungen, bei denen diese Daten schwer zu erfassen sind.

Im Allgemeinen sollten Sie __builtin_expect nicht verwenden es sei denn:

  • Sie haben ein sehr reales Leistungsproblem
  • Sie haben die Algorithmen im System bereits entsprechend optimiert
  • Sie haben Leistungsdaten, die Ihre Behauptung untermauern, dass ein bestimmter Fall am wahrscheinlichsten ist

Linux
  1. Linux vs. Unix:Was ist der Unterschied?

  2. Was bedeutet POSIX?

  3. Was sind die standardmäßigen GCC-Include-Verzeichnisse?

  4. Was ist das Standardpasswort des Bildschirms?

  5. Was ist der Goldlinker?

Was ist die Shell unter Linux?

Was ist mit dem freien Speicherplatz passiert?

iptables vs. nftables:Was ist der Unterschied?

Was ist der Kill-Befehl in Linux?

Was ist die Logjam-Schwachstelle?

Was ist der debian-+ Benutzer?