Warum macht die Baumvektorisierung diesen Sortieralgorithmus 2x langsamer?

Lesezeit: 10 Minuten

Benutzer-Avatar
Ali

Der Sortieralgorithmus dieser Frage wird doppelt so schnell(!), wenn -fprofile-arcs ist in gcc (4.7.2) aktiviert. Der stark vereinfachte C-Code dieser Frage (es stellte sich heraus, dass ich das Array mit allen Nullen initialisieren kann, das seltsame Leistungsverhalten bleibt bestehen, aber es macht die Argumentation viel einfacher):

#include <time.h>
#include <stdio.h>

#define ELEMENTS 100000

int main() {
  int a[ELEMENTS] = { 0 };
  clock_t start = clock();
  for (int i = 0; i < ELEMENTS; ++i) {
    int lowerElementIndex = i;
    for (int j = i+1; j < ELEMENTS; ++j) {
      if (a[j] < a[lowerElementIndex]) {
        lowerElementIndex = j;
      }
    }
    int tmp = a[i];
    a[i] = a[lowerElementIndex];
    a[lowerElementIndex] = tmp;
  } 
  clock_t end = clock();
  float timeExec = (float)(end - start) / CLOCKS_PER_SEC;
  printf("Time: %2.3f\n", timeExec);
  printf("ignore this line %d\n", a[ELEMENTS-1]);
}

Nachdem ich lange mit den Optimierungsflags gespielt hatte, stellte sich heraus, dass dies der Fall war -ftree-vectorize ergibt auch dieses seltsame Verhalten, damit wir nehmen können -fprofile-arcs Außer Frage. Nach der Profilierung mit perf Das habe ich gefunden Der einzige relevante Unterschied ist:

Schneller Fall gcc -std=c99 -O2 simp.c (läuft in 3,1 s)

    cmpl    %esi, %ecx
    jge .L3
    movl    %ecx, %esi
    movslq  %edx, %rdi
.L3:

Langsamer Fall gcc -std=c99 -O2 -ftree-vectorize simp.c (läuft in 6,1 s)

    cmpl    %ecx, %esi
    cmovl   %edx, %edi
    cmovl   %esi, %ecx

Was den ersten Ausschnitt betrifft: Da das Array nur Nullen enthält, springen wir immer zu .L3. Es kann stark von der Verzweigungsvorhersage profitieren.

Ich vermute die cmovl Anweisungen können nicht von der Verzweigungsvorhersage profitieren.


Fragen:

  1. Sind alle meine obigen Vermutungen richtig? Macht das den Algorithmus langsam?

  2. Wenn ja, wie kann ich verhindern, dass gcc diese Anweisung ausgibt (außer der trivialen -fno-tree-vectorization Problemumgehung natürlich), aber immer noch so viele Optimierungen wie möglich?

  3. Was ist das -ftree-vectorization? Die Dokumentation ist ziemlich vage, ich bräuchte ein wenig mehr Erklärung, um zu verstehen, was passiert.


Aktualisieren: Da kam es in Kommentaren auf: Das komische Leistungsverhalten bzgl. der -ftree-vectorize Flag bleibt bei zufälligen Daten. Wie Yakk betont, ist es für die Auswahlsortierung tatsächlich schwierig, einen Datensatz zu erstellen, der zu vielen Fehlvorhersagen für Zweige führen würde.

Da es auch aufkam: Ich habe eine Core i5 CPU.


Basierend auf Yakks Kommentar habe ich einen Test erstellt. Der folgende Code (Online ohne Boost) ist natürlich kein Sortieralgorithmus mehr; Ich habe nur die innere Schleife herausgenommen. Sein einziges Ziel ist es, die Wirkung der Verzweigungsvorhersage zu untersuchen: Wir überspringen die if Filiale in der for Schleife mit Wahrscheinlichkeit p.

#include <algorithm>
#include <cstdio>
#include <random>
#include <boost/chrono.hpp>
using namespace std;
using namespace boost::chrono;
constexpr int ELEMENTS=1e+8; 
constexpr double p = 0.50;

int main() {
  printf("p = %.2f\n", p);
  int* a = new int[ELEMENTS];
  mt19937 mt(1759);
  bernoulli_distribution rnd(p);
  for (int i = 0 ; i < ELEMENTS; ++i){
    a[i] = rnd(mt)? i : -i;
  }
  auto start = high_resolution_clock::now();
  int lowerElementIndex = 0;
  for (int i=0; i<ELEMENTS; ++i) {
    if (a[i] < a[lowerElementIndex]) {
      lowerElementIndex = i;
    }
  } 
  auto finish = high_resolution_clock::now();
  printf("%ld  ms\n", duration_cast<milliseconds>(finish-start).count());
  printf("Ignore this line   %d\n", a[lowerElementIndex]);
  delete[] a;
}

Die Schleifen von Interesse:

Dies wird als bezeichnet cmov

g++ -std=c++11 -O2 -lboost_chrono -lboost_system -lrt branch3.cpp

    xorl    %eax, %eax
.L30:
    movl    (%rbx,%rbp,4), %edx
    cmpl    %edx, (%rbx,%rax,4)
    movslq  %eax, %rdx
    cmovl   %rdx, %rbp
    addq    $1, %rax
    cmpq    $100000000, %rax
    jne .L30

Dies wird als bezeichnet kein cmovdas -fno-if-conversion Flagge wurde von Turix in seiner Antwort darauf hingewiesen.

g++ -std=c++11 -O2 -fno-if-conversion -lboost_chrono -lboost_system -lrt branch3.cpp

    xorl    %eax, %eax
.L29:
    movl    (%rbx,%rbp,4), %edx
    cmpl    %edx, (%rbx,%rax,4)
    jge .L28
    movslq  %eax, %rbp
.L28:
    addq    $1, %rax
    cmpq    $100000000, %rax
    jne .L29

Der Unterschied nebeneinander

cmpl    %edx, (%rbx,%rax,4) |     cmpl  %edx, (%rbx,%rax,4)
movslq  %eax, %rdx          |     jge   .L28
cmovl   %rdx, %rbp          |     movslq    %eax, %rbp
                            | .L28:

Die Ausführungszeit als Funktion des Bernoulli-Parameters p

Effekt der Verzweigungsvorhersage

Der Code mit der cmov Unterricht ist absolut unempfindlich p. Der Code ohne das cmov Unterricht ist der Gewinner, wenn p<0.26 oder 0.81<p und ist höchstens 4,38x schneller (p=1). Die schlechtere Situation für den Verzweigungsprädiktor liegt natürlich bei ungefähr p=0.5 wobei der Code 1,58x langsamer ist als der Code mit der cmov Anweisung.

  • Erhalten Sie die gleichen Ergebnisse mit echten (zufälligen) Daten anstelle von Nullen?

    – PaulR

    10. Januar 2014 um 22:58 Uhr

  • Nur eine Vermutung, aber ich denke, der Baumteil von tree-vectorize bedeutet, dass es an der “Baum” -Form des Codes arbeitet (gcc konvertiert Ihren Code nach dem Parsen in eine Reihe verschiedener Formen, und Baum ist eine dieser Formen iirc ). Vektorisieren bezieht sich natürlich nur auf die Verwendung von Vektoranweisungen (SSE, MMX, 3D-Now usw.).

    – CrazyCasta

    10. Januar 2014 um 22:59 Uhr

  • @PaulR Ja, das tue ich. Andere berichteten dasselbe.

    – Ali

    10. Januar 2014 um 22:59 Uhr


  • Vollständig zufällige und gleichförmige Elemente in einer Auswahlsortierung verursachen nicht viele Verzweigungsvorhersagefehler. Das erste Element ist k_0 einheitlich von 0 bis N. Jedes Element danach hat ein a k_0/N Wahrscheinlichkeit, kleiner zu sein, was zu einer Verzweigung führt. Dieses Element wird dann einheitlich von 0 bis ausgewählt k_0-1. Stellen Sie sich vor, es würde immer den mittleren nehmen: Dann finden Sie nach einer logarithmischen Anzahl von Schritten den kleinsten übrig. Im Durchschnitt ist das die Leistung, die Sie erhalten. Auf einen 100000 Anordnung, lg ist 16was bedeutet, dass der Zweig anfangs 6000 Mal mehr in eine Richtung geht als in die andere.

    – Yakk – Adam Nevraumont

    11. Januar 2014 um 1:27 Uhr

  • Tatsächlich bedeutet Ihre ineffiziente Sortierung, dass jeder Vergleich wenig Entropie hat, also sehr vorhersehbar ist, sodass die Verzweigungsvorhersage nahezu perfekt ist. Jetzt können wir pathologische Daten arrangieren. Wir möchten, dass jeder Vergleich eine 50:50-Chance hat, größer oder kleiner als der aktuell niedrigste zu sein. Satz array[i] = +/- iwobei das Zeichen zufällig ausgewählt wird, und reprofilieren (alternierend könnte funktionieren, aber einen Verzweigungsprädiktor zu haben, der alternierend behandelt, wäre in der Hardware einfach, so dass zufällig böse genug ist).

    – Yakk – Adam Nevraumont

    11. Januar 2014 um 1:31 Uhr


Benutzer-Avatar
Turix

Hinweis: Beantwortet, bevor die Grafikaktualisierung zur Frage hinzugefügt wurde; Einige Assemblercode-Referenzen hier sind möglicherweise veraltet.

(Adaptiert und erweitert von unserem obigen Chat, der anregend genug war, um mich zu veranlassen, ein bisschen mehr zu recherchieren.)

Erstens (wie in unserem obigen Chat) scheint die Antwort auf Ihre erste Frage “Ja” zu sein. In dem “optimierten” Vektorcode ist die Optimierung, die sich (negativ) auf die Leistung auswirkt, eine Verzweigung prädikanungwährend im ursprünglichen Code die Leistung durch Verzweigung (positiv) beeinflusst wird Vorhersage. (Beachten Sie das zusätzliche ‘a‘ in der ehemaligen.)

Zu Ihrer 3. Frage: Obwohl in Ihrem Fall ab Schritt 11 (“Bedingte Ausführung”) eigentlich keine Vektorisierung durchgeführt wird hier Es scheint, dass einer der Schritte im Zusammenhang mit Vektorisierungsoptimierungen darin besteht, Bedingungen innerhalb gezielter Schleifen zu “glätten”, wie dieses Bit in Ihrer Schleife:

if (a[j] < a[lowerElementIndex]
    lowerElementIndex = j;

Anscheinend geschieht dies auch dann, wenn keine Vektorisierung vorhanden ist.

Dies erklärt, warum der Compiler die bedingten Bewegungsanweisungen verwendet (cmovl). Das Ziel dort ist vermeiden einen Zweig vollständig (im Gegensatz zu dem Versuch, dies zu tun vorhersagen es richtig). Stattdessen die beiden cmovl Anweisungen werden vor dem Ergebnis der vorherigen durch die Pipeline gesendet cmpl bekannt ist und das Vergleichsergebnis dann “weitergeleitet” wird, um die Bewegungen vor ihrem Zurückschreiben zu aktivieren/zu verhindern (dh bevor sie tatsächlich wirksam werden).

Beachten Sie, dass es sich gelohnt hätte, wenn die Schleife vektorisiert worden wäre, bis zu dem Punkt zu gelangen, an dem mehrere Iterationen durch die Schleife effektiv parallel ausgeführt werden könnten.

In Ihrem Fall schlägt der Optimierungsversuch jedoch tatsächlich fehl, da in der abgeflachten Schleife die beiden bedingten Bewegungen jedes Mal durch die Schleife durch die Pipeline gesendet werden. Dies ist an sich vielleicht auch nicht so schlimm, außer dass es eine RAW-Datengefahr gibt, die den zweiten Zug verursacht (cmovl %esi, %ecx) warten müssen, bis der Array-/Speicherzugriff (movl (%rsp,%rsi,4), %esi) abgeschlossen ist, auch wenn das Ergebnis letztendlich ignoriert wird. Daher die enorme Zeit, die für dieses spezielle aufgewendet wurde cmovl. (Ich würde erwarten, dass dies ein Problem ist, da Ihr Prozessor nicht über eine ausreichend komplexe Logik in seiner Prädikations-/Weiterleitungsimplementierung verfügt, um mit der Gefahr fertig zu werden.)

Auf der anderen Seite, im nicht optimierten Fall, wie Sie richtig herausgefunden haben, verzweigen Sie Vorhersage kann helfen, dort nicht auf das Ergebnis des entsprechenden Array-/Speicherzugriffs warten zu müssen (die movl (%rsp,%rcx,4), %ecx Anweisung). Wenn der Prozessor in diesem Fall eine genommene Verzweigung korrekt vorhersagt (was für ein All-0-Array jedes Mal der Fall sein wird, aber [even] in einem zufälligen Array sollte [still] sein grobmehr als [edited per @Yakk’s comment] die Hälfte der Zeit), muss er nicht warten, bis der Speicherzugriff beendet ist, um fortzufahren und die nächsten paar Befehle in der Schleife einzureihen. Bei korrekten Vorhersagen erhalten Sie also einen Schub, während bei falschen Vorhersagen das Ergebnis nicht schlechter ist als im “optimierten” Fall und außerdem besser, da manchmal vermieden werden kann, dass die 2 “verschwendet” wird. cmovl Anweisungen in der Pipeline.

[The following was removed due to my mistaken assumption about your processor per your comment.]
Zurück zu Ihren Fragen, ich würde vorschlagen, sich diesen Link oben anzusehen, um mehr über die für die Vektorisierung relevanten Flags zu erfahren, aber am Ende bin ich mir ziemlich sicher, dass es in Ordnung ist, diese Optimierung zu ignorieren, da Ihr Celeron nicht in der Lage ist, sie zu verwenden (in diesem Zusammenhang) sowieso.

[Added after above was removed]

Zu deiner zweiten Frage (“… wie kann ich verhindern, dass gcc diese Anweisung ausgibt …“), könnten Sie das versuchen -fno-if-conversion und -fno-if-conversion2 Flags (nicht sicher, ob diese immer funktionieren – sie funktionieren auf meinem Mac nicht mehr), obwohl ich nicht glaube, dass Ihr Problem bei den liegt cmovl Unterricht im Allgemeinen (dh ich würde nicht stets Verwenden Sie diese Flags), nur mit ihrer Verwendung in diesem speziellen Kontext (wo die Verzweigungsvorhersage angesichts des Standpunkts von @Yakk zu Ihrem Sortieralgorithmus sehr hilfreich sein wird).

  • Vielen Dank! Jetzt noch eine Frage: Was lässt Sie glauben, dass ich einen Celeron habe? Ich habe einen Core i5. Wie ändert das Ihre Antwort?

    – Ali

    11. Januar 2014 um 1:07 Uhr


  • @Ali Entschuldigung für die schlechte Annahme. Ich ging von diesem Bit in die andere Frage über: “Ich habe vergessen zu erwähnen, dass ich auf einem alten PC mit einem Intel Celeron at2.8GHz-Prozessor und Linux (Fedora 20 mit xfce) installiert bin.” die ich fälschlicherweise für deine hielt, aber jetzt sehe ich, dass du sie gerade bearbeitet hast. Es tut uns leid! Und obwohl ich mir nicht sicher bin, denke ich das sollte ändere meine Antwort, da ich glaube, dass dein i5 die Vektorisierung unterstützen könnte. (Ich werde das jetzt recherchieren und meine Antwort aktualisieren.)

    – Turix

    11. Januar 2014 um 1:34 Uhr

  • Überhaupt kein Problem. Vielen Dank für Ihre Bemühungen, ich weiß das sehr zu schätzen. Ich habe versucht und -fno-if-conversion hilft aber nur wenig (7,4s reduziert auf 6,4s). -fno-tree-vectorize gewinnt trotzdem (4,7 s). Getestet an einem bösartigen Datensatz, der sein Bestes tut, um die Verzweigungsvorhersage zu brechen. Überprüfen Sie jetzt bitte meine Antwort auf Yakks Kommentare.

    – Ali

    11. Januar 2014 um 17:12 Uhr

  • @ Ali Interessant. Ich wäre daran interessiert, den Rest des Assembler-Codes für die s_sort-Funktion (dh die “äußere Schleife”) und das vollständige Profil der Funktion zu sehen. Ich frage mich, ob der Versuch, die “innere Schleife” zu vektorisieren, die offensichtliche Optimierung der äußeren Schleife vermasselt. (Ich kann mir ein paar Möglichkeiten vorstellen, wie dies passieren könnte, einschließlich zum Beispiel einer weiteren unnötigen Suche nach a[i].)

    – Turix

    11. Januar 2014 um 19:05 Uhr

  • @Ali auch, ich nehme an, Sie haben sich den Assembler-Code in der angesehen -fno-if-conversion Fall und es schien vernünftig. Ich war davon ausgegangen, dass es fast identisch mit dem sein würde -fno-tree-vectorize Daher interessiert mich auch, was die Hauptunterschiede waren, die vom Versuch der Vektorisierung übrig geblieben sind.

    – Turix

    11. Januar 2014 um 19:07 Uhr

1229510cookie-checkWarum macht die Baumvektorisierung diesen Sortieralgorithmus 2x langsamer?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy