Warum dauert Mulss nur 3 Zyklen auf Haswell, anders als in Agners Anweisungstabellen? (Abrollen von FP-Loops mit mehreren Akkumulatoren)

Lesezeit: 7 Minuten

Benutzeravatar von Forward
Nach vorne

Ich bin ein Neuling in der Befehlsoptimierung.

Ich habe eine einfache Analyse an einer einfachen Funktion dotp durchgeführt, die verwendet wird, um das Skalarprodukt zweier Float-Arrays zu erhalten.

Der C-Code lautet wie folgt:

float dotp(               
    const float  x[],   
    const float  y[],     
    const short  n      
)
{
    short i;
    float suma;
    suma = 0.0f;

    for(i=0; i<n; i++) 
    {    
        suma += x[i] * y[i];
    } 
    return suma;
}

Ich verwende den von Agner Fog im Internet bereitgestellten Testrahmen testp.

Die in diesem Fall verwendeten Arrays sind ausgerichtet:

int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);

float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;

Dann rufe ich die Funktion dotp, n=2048, repeat=100000 auf:

 for (i = 0; i < repeat; i++)
 {
     sum = dotp(x,y,n);
 }

Ich kompiliere es mit gcc 4.8.3, mit der Kompilieroption -O3.

Ich kompiliere diese Anwendung auf einem Computer, der keine FMA-Anweisungen unterstützt, sodass Sie sehen können, dass es nur SSE-Anweisungen gibt.

Der Assemblercode:

.L13:
        movss   xmm1, DWORD PTR [rdi+rax*4]  
        mulss   xmm1, DWORD PTR [rsi+rax*4]   
        add     rax, 1                       
        cmp     cx, ax
        addss   xmm0, xmm1
        jg      .L13

Ich mache eine Analyse:

          μops-fused  la    0    1    2    3    4    5    6    7    
movss       1          3             0.5  0.5
mulss       1          5   0.5  0.5  0.5  0.5
add         1          1   0.25 0.25               0.25   0.25 
cmp         1          1   0.25 0.25               0.25   0.25
addss       1          3         1              
jg          1          1                                   1                                                   -----------------------------------------------------------------------------
total       6          5    1    2     1     1      0.5   1.5

Nach dem Ausführen erhalten wir das Ergebnis:

   Clock  |  Core cyc |  Instruct |   BrTaken | uop p0   | uop p1      
--------------------------------------------------------------------
542177906 |609942404  |1230100389 |205000027  |261069369 |205511063 
--------------------------------------------------------------------  
   2.64   |  2.97     | 6.00      |     1     | 1.27     |  1.00   

   uop p2   |    uop p3   |  uop p4 |    uop p5  |  uop p6    |  uop p7       
-----------------------------------------------------------------------   
 205185258  |  205188997  | 100833  |  245370353 |  313581694 |  844  
-----------------------------------------------------------------------          
    1.00    |   1.00      | 0.00    |   1.19     |  1.52      |  0.00           

Die zweite Zeile ist der aus den Intel-Registern gelesene Wert; die dritte Zeile wird durch die Zweignummer “BrTaken” geteilt.

Wir können also sehen, dass es in der Schleife 6 Anweisungen gibt, 7 uops, in Übereinstimmung mit der Analyse.

Die Anzahl der Uops, die in Port0, Port1, Port 5, Port6 ausgeführt werden, ist ähnlich wie in der Analyse angegeben. Ich denke, vielleicht macht das der Uops-Scheduler, er versucht vielleicht, die Lasten auf den Ports auszugleichen, habe ich recht?

Ich verstehe absolut nicht, warum es nur ca. 3 Zyklen pro Schleife sind. Laut Agners Anweisungstabelledie Latenzzeit des Unterrichts mulss ist 5, und es gibt Abhängigkeiten zwischen den Schleifen, so dass es meines Erachtens mindestens 5 Zyklen pro Schleife dauern sollte.

Könnte jemand einen Einblick geben?

=============================================== ================

Ich habe versucht, eine optimierte Version dieser Funktion in nasm zu schreiben, indem ich die Schleife um den Faktor 8 entrollte und die vfmadd231ps Anweisung:

.L2:
    vmovaps         ymm1, [rdi+rax]             
    vfmadd231ps     ymm0, ymm1, [rsi+rax]       

    vmovaps         ymm2, [rdi+rax+32]          
    vfmadd231ps     ymm3, ymm2, [rsi+rax+32]    

    vmovaps         ymm4, [rdi+rax+64]          
    vfmadd231ps     ymm5, ymm4, [rsi+rax+64]    

    vmovaps         ymm6, [rdi+rax+96]          
    vfmadd231ps     ymm7, ymm6, [rsi+rax+96]   

    vmovaps         ymm8, [rdi+rax+128]         
    vfmadd231ps     ymm9, ymm8, [rsi+rax+128]  

    vmovaps         ymm10, [rdi+rax+160]               
    vfmadd231ps     ymm11, ymm10, [rsi+rax+160] 

    vmovaps         ymm12, [rdi+rax+192]                
    vfmadd231ps     ymm13, ymm12, [rsi+rax+192] 

    vmovaps         ymm14, [rdi+rax+224]                
    vfmadd231ps     ymm15, ymm14, [rsi+rax+224] 
    add             rax, 256                    
    jne             .L2

Das Ergebnis:

  Clock   | Core cyc |  Instruct  |  BrTaken  |  uop p0   |   uop p1  
------------------------------------------------------------------------
 24371315 |  27477805|   59400061 |   3200001 |  14679543 |  11011601  
------------------------------------------------------------------------
    7.62  |     8.59 |  18.56     |     1     | 4.59      |     3.44


   uop p2  | uop p3  |  uop p4  |   uop p5  |   uop p6   |  uop p7  
-------------------------------------------------------------------------
 25960380  |26000252 |  47      |  537      |   3301043  |  10          
------------------------------------------------------------------------------
    8.11   |8.13     |  0.00    |   0.00    |   1.03     |  0.00        

Wir können also sehen, dass der L1-Datencache 2 * 256 Bit / 8,59 erreicht, er ist sehr nahe am Spitzenwert von 2 * 256 / 8, die Nutzung beträgt etwa 93 %, die FMA-Einheit verwendete nur 8 / 8,59, der Spitzenwert ist 2 * 8 /8 beträgt die Auslastung 47 %.

Ich denke also, ich habe den L1D-Engpass erreicht, wie Peter Cordes erwartet.

=============================================== ================

Besonderer Dank geht an Boann, behebt so viele Grammatikfehler in meiner Frage.

=============================================== ===============

Aus Peters Antwort geht hervor, dass nur “Lese- und Schreibregister” die Abhängigkeit wären, “Nur-Schreiber”-Register wären nicht die Abhängigkeit.

Also versuche ich, die in der Schleife verwendeten Register zu reduzieren, und ich versuche, um 5 abzurollen, wenn alles in Ordnung ist, sollte ich auf den gleichen Engpass treffen, L1D.

.L2:
    vmovaps         ymm0, [rdi+rax]    
    vfmadd231ps     ymm1, ymm0, [rsi+rax]    

    vmovaps         ymm0, [rdi+rax+32]    
    vfmadd231ps     ymm2, ymm0, [rsi+rax+32]   

    vmovaps         ymm0, [rdi+rax+64]    
    vfmadd231ps     ymm3, ymm0, [rsi+rax+64]   

    vmovaps         ymm0, [rdi+rax+96]    
    vfmadd231ps     ymm4, ymm0, [rsi+rax+96]   

    vmovaps         ymm0, [rdi+rax+128]    
    vfmadd231ps     ymm5, ymm0, [rsi+rax+128]   

    add             rax, 160                    ;n = n+32
    jne             .L2 

Das Ergebnis:

    Clock  | Core cyc  | Instruct  |  BrTaken |    uop p0  |   uop p1  
------------------------------------------------------------------------  
  25332590 |  28547345 |  63700051 |  5100001 |   14951738 |  10549694   
------------------------------------------------------------------------
    4.97   |  5.60     | 12.49     |    1     |     2.93   |    2.07    

    uop p2  |uop p3   | uop p4 | uop p5 |uop p6   |  uop p7 
------------------------------------------------------------------------------  
  25900132  |25900132 |   50   |  683   | 5400909 |     9  
-------------------------------------------------------------------------------     
    5.08    |5.08     |  0.00  |  0.00  |1.06     |     0.00    

Wir können 5/5,60 = 89,45 % sehen, es ist etwas kleiner als 8 zu würfeln, stimmt etwas nicht?

=============================================== ===============

Ich versuche, die Schleife um 6, 7 und 15 zu entrollen, um das Ergebnis zu sehen. Ich entrolle auch wieder um 5 und 8, um das Ergebnis doppelt zu bestätigen.

Das Ergebnis ist wie folgt, wir können sehen, dass das Ergebnis diesmal viel besser ist als zuvor.

Obwohl das Ergebnis nicht stabil ist, ist der Abrollfaktor größer und das Ergebnis besser.

            | L1D bandwidth     |  CodeMiss | L1D Miss | L2 Miss 
----------------------------------------------------------------------------
  unroll5   | 91.86% ~ 91.94%   |   3~33    | 272~888  | 17~223
--------------------------------------------------------------------------
  unroll6   | 92.93% ~ 93.00%   |   4~30    | 481~1432 | 26~213
--------------------------------------------------------------------------
  unroll7   | 92.29% ~ 92.65%   |   5~28    | 336~1736 | 14~257
--------------------------------------------------------------------------
  unroll8   | 95.10% ~ 97.68%   |   4~23    | 363~780  | 42~132
--------------------------------------------------------------------------
  unroll15  | 97.95% ~ 98.16%   |   5~28    | 651~1295 | 29~68

=============================================== ===================

Ich versuche die Funktion mit gcc 7.1 im Web zu kompilieren “https://gcc.godbolt.org

Die Kompilierungsoption ist “-O3 -march=haswell -mtune=intel”, das ist ähnlich wie bei gcc 4.8.3.

.L3:
        vmovss  xmm1, DWORD PTR [rdi+rax]
        vfmadd231ss     xmm0, xmm1, DWORD PTR [rsi+rax]
        add     rax, 4
        cmp     rdx, rax
        jne     .L3
        ret

  • Befürworten Sie die Forschungsanstrengungen.

    – fuz

    15. Juli 2017 um 1:47 Uhr

  • Es gibt zwei Ausführungseinheiten, die FP-Multiplikationen auf Haswell ausführen können, sodass zwei MULSS-Befehle parallel ausgeführt werden können. Es gibt keine Abhängigkeit zwischen MULSS-Befehlen in jeder Schleifeniteration.

    – Rossgrat

    15. Juli 2017 um 3:41 Uhr

  • @Ross Ridge, ja,Ich verstehe es mit der Antwort von Peter Cordes, die Abhängigkeit ist xmm0, also ist „addss“ der Engpass.

    – Nach vorne

    15. Juli 2017 um 5:20 Uhr

  • Ja, schöne Arbeit auf der abgerollten FMA-Schleife. Ich habe in meiner Antwort einen Abschnitt darüber hinzugefügt. Sie können die Codegröße und die Anzahl der Fused-Domain-Uops verkleinern, aber Sie können der Sättigung des p2/p3-Uop-Durchsatzes wahrscheinlich nicht viel näher kommen, wodurch Sie auf zwei L1D-Lasten pro Zyklus beschränkt sind, die durchschnittlich eine FMA pro Zyklus speisen. Ich habe meine Antwort aktualisiert, um klarer zu machen, dass die Wiederverwendung von Registern mit Nur-Schreib-Anweisungen in Ordnung ist. Ihre FMA-Schleife verwendet viele architektonische Register als Ladeziele ohne Nutzen. (Aber nur ein Code-Size-Nachteil).

    – Peter Cordes

    15. Juli 2017 um 17:22 Uhr

  • Im Allgemeinen möchten Sie einen Compiler, der neuer als die Hardware ist, daher hatten sie Zeit, die Tuning-Optionen für zu aktualisieren -march=native. Und beheben Sie einige Makes-Slow-Code-Probleme, die möglicherweise erst bemerkt werden, wenn AVX2 schon eine Weile auf dem Markt ist. Ich denke jedoch, dass viele Leute alte Compiler mit guten Ergebnissen verwenden. Vielleicht mache ich zu viel Aufhebens darum, aber wenn ich mir die Compiler-asm-Ausgabe ansehe, schneidet neuerer gcc oft besser ab. Oft auf eine Weise, die insgesamt jedoch nicht wirklich wichtig ist.

    – Peter Cordes

    16. Juli 2017 um 0:34 Uhr


  • Hallo, Peter Cordes, vielen Dank, ich verstehe, dass die Abhängigkeit das Register xmm0 ist und das Addss der Engpass ist. Am Anfang sehe ich, dass cmp und add in Port0, Port1, Port5, Port5 laufen könnten, also setze ich ein * auf cmp und addiere, um zu zeigen, dass es in vielen Ports laufen könnte … nun, ich weiß nicht, dass es eine besondere Bedeutung gibt über “*”, ich habe es behoben.

    – Nach vorne

    15. Juli 2017 um 5:49 Uhr


  • Was halten Sie davon, tatsächlich gibt es 1,19 Uops pro Schleife in Port 5, es ist viel mehr als 0,5 erwartet, ist es die Sache, dass der Uops-Dispatcher versucht, Uops auf jedem Port gleich zu machen?

    – Nach vorne

    15. Juli 2017 um 5:52 Uhr


  • i++ Wenn i ist 2^15-1 und i wurde erklärt short ist nicht UB. i++ erweitert sich zu i = (short) ((int) i + 1); und das implementierungsdefinierte Verhalten des Überlaufs bei der Konvertierung aus int zu short muss vorkommen. Die Codetransformation von GCC ist dennoch korrekt.

    – Pascal Cuoq

    15. Juli 2017 um 13:24 Uhr

  • @Forward: Ja, ich habe diese Antwort nicht auf Anfänger-Sachen beschränkt: P Dies schien ein guter Ort zu sein, um zu versuchen, eine kanonische Version zu schreiben, wie man Latenz, Front-End-Uops und Ausführungsport-Uops zählt. Und wenn ich hier von anderen Antworten aus verlinke, kann ich genauso gut auf viele interessante Details für jeden mit Erfahrungsniveau eingehen, der sie lesen möchte. 🙂 Bitte stellen Sie in Zukunft weitere gute Fragen wie diese, wenn Sie nach dem Lesen von Agner Fogs Leitfäden (insbesondere dem Microarch-Leitfaden) und der Suche nach SO immer noch nicht weiterkommen. Hier gibt es einige gute x86-Perf-Antworten (einige davon von mir 🙂

    – Peter Cordes

    16. Juli 2017 um 0:11 Uhr


  • @PeterCordes, ja, in meinem Test ist 15 messbar schneller als 8, aber nur ein bisschen, man sieht, dass der Best Case in 8 dem Worst Case in 15 ähnlich ist.

    – Nach vorne

    18. Juli 2017 um 6:23 Uhr

1406470cookie-checkWarum dauert Mulss nur 3 Zyklen auf Haswell, anders als in Agners Anweisungstabellen? (Abrollen von FP-Loops mit mehreren Akkumulatoren)

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

Privacy policy