Leistung der C-Code-Schleife

Lesezeit: 6 Minuten

Benutzeravatar von Ricky
Ricky

Ich habe einen Multiply-Add-Kernel in meiner Anwendung und möchte seine Leistung erhöhen.

Ich verwende einen Intel Core i7-960 (3,2 GHz Takt) und habe den Kernel bereits per SSE Intrinsic wie folgt manuell implementiert:

 for(int i=0; i<iterations; i+=4) {
    y1 = _mm_set_ss(output[i]);
    y2 = _mm_set_ss(output[i+1]);
    y3 = _mm_set_ss(output[i+2]);
    y4 = _mm_set_ss(output[i+3]);

    for(k=0; k<ksize; k++){
        for(l=0; l<ksize; l++){
            w  = _mm_set_ss(weight[i+k+l]);

            x1 = _mm_set_ss(input[i+k+l]);
            y1 = _mm_add_ss(y1,_mm_mul_ss(w,x1));
            …
            x4 = _mm_set_ss(input[i+k+l+3]);
            y4 = _mm_add_ss(y4,_mm_mul_ss(w,x4));
        }
    }
    _mm_store_ss(&output[i],y1);
    _mm_store_ss(&output[i+1],y2);
    _mm_store_ss(&output[i+2],y3);
    _mm_store_ss(&output[i+3],y4);
 }

Ich weiß, dass ich gepackte fp-Vektoren verwenden kann, um die Leistung zu steigern, und ich habe dies bereits erfolgreich getan, aber ich möchte wissen, warum der einzelne skalare Code die Spitzenleistung des Prozessors nicht erreichen kann.

Die Leistung dieses Kernels auf meiner Maschine beträgt ~1,6 FP-Operationen pro Zyklus, während das Maximum 2 FP-Operationen pro Zyklus wären (da FP add + FP mul parallel ausgeführt werden können).

Wenn ich beim Studium des generierten Assemblercodes richtig liege, würde der ideale Zeitplan wie folgt aussehen, wobei die mov Der Befehl dauert 3 Zyklen, die Umschaltlatenz vom Ladebereich zum FP-Bereich für die abhängigen Befehle dauert 2 Zyklen, die FP-Multiplikation dauert 4 Zyklen und die FP-Addition dauert 3 Zyklen. (Beachten Sie, dass die Abhängigkeit von Multiplizieren -> Addieren keine Switch-Latenz verursacht, da die Operationen zur selben Domäne gehören).

zeitlicher Ablauf

Entsprechend der gemessenen Performance (~80% der maximalen theoretischen Performance) ergibt sich ein Overhead von ~3 Instruktionen pro 8 Zyklen.

Ich versuche es entweder:

  • Befreien Sie sich von diesem Overhead, oder
  • erklären, woher es kommt

Natürlich gibt es das Problem mit Cache-Fehlern und Datenfehlausrichtungen, die die Latenz der Bewegungsanweisungen erhöhen können, aber gibt es noch andere Faktoren, die hier eine Rolle spielen könnten? Wie Registerlesestände oder so etwas?

Ich hoffe, mein Problem ist klar, danke im Voraus für Ihre Antworten!


Update: Die Montage der Innenschleife sieht wie folgt aus:

...
Block 21: 
  movssl  (%rsi,%rdi,4), %xmm4 
  movssl  (%rcx,%rdi,4), %xmm0 
  movssl  0x4(%rcx,%rdi,4), %xmm1 
  movssl  0x8(%rcx,%rdi,4), %xmm2 
  movssl  0xc(%rcx,%rdi,4), %xmm3 
  inc %rdi 
  mulss %xmm4, %xmm0 
  cmp $0x32, %rdi 
  mulss %xmm4, %xmm1 
  mulss %xmm4, %xmm2 
  mulss %xmm3, %xmm4 
  addss %xmm0, %xmm5 
  addss %xmm1, %xmm6 
  addss %xmm2, %xmm7 
  addss %xmm4, %xmm8 
  jl 0x401b52 <Block 21> 
...

  • Es hängt wirklich viel vom Compiler (sogar von seiner Version) und den Optimierungsflags ab, die Sie ihm übergeben. Wenn Ihnen die numerische Leistung so wichtig ist, investieren Sie möglicherweise auch Ihre Zeit und Mühe in das Erlernen numerischer Bibliotheken und/oder OpenCL oder CUDA (um die Vorteile von GPGPU zu nutzen). Es gibt auch Überlegungen zum Cache. Das Vorhersagen der tatsächlichen Zeit einer Schleife ist bei gegenwärtigen Prozessoren schwierig.

    – Basile Starynkevitch

    3. April 2012 um 11:13 Uhr


  • Ich verstehe nicht, warum Sie denken würden, dass die Schleifensteuerung immer parallel erfolgen kann, während sie tatsächlich eine perfekte Abhängigkeitskette im Out-of-Order-Ausführungsschema erzeugt. Der INC-Befehl modifiziert ein Register. Der CMP-Befehl muss warten, bis INC beendet ist, um den Wert in diesem Register zu prüfen und die Flags entsprechend zu modifizieren. Dann muss der bedingte Sprungbefehl darauf warten, dass CMP die Flags schreibt, um zu entscheiden, ob tatsächlich gesprungen werden soll oder nicht. Da gibt es keine Parallelisierung, fürchte ich. Ganz zu schweigen davon, dass Sprünge Pipeline-Stalls verursachen – der Verzweigungsprädiktor kümmert sich darum.

    – Daniel Kamil Kozar

    3. April 2012 um 11:13 Uhr

  • Ganz zu schweigen davon, dass der INC-Befehl auf den vorherigen Befehl warten muss, der die Flags modifiziert hat, um den Zustand des CF-Flags zu bewahren. Abhilfe schaffen Sie einfach, indem Sie das INC durch das entsprechende ADD ersetzen.

    – Daniel Kamil Kozar

    3. April 2012 um 11:18 Uhr

  • Kannst du den Rohbau posten?

    – Harald

    3. April 2012 um 12:14 Uhr

  • @OrgnlDave: also? Sie werden alle ~4 ms unterbrochen und führen Kernelcode aus, der im schlimmsten Fall einige µs dauert. Dieser Overhead liegt weit unter 20 %, ich wäre überrascht, wenn es tatsächlich > 1 % wäre.

    – ninjalj

    3. April 2012 um 18:43 Uhr

  • Ich werde auch dieses ganzzahlige SSE-Register-zu-Register erwähnen movs haben einen Durchsatz von 3 Inst/Zyklus, aber das ist irrelevant. Alle Lasten/Speicher sind immer noch 1 Inst/Zyklus.

    – Mystisch

    3. April 2012 um 17:04 Uhr

  • Wie können Sie das auf einem Multitasking-System sagen? Ernsthaft? 80% theoretischer Durchsatz mit dem Desktop-Scheduler von Linux und dem damit verbundenen Kontextwechsel … Ich würde wirklich gerne sehen, ob er die Schleife um 1 Anweisung reduzieren und eine bessere Geschwindigkeit erzielen könnte (mit einem unvollständigen Kernel).

    – std”OrgnlDave

    3. April 2012 um 18:00 Uhr

  • @OrgnlDave OS/Kernel-Overhead ist normalerweise geringer als Sie denken. Aus meiner Erfahrung ist es vernachlässigbar (< 1%). In dieser Frage finden Sie Beispiele für Code, der sowohl unter Windows als auch unter Linux mehr als 97 % der Spitzenflops erreicht.

    – Mystisch

    3. April 2012 um 18:17 Uhr

  • OK, ich gebe Ihnen zu, dass es normalerweise vernachlässigbar ist. Aber die Kosten für den Kontextwechsel sind hoch, das ist eine ehrliche Frage – wie viele Kontextfenster hat Nehalem? Die einzige Möglichkeit, wie ich diese sich nähernde Spitzenauslastung unabhängig vom Betriebssystem sehen kann, besteht darin, dass sie auf einem Kern feststeckt und meistens das einzige ist, was auf diesem Kern geplant ist. Was wahrscheinlich stimmt, wenn ich darüber nachdenke. Denken Sie auch daran, dass sich diese % der Zeit nicht auf die tatsächlichen % beziehen, sondern auf % der angegebenen Zeitscheiben

    – std”OrgnlDave

    3. April 2012 um 18:36 Uhr

  • Eigentlich in der Frage, die ich verlinkt habe. Diese % werden aus Wandzeiten berechnet – buchstäblich durch Hochzählen der Anzahl der berechneten Flops und Teilen durch die insgesamt verstrichene Wandzeit.

    – Mystisch

    3. April 2012 um 19:56 Uhr

  • Ich denke, dies ist als separate Frage geeignet. Seit jetzt hast du ein neues Problem mit dem Shuffle. (was ich im Moment nicht sehe) Sie können es mit diesem verlinken und angeben, dass es eine Fortsetzung ist.

    – Mystisch

    4. April 2012 um 7:58 Uhr


  • Einfach herauszufinden. Stellen Sie sicher, dass der Gewichtsvektor keine denormalisierten Werte enthält. Probieren Sie die Schleife ohne die Shuffle-Anweisung aus. Es wird keine brauchbaren Ergebnisse liefern, aber vielleicht finden Sie heraus, welche Anweisung Sie zusätzliche Zyklen kostet (ich vermute natürlich das Mischen).

    – Günther Piez

    4. April 2012 um 8:23 Uhr


  • @drhirsch Die neue Frage ist hier: stackoverflow.com/questions/10007243/… Also reposte deinen Kommentar dort.

    – Mystisch

    4. April 2012 um 8:33 Uhr

  • Entschuldigung, aber das ist Unsinn. Ich bin in der Lage, Prozessorzyklen für einfache Befehlssequenzen auf einem Linux-System zu messen, präemptiv und mit 1-kHz-Scheduler. Selbst wenn X läuft, liegt der Overhead des Systems normalerweise weit unter 1 %. Außerdem wäre es ein sehr unwahrscheinlicher Zufall, wenn die Zykluszahl in den OP-Fragen wegen des Overheads von 4 auf genau 5 geht – die natürlichere Erklärung ist, dass die Schleife tatsächlich 5 Zyklen benötigt.

    – Günther Piez

    3. April 2012 um 23:23 Uhr


  • @drhirsch Ich wette, du hast zwei Kerne. Dies wurde in den Kommentaren zu einer anderen Frage angesprochen. Ich werde dies bearbeiten, um dies widerzuspiegeln.

    – std”OrgnlDave

    3. April 2012 um 23:27 Uhr

  • Ändert nichts. Ich kann immer noch dieselben Messungen durchführen, während ich n Instanzen des Testprogramms ausführe, wobei n die Anzahl der Kerne ist.

    – Günther Piez

    3. April 2012 um 23:34 Uhr

  • @drhirsch Bitte tun Sie dies, ich hatte ein ähnliches Problem und es wäre sehr aufschlussreich für mich (da ich an der FALSCHEN Stelle gesucht habe, um das Problem zu lösen). Bitte verbinden Sie alle Ihre Kerne und messen Sie die Wanduhrzeit mit Läufen von mindestens 1 Sekunde Länge, wobei eine vollständige Desktop-Distribution ausgeführt wird.

    – std”OrgnlDave

    4. April 2012 um 0:26 Uhr

1402840cookie-checkLeistung der C-Code-Schleife

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

Privacy policy