Warum die Vektorisierung der Schleife keine Leistungsverbesserung bringt

Lesezeit: 9 Minuten

Benutzeravatar von Pouya
Pouya

Ich untersuche die Auswirkung der Vektorisierung auf die Leistung des Programms. Dazu habe ich folgenden Code geschrieben:

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>

#define LEN 10000000

int main(){

    struct timeval stTime, endTime;

    double* a = (double*)malloc(LEN*sizeof(*a));
    double* b = (double*)malloc(LEN*sizeof(*b));
    double* c = (double*)malloc(LEN*sizeof(*c));

    int k;
    for(k = 0; k < LEN; k++){
        a[k] = rand();
        b[k] = rand();
    }

    gettimeofday(&stTime, NULL);

    for(k = 0; k < LEN; k++)
        c[k] = a[k] * b[k];

    gettimeofday(&endTime, NULL);

    FILE* fh = fopen("dump", "w");
    for(k = 0; k < LEN; k++)
        fprintf(fh, "c[%d] = %f\t", k, c[k]);
    fclose(fh);

    double timeE = (double)(endTime.tv_usec + endTime.tv_sec*1000000 - stTime.tv_usec - stTime.tv_sec*1000000);

    printf("Time elapsed: %f\n", timeE);

    return 0;
}

In diesem Code initialisiere und multipliziere ich einfach zwei Vektoren. Die Ergebnisse werden im Vektor gespeichert c. Was mich hauptsächlich interessiert, ist der Effekt der Vektorisierung der folgenden Schleife:

for(k = 0; k < LEN; k++)
    c[k] = a[k] * b[k];

Ich kompiliere den Code mit den folgenden zwei Befehlen:

1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2

Ich erwarte eine Leistungsverbesserung, da der zweite Befehl die Schleife erfolgreich vektorisiert. Meine Studien zeigen jedoch, dass es keine Leistungsverbesserung gibt, wenn die Schleife vektorisiert wird.

Vielleicht habe ich hier etwas übersehen, da ich mich mit dem Thema nicht so gut auskenne. Bitte lassen Sie mich wissen, wenn etwas mit meinem Code nicht stimmt.

Vielen Dank im Voraus für Ihre Hilfe.

PS: Ich verwende Mac OSX, daher müssen die Daten nicht ausgerichtet werden, da alle zugewiesenen Speicher 16-Byte-ausgerichtet sind.

Bearbeiten: Ich möchte mich zunächst bei Ihnen allen für Ihre Kommentare und Antworten bedanken. Ich habe über die von @Mystcial vorgeschlagene Antwort nachgedacht und es gibt einige weitere Punkte, die hier erwähnt werden sollten. Erstens, wie @Vinska erwähnte, c[k]=a[k]*b[k] dauert nicht nur einen Zyklus. Zusätzlich zum Inkrement des Schleifenindexes und des durchgeführten Vergleichs wird dies sichergestellt k ist kleiner als LEN, müssen noch andere Dinge getan werden, um die Operation durchzuführen. Wenn man sich den vom Compiler generierten Assemblercode ansieht, sieht man, dass eine einfache Multiplikation viel mehr als einen Zyklus benötigt. Die vektorisierte Version sieht so aus:

L_B1.9:                         # Preds L_B1.8
        movq      %r13, %rax                                    #25.5
        andq      $15, %rax                                     #25.5
        testl     %eax, %eax                                    #25.5
        je        L_B1.12       # Prob 50%                      #25.5
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.10:                        # Preds L_B1.9
        testb     $7, %al                                       #25.5
        jne       L_B1.32       # Prob 10%                      #25.5
                                # LOE rbx r12 r13 r14 r15
L_B1.11:                        # Preds L_B1.10
        movsd     (%r14), %xmm0                                 #26.16
        movl      $1, %eax                                      #25.5
        mulsd     (%r15), %xmm0                                 #26.23
        movsd     %xmm0, (%r13)                                 #26.9
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.12:                        # Preds L_B1.11 L_B1.9
        movl      %eax, %edx                                    #25.5
        movl      %eax, %eax                                    #26.23
        negl      %edx                                          #25.5
        andl      $1, %edx                                      #25.5
        negl      %edx                                          #25.5
        addl      $10000000, %edx                               #25.5
        lea       (%r15,%rax,8), %rcx                           #26.23
        testq     $15, %rcx                                     #25.5
        je        L_B1.16       # Prob 60%                      #25.5
                                # LOE rdx rbx r12 r13 r14 r15 eax
L_B1.13:                        # Preds L_B1.12
        movl      %eax, %eax                                    #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.14:                        # Preds L_B1.14 L_B1.13
        movups    (%r15,%rax,8), %xmm0                          #26.23
        movsd     (%r14,%rax,8), %xmm1                          #26.16
        movhpd    8(%r14,%rax,8), %xmm1                         #26.16
        mulpd     %xmm0, %xmm1                                  #26.23
        movntpd   %xmm1, (%r13,%rax,8)                          #26.9
        addq      $2, %rax                                      #25.5
        cmpq      %rdx, %rax                                    #25.5
        jb        L_B1.14       # Prob 99%                      #25.5
        jmp       L_B1.20       # Prob 100%                     #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.16:                        # Preds L_B1.12
        movl      %eax, %eax                                    #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.17:                        # Preds L_B1.17 L_B1.16
        movsd     (%r14,%rax,8), %xmm0                          #26.16
        movhpd    8(%r14,%rax,8), %xmm0                         #26.16
        mulpd     (%r15,%rax,8), %xmm0                          #26.23
        movntpd   %xmm0, (%r13,%rax,8)                          #26.9
        addq      $2, %rax                                      #25.5
        cmpq      %rdx, %rax                                    #25.5
        jb        L_B1.17       # Prob 99%                      #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.18:                        # Preds L_B1.17
        mfence                                                  #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.19:                        # Preds L_B1.18
        mfence                                                  #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.20:                        # Preds L_B1.14 L_B1.19 L_B1.32
        cmpq      $10000000, %rdx                               #25.5
        jae       L_B1.24       # Prob 0%                       #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.22:                        # Preds L_B1.20 L_B1.22
        movsd     (%r14,%rdx,8), %xmm0                          #26.16
        mulsd     (%r15,%rdx,8), %xmm0                          #26.23
        movsd     %xmm0, (%r13,%rdx,8)                          #26.9
        incq      %rdx                                          #25.5
        cmpq      $10000000, %rdx                               #25.5
        jb        L_B1.22       # Prob 99%                      #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.24:                        # Preds L_B1.22 L_B1.20

Und die nicht vektoisierte Version ist:

L_B1.9:                         # Preds L_B1.8
        xorl      %eax, %eax                                    #25.5
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.10:                        # Preds L_B1.10 L_B1.9
        lea       (%rax,%rax), %edx                             #26.9
        incl      %eax                                          #25.5
        cmpl      $5000000, %eax                                #25.5
        movsd     (%r15,%rdx,8), %xmm0                          #26.16
        movsd     8(%r15,%rdx,8), %xmm1                         #26.16
        mulsd     (%r13,%rdx,8), %xmm0                          #26.23
        mulsd     8(%r13,%rdx,8), %xmm1                         #26.23
        movsd     %xmm0, (%rbx,%rdx,8)                          #26.9
        movsd     %xmm1, 8(%rbx,%rdx,8)                         #26.9
        jb        L_B1.10       # Prob 99%                      #25.5
                                # LOE rbx r12 r13 r14 r15 eax

Außerdem lädt der Prozessor nicht nur 24 Bytes. Bei jedem Zugriff auf den Speicher wird eine volle Zeile (64 Bytes) geladen. Noch wichtiger, da der erforderliche Speicher für a, bund c zusammenhängend ist, würde der Prefetcher definitiv viel helfen und lädt die nächsten Blöcke im Voraus. Allerdings halte ich die von @Mystcial berechnete Speicherbandbreite für zu pessimistisch.

Darüber hinaus wird die Verwendung von SIMD zur Verbesserung der Programmleistung für eine sehr einfache Addition in erwähnt Leitfaden zur Intel-Vektorisierung. Daher scheint es, dass wir in der Lage sein sollten, eine Leistungsverbesserung für diese sehr einfache Schleife zu erzielen.

Edit2: Nochmals vielen Dank für Ihre Kommentare. Dank des Beispielcodes von @Mystcial habe ich endlich die Auswirkung von SIMD auf die Leistungsverbesserung gesehen. Das Problem war, wie Mystcial erwähnte, die Speicherbandbreite. Mit der Wahl kleiner Größe für a, bund c die in den L1-Cache passen, zeigt sich, dass SIMD helfen kann, die Performance deutlich zu verbessern. Hier sind die Ergebnisse, die ich erhalten habe:

icc -O2 -o TestSMIDNoVec -no-vec TestSMID2.c: 17.34 sec

icc -O2 -o TestSMIDVecNoUnroll -vec-report2 TestSMID2.c: 9.33 sec

Und das Aufrollen der Schleife verbessert die Leistung noch weiter:

icc -O2 -o TestSMIDVecUnroll -vec-report2 TestSMID2.c -unroll=8: 8.6sec

Außerdem sollte ich erwähnen, dass mein Prozessor nur einen Zyklus benötigt, um eine Iteration abzuschließen, wenn er mit kompiliert wird -O2.

PS: Mein Computer ist ein Macbook Pro Core i5 @2.5GHz (Dual Core)

  • Ich habe gerade meine Antwort aktualisiert, um zu beweisen, dass mein Prozessor 1 Iteration pro Zyklus ausführen kann, und um zu erklären, wie dies möglich ist.

    – Mystisch

    10. August 2013 um 20:16 Uhr

  • Ich hasse es wirklich, das anzusprechen, aber die Build-Befehle platzieren beide Versionen der ausführbaren Datei in derselben Datei. Es wäre viel klarer gewesen, wenn die beiden Versionen unterschiedliche Namen gehabt hätten.

    – Wallyk

    11. August 2013 um 3:07 Uhr

  • Sie sagen, dass es “keine Notwendigkeit zum Ausrichten” gibt, aber der generierte asm-Code prüft alle Ausrichtungsmöglichkeiten. Es gibt eine Schleife für Quellen, die nicht ausgerichtet sind, und eine Schleife, die verwendet wird mulpd mit einem Speicheroperanden. Aber auch die ausgerichtete Version verwendet das Seltsame movsd + movhpd Folge zum Laden 128b. Ich denke, das ist für c und a ausgerichtet, b nicht ausgerichtet (nach Skalar-Intro). Ich glaube, ich erinnere mich, dass ich gelesen habe, dass auf einigen älteren Architekturen eine 2-Insn-Sequenz manchmal schneller war als movupd. Die Nur-Ziel-ausgerichtete Version der Schleife verwendet movupd für eine Quelle und die 2 insn-Methode für die andere, /boggle.

    – Peter Cordes

    5. Juli 2015 um 2:51 Uhr


  • Welche Größe von LEN haben sie gewählt?

    – Manolete

    23. November 2018 um 18:44 Uhr

  • Danke für deine Antwort. Sie haben Recht. Ich komplizierte die Dinge und erlebte die Leistungsverbesserung.

    – Poya

    10. August 2013 um 7:11 Uhr

  • +1: Dies muss in einer FAQ enthalten sein oder zu einer “go to” -Antwort werden – ein großer Teil der Optimierungsfragen für Anfänger scheint in diese Kategorie zu fallen.

    – PaulR

    10. August 2013 um 7:12 Uhr

  • Was ist, wenn wir es mit -O0 kompilieren? Führt die CPU jede Iteration in einem Zyklus aus?

    – Poya

    10. August 2013 um 20:38 Uhr

  • @matmul Es funktioniert nur, wenn Sie Daten wiederverwenden. Wenn alles nur einmal angefasst wird, ist nicht viel zu machen.

    – Mystisch

    10. Juni 2014 um 16:51 Uhr

  • @Zboson Offensichtlich hängt es von der Maschine ab. Es ist unwahrscheinlich, dass Sie die volle Bandbreite auf einem einzelnen Thread auf einem Computer mit mehreren NUMA-Knoten erhalten. Auf Haswell-E ist der Speicher schnell genug, wo Sie möglicherweise mit nur einem einzigen Thread vektorisieren müssen, um die Bandbreite zu maximieren. Das heißt, es nimmt jedoch nichts vom Punkt ab. Der Code in dieser Frage wird früher oder später auf Bandbreitenprobleme stoßen.

    – Mystisch

    25. September 2015 um 13:51 Uhr

  • Along with the multiply instruction, the code of the loop has to execute several other instructions as well, including the conditional Ah, du hast uns das nicht gezeigt real Code. Das Hinzufügen von Bedingungen innerhalb einer Schleife wird die Verzweigungsvorhersage effektiv verfälschen. Übrigens sind die paar Prozent Gewinn, die Sie melden, zwecklos. Sie sind immer noch an die Busbandbreite gebunden. IMHO verursacht das manuelle Entrollen nur weniger Verzweigungsvorhersagefehler, da es weniger Iterationen gibt. Die L1-Lokalität ist im Grunde die gleiche.

    – Wildpässer

    10. August 2013 um 12:57 Uhr

  • @wildplasser definiere “echten Code”. Auch einige andere Dinge: Die Gesamtgröße der Daten beträgt 10.000.000 * 8 * 3 = 228 Megabyte. Auf meinen normalen Uhren beträgt meine theoretische Speicherbandbreite 29,8 GB/s. Dieser Teil des Codes läuft etwa 1,1 Sekunden, wenn ich meine CPU auf die niedrigste verfügbare Taktrate einstelle. In dieser Zeit kann er die gesamten Daten 131 Mal verschicken. Ich sehe also nicht, wo ein Speicherengpass auftreten würde. Auch eine “Speicherengpass”-Theorie würde nicht mit der Tatsache einhergehen, dass, wenn ich meinen CPU-Takt verdoppele, dieser Teil des Codes doppelt so schnell läuft, während das Verdoppeln des Speichertakts kaum etwas bewirkt.

    – librin.so.1

    10. August 2013 um 13:23 Uhr

  • @wildplasser Auch ein paar Prozent? Der Unterschied zwischen dem schnellsten nicht vektorisierten und dem schnellsten vektorisierten beträgt etwas mehr als 6,5 %. Das sieht vielleicht nicht nach viel aus, kann aber in größerem Maßstab sehr bedeutsam sein. Bei einem solchen Unterschied würde es zB bedeuten, 11 Stunden und 20 Minuten CPU-Zeit zu verbringen, anstatt 12 Stunden zu verbringen. Atemberaubende 40 Minuten. Kleine Dinge summieren sich, also ist es alles andere als “sinnlos”

    – librin.so.1

    10. August 2013 um 13:29 Uhr

  • Das Kopieren in den automatischen Speicher vermeidet/reduziert L2-Cache-Effekte, hier werden 30 % eingespart. Ich werde es als Antwort hinzufügen, da ich die Formatierung benötige.

    – Wildpässer

    10. August 2013 um 13:54 Uhr


  • WRT real code : Ich dachte zuerst, du wärst der OP. Es tut uns leid!

    – Wildpässer

    10. August 2013 um 14:26 Uhr


1393550cookie-checkWarum die Vektorisierung der Schleife keine Leistungsverbesserung bringt

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

Privacy policy