Dlaczego mulss wykonuje tylko 3 cykle na Haswell, różni się od tabel instrukcji Agnera? (Rozwijanie pętli FP z wieloma akumulatorami)

Jestem nowicjuszem w optymalizacji instrukcji.

Zrobiłem prostą analizę na prostej funkcji dotp, która jest używana do uzyskania iloczynu kropkowego dwóch macierzy float.

Kod C jest następujący:

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;
}

Używam ramki testowej dostarczonej przez Agner Fog w sieci testp.

Użyte w tym przypadku tablice są wyrównane:

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;

Następnie wywołuję funkcję dotp, n = 2048, repeat=100000:

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

Kompiluję go z gcc 4.8.3, z opcją kompilacji-O3.

Kompiluję tę aplikację na komputerze, który nie obsługuje instrukcji FMA, więc widać, że są tylko instrukcje SSE.

Kod zespołu:

.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

Przeprowadzam analizę:

          μ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

Po uruchomieniu otrzymujemy wynik:

   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           

Druga linia jest wartością odczytywaną z rejestrów Intel; trzecia linia jest podzielona przez numer gałęzi, "BrTaken".

Widzimy więc, że w pętli są 6 instrukcji, 7 uops, w porozumieniu z analizą.

Liczba uops uruchomionych w port0 port1 port 5 port6 są podobne do tego, co mówi analiza. Myślę, że może UOPs scheduler to robi, może próbować zrównoważyć obciążenia na portach, mam rację?

Absolutnie nie rozumiem, dlaczego są tylko 3 cykle na pętlę. Zgodnie z tablicą instrukcji Agnera , opóźnienie instrukcji mulss wynosi 5, a między pętlami istnieją zależności, tak dalece jak widzę, powinno to zająć co najmniej 5 cykli na pętlę.

Czy ktoś mógłby rzucić trochę wglądu?

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

W 1997 roku, w wyniku połączenia z firmą Microsoft, powstała nowa wersja tej funkcji, która została zaprojektowana z myślą o klientach z całego świata.]}
.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

Wynik:

  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        

Widzimy więc, że pamięć podręczna danych L1 osiąga 2 * 256bit / 8.59, jest bardzo blisko szczytu 2*256/8, zużycie wynosi około 93%, FMA jednostka używana tylko 8 / 8.59, szczyt wynosi 2*8 / 8, zużycie wynosi 47%.

Więc myślę, że dotarłem do wąskiego gardła L1D, jak oczekuje Peter Cordes.

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

Specjalne podziękowania dla Boanna, napraw tyle błędów gramatycznych w moim pytaniu.

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

Z odpowiedzi Piotra rozumiem, że tylko rejestr "przeczytany i zapisany" byłby zależnością, rejestry "writer-only" nie byłyby zależnością.

Więc staram się zmniejszyć rejestry używane w pętli, i staram się rozwinąć o 5, jeśli wszystko jest ok, powinienem spotkać to samo wąskie gardło, 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 

Wynik:

    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    

Widzimy 5/5. 60 = 89.45%, jest trochę mniejszy niż urolling o 8, czy coś jest nie tak?

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

Staram się rozwinąć pętlę o 6, 7 i 15, aby zobaczyć wynik. I również rozwinąć przez 5 i 8 ponownie, aby dwukrotnie potwierdzić wynik.

Wynik jest następujący, widzimy, że tym razem wynik jest znacznie lepszy niż wcześniej.

Chociaż wynik nie jest stabilny, współczynnik rozwijania jest większy i wynik jest lepszy.

            | 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

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

Próbuję skompilować funkcję z gcc 7.1 w sieci " https://gcc.godbolt.org "

The compile opcja jest "-O3-march=haswell-mtune=intel", która jest podobna do 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
Author: Peter Cordes, 2017-07-15

1 answers

Spójrz na swoją pętlę jeszcze raz: movss xmm1, src nie ma zależności od starej wartości xmm1, ponieważ jej celem jest tylko zapis. Każda iteracja mulss jest niezależna. Out-of-order execution może i wykorzystuje tę równoległość na poziomie instrukcji, więc zdecydowanie nie ograniczasz opóźnienia mulss.

Opcjonalny odczyt: w architekturze komputera: zmiana nazwy rejestru pozwala uniknąć zagrożenia danych ANTYZALEŻNOŚCI o ponownym użyciu tego samego rejestru architektonicznego. (Niektóre Schematy pipelining + dependency-tracking przed zmianą nazwy rejestru nie rozwiązały wszystkich problemów, więc dziedzina architektury komputera ma duże znaczenie z powodu różnego rodzaju zagrożeń dla danych.

Zmiana nazwy rejestru za pomocą algorytm Tomasulo sprawia, że wszystko odchodzi poza rzeczywistymi, prawdziwymi zależnościami (odczyt po zapisie), więc każda instrukcja, w której cel nie jest również rejestrem źródłowym, nie ma interakcji z łańcuchem zależności zawierającym starą wartość tego Zarejestruj się. (Z wyjątkiem fałszywych zależności, jak popcnt na procesorach Intela i zapisywanie tylko części rejestru bez wyczyszczenia reszty (jak mov al, 5 lub sqrtss xmm2, xmm1). Related: dlaczego większość instrukcji x64 zeruje górną część 32-bitowego rejestru).


Powrót do kodu:

.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

Zależności przenoszone w pętli (od jednej iteracji do następnej) są następujące:

  • xmm0, przeczytane i napisane przez addss xmm0, xmm1, który ma 3 opóźnienia cyklu na Haswell.
  • rax, czytane i pisane przez add rax, 1. Opóźnienie 1c, więc nie jest to ścieżka krytyczna.

Wygląda na to, że mierzyłeś poprawnie czas wykonania / cykl-count, ponieważ wąskie gardła pętli na opóźnieniu 3c addss.

Oczekuje się tego: zależność szeregowa w produkcie kropkowym jest dodawaniem do pojedynczej sumy( aka redukcją), a nie mnożeniem między elementami wektorowymi.

To zdecydowanie dominujące wąskie gardło dla tej pętli, pomimo różnych mała nieefektywność:


short i stworzył silly cmp cx, ax, który wymaga dodatkowego przedrostka operandowego. Na szczęście gcc udało się uniknąć wykonywania add ax, 1, ponieważ signed-overflow jest niezdefiniowanym zachowaniem w C. , więc optymalizator może założyć, że tak się nie stało. (update: zasady promocji integer sprawiają, że jest inaczej dla short, więc UB nie wchodzi w to, ale gcc nadal może legalnie zoptymalizować. Całkiem pokręcone.)

Jeśli skompilowałeś z -mtune=intel, lub lepiej, -march=haswell, gcc umieściłoby cmp i jg obok siebie, gdzie mogłyby się połączyć.

Nie jestem pewien, dlaczego masz * w tabeli na cmp i add instrukcje. (update: ja tylko zgadywałam, że używasz notacji jak IACA robi, ale najwyraźniej nie byłeś). Żadne z nich się nie zapalają. Jedyną fuzją jest mikro-fuzja mulss xmm1, [rsi+rax*4].

A ponieważ jest to 2-operandowa Instrukcja ALU z przeznaczeniem read-modify-write Zarejestruj się, to pozostanie w makro-stopie nawet w ROB na Haswell. (Sandybridge odkleja laminat w momencie wydania. W tym celu należy zwrócić uwagę, że na Haswella również nie laminuje się laminatu.

Nic z tego nie ma znaczenia, ponieważ po prostu całkowicie wąskie gardło na opóźnienie FP-add, znacznie wolniejsze niż jakiekolwiek limity przepustowości uop. Bez -ffast-math, Kompilatory nie mogą nic zrobić. Clang zwykle rozwija się z wieloma akumulatorami i automatycznie wektoryzuje, więc będą to akumulatory wektorowe. Więc prawdopodobnie można nasycić limit przepustowości Haswella 1 wektor lub Skalar FP dodać na zegar, jeśli trafisz w pamięci podręcznej L1D.

[[51]}z opóźnieniem FMA wynoszącym 5c i przepustowością 0,5 c na Haswell, potrzebujesz 10 akumulatorów, aby utrzymać 10 FMA w locie i maksymalną przepustowość FMA, utrzymując P0 / p1 nasycone FMA. (Skylake zmniejszył opóźnienie FMA do 4 cykli i uruchamia mnożenie, dodawanie i FMA na jednostkach FMA. Więc faktycznie ma większe opóźnienie dodania niż Haswell.)

(you ' re bottlecked on obciążenia, ponieważ potrzebujesz dwóch obciążeń dla każdego FMA. W innych przypadkach można rzeczywiście zwiększyć przepustowość poprzez zastąpienie instrukcji vaddps instrukcją FMA z mnożnikiem 1.0. Oznacza to więcej opóźnień do ukrycia, więc najlepiej jest w bardziej złożonym algorytmie, w którym masz add, który nie znajduje się na ścieżce krytycznej.)


Re: uops na port :

Jest 1.19 uops na pętlę w porcie 5, to znacznie więcej niż oczekiwano 0.5, czy to jest sprawa o dyspozytorze uops próbującym uczynić uops na każdym porcie takim samym

Tak, coś w tym stylu. UOPs nie są przydzielane losowo, ani w jakiś sposób równomiernie rozmieszczone na każdym porcie, na którym mogą działać. Zakładałeś, że add i cmp uops rozłożą się równomiernie na p0156, ale tak nie jest.

Etap emisji przypisuje UOPs do portów na podstawie tego, ile uops już czeka na ten port. Ponieważ addss może działać tylko na p1 (i jest to wąskie gardło pętli), zwykle jest wiele P1 UOPs wydanych, ale nie wykonanych. Tak więc niewiele innych uops zostanie kiedykolwiek zaplanowanych na port1. (Dotyczy to mulss: większość mulss uops zakończy się planowo na porcie 0.)

Taken-Branch może działać tylko na porcie 6. Port 5 nie ma żadnych uops w tej pętli, które mogą tylko tam działać, więc ostatecznie przyciąga wiele wielu-portowych uops.

Scheduler (który wybiera unfused-domain uops ze stacji rezerwacji) nie jest wystarczająco inteligentny, aby uruchomić critical-path-first, więc algorytm przydziału zmniejsza opóźnienie konfliktu zasobów(inne uops kradną port1 podczas cykli, gdy addss mogło się uruchomić). Jest to również przydatne w przypadkach, gdy wąskie gardło na przepustowość danego portu.

Planowanie już przypisanych uops jest zwykle gotowe do najstarszych, jak to Rozumiem. Ten prosty algorytm nie jest zaskakujący, ponieważ musi wybrać uop z jego wejściami gotowymi dla każdego portu z wejścia 60 RS każdy cykl zegara, bez topienia procesora. W przeciwieństwie do innych procesorów, procesory te nie są w pełni kompatybilne z procesorami PROCESOROWYMI, ale są w pełni kompatybilne z procesorami PROCESOROWYMI.

Powiązane / więcej szczegółów: jak dokładnie zaplanowane są x86 uops?


Więcej analiz wydajności:

Oprócz błędów w pamięci podręcznej / błędnej interpretacji gałęzi, trzy główne możliwe wąskie gardła dla pętli związanych z procesorem są:

  • łańcuchy zależności (jak w tym przypadku)
  • W 2008 roku firma Haswell wprowadziła do swojej oferty nowe rozwiązania.]}
  • wąskie gardła portów wykonania, jak w przypadku, gdy wiele UOP potrzebuje p0/p1 lub p2 / p3, jak w rozwijanej pętli. Policz nieużywane uops domeny dla określonych portów. Generalnie można zakładać dystrybucję w najlepszym przypadku, z uops, które mogą działać na innych portach, nie kradnąc zajętych portów bardzo często, ale zdarza się to trochę.

Ciało pętli lub krótki blok kodu może być w przybliżeniu scharakteryzowany przez 3 rzeczy: liczbę zespolonych domen uop, liczbę nieużywanych domen, na których jednostkach wykonawczych może działać, i całkowite opóźnienie ścieżki krytycznej przy założeniu, że najlepiej będzie zaplanować ścieżkę krytyczną. (Lub opóźnienia z każdego z wejść A / B / C na wyjście...)

Na przykład wykonując wszystkie trzy, aby porównać kilka krótkich sekwencji, zobacz moją odpowiedź na jaki jest skuteczny sposób na liczenie ustawionych bitów w pozycji lub niższej?

W skrócie pętle, nowoczesne procesory mają wystarczająco dużo niekonwencjonalnych zasobów wykonawczych (Rozmiar pliku rejestru fizycznego, więc zmiana nazwy nie kończy się z rejestrów, rozmiar ROB), aby mieć wystarczająco dużo iteracji pętli w locie, aby znaleźć wszystkie równoległości. Ale gdy łańcuchy zależności w pętlach stają się dłuższe, w końcu się kończą. Zobacz Pomiar pojemności bufora zmiany kolejności, aby dowiedzieć się, co się dzieje, gdy procesor skończy się z rejestrami do zmiany nazwy.

Zobacz także wiele linków wydajności i odniesienia w x86 tag wiki.


Strojenie pętli FMA:

Tak, dot-product na Haswell będzie ograniczał przepustowość L1D Przy tylko połowie przepustowości jednostek FMA, ponieważ zajmuje dwa obciążenia na mnożenie+dodawanie.

Jeśli robisz B[i] = x * A[i] + y; lub sum(A[i]^2), możesz nasycić przepustowość FMA.

Wygląda na to, że nadal próbujesz uniknąć ponownego użycia rejestru nawet w przypadkach tylko do zapisu, takich jak miejsce przeznaczenia obciążenia vmovaps, więc zabrakło rejestrów po rozwijany przez 8 . W porządku, ale może mieć znaczenie w innych przypadkach.

Również użycie ymm8-15 może nieznacznie zwiększyć rozmiar kodu, jeśli oznacza to, że potrzebny jest 3-bajtowy prefiks VEX zamiast 2-bajtowego. Ciekawostka: vpxor ymm7,ymm7,ymm8 potrzebuje 3-bajtowego VEX, podczas gdy vpxor ymm8,ymm8,ymm7 potrzebuje tylko 2-bajtowego prefiksu VEX. W operacjach komutacyjnych, posortuj źródła od wysokiego do niskiego.

Nasze wąskie gardło obciążenia oznacza, że przepustowość FMA w najlepszym przypadku jest o połowę mniejsza, więc potrzebujemy co najmniej 5 akumulatorów wektorowych, aby ukryć ich opóźnienie. 8 jest dobre, więc jest wiele luzu w łańcuchach zależności, aby pozwolić im nadrobić zaległości po wszelkich opóźnieniach spowodowanych nieoczekiwanym opóźnieniem lub konkurencją dla p0 / p1. 7 a może nawet 6 byłoby w porządku, zbyt: Twój współczynnik rozwijania nie musi być moc 2.

Rozwinięcie dokładnie o 5 oznaczałoby, że jesteś również w wąskim gardle dla łańcuchów zależności . Za każdym razem, gdy FMA nie działa w dokładnym cyklu, jego dane wejściowe są gotowe, oznacza to utratę cyklu w tym łańcuchu zależności. Może się to zdarzyć, jeśli obciążenie jest wolne (np. pomija w pamięci podręcznej L1 i musi czekać na L2), lub jeśli Ładowanie zakończy się nieprawidłowo, a FMA z innego łańcucha zależności wykrada port, dla którego FMA było zaplanowane. (Pamiętaj, że planowanie ma miejsce w momencie wydania, więc uops siedzący w harmonogramie to port0 FMA lub port1 FMA, a nie FMA, który może przyjąć dowolny port bezczynności).

Jeśli zostawisz trochę luzu w łańcuchach zależności, out-of-order execution może "dogonić" FMA, ponieważ nie będą one blokowane na przepustowość lub opóźnienie, tylko czekając na wyniki ładowania. @Forward stwierdził (w aktualizacji do pytania), że rozwijanie o 5 zmniejszyło wydajność z 93% przepustowości L1D do 89,5% dla tej pętli.

Domyślam się, że rozwiń o 6 (o jeden więcej niż minimum, aby ukryć opóźnienie) byłoby ok tutaj, i uzyskać taką samą wydajność jak rozwiń o 8. Jeśli zbliżylibyśmy się do maksymalizacji przepustowości FMA (a nie tylko wąskiego gardła przy przepustowości obciążenia), o jeden więcej niż minimum może nie być wystarczy.

Update :test eksperymentalny @ Forward pokazuje, że moje przypuszczenie było błędne . Nie ma dużej różnicy między unroll5 i unroll6. Ponadto unroll15 jest dwa razy bliżej niż unroll8 do teoretycznej maksymalnej przepustowości 2x 256b obciążeń na zegar. Pomiar z niezależnymi obciążeniami w pętli, lub z niezależnymi obciążeniami i tylko rejestrem FMA, powie nam, ile z tego wynika z interakcji z łańcuchem zależności FMA. Nawet najlepszy przypadek nie dostanie idealnego 100% przepustowość, choćby ze względu na błędy pomiarowe i zakłócenia spowodowane przerwami czasowymi. (Linux perf mierzy tylko cykle przestrzeni użytkownika, chyba że uruchomisz go jako root, ale czas nadal obejmuje czas spędzony w programach obsługi przerwań. Dlatego Częstotliwość PROCESORA może być podawana jako 3.87 GHz, gdy jest uruchomiony jako root, ale 3.900 GHz, gdy jest uruchomiony jako root i pomiar cycles zamiast cycles:u.)


Nie ograniczamy przepustowości front-end, ale możemy zmniejszyć liczbę połączonych domen uop o unikanie indeksowanych trybów adresowania dla instrukcji nie-mov. Mniej jest lepsze i sprawia, że jest to bardziej przyjazne dla hyperthreading , gdy dzielenie rdzenia z czymś innym niż to.

Prosty sposób polega na wykonaniu dwóch przyrostów wskaźnika wewnątrz pętli. Metoda ta polega na indeksowaniu jednej tablicy względem drugiej.]}

;; input pointers for x[] and y[] in rdi and rsi
;; size_t n  in rdx

    ;;; zero ymm1..8, or load+vmulps into them

    add             rdx, rsi             ; end_y
    ; lea rdx, [rdx+rsi-252]  to break out of the unrolled loop before going off the end, with odd n

    sub             rdi, rsi             ; index x[] relative to y[], saving one pointer increment

.unroll8:
    vmovaps         ymm0, [rdi+rsi]            ; *px, actually py[xy_offset]
    vfmadd231ps     ymm1, ymm0, [rsi]          ; *py

    vmovaps         ymm0,       [rdi+rsi+32]   ; write-only reuse of ymm0
    vfmadd231ps     ymm2, ymm0, [rsi+32]

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

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

    add             rsi, 256       ; pointer-increment here
                                   ; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
                                   ; smaller code-size helps in the big picture, but not for a micro-benchmark

    vmovaps         ymm0,       [rdi+rsi+128-256]  ; be pedantic in the source about compensating for the pointer-increment
    vfmadd231ps     ymm5, ymm0, [rsi+128-256]
    vmovaps         ymm0,       [rdi+rsi+160-256]
    vfmadd231ps     ymm6, ymm0, [rsi+160-256]
    vmovaps         ymm0,       [rdi+rsi-64]       ; or not
    vfmadd231ps     ymm7, ymm0, [rsi-64]
    vmovaps         ymm0,       [rdi+rsi-32]
    vfmadd231ps     ymm8, ymm0, [rsi-32]

    cmp             rsi, rdx
    jb              .unroll8                 ; } while(py < endy);

Użycie nieindeksowanego trybu adresowania jako operandu pamięci dla vfmaddps pozwala na mikro-zespolenie się w rdzeniu poza porządkiem, zamiast być nie laminowane w sprawie. tryby mikro fuzji i adresacji

Więc moja pętla to 18 zespolonych domen uops dla 8 wektorów. Twój pobiera 3 UOPs dla każdej pary vmovaps + vfmaddps, zamiast 2, ze względu na un-laminowanie indeksowanych trybów adresowania. Oba z nich nadal oczywiście mają 2 unfused-domain load UOPs (port2/3) na parę, więc to nadal wąskie gardło.

Less fused-domain UOPs lets out-of-order execution Zobacz więcej iteracji przed nami, potencjalnie pomaga lepiej absorbować pudła pamięci podręcznej. Jest to drobna rzecz, gdy jesteśmy wąskie gardło na jednostce wykonawczej (load uops w tym przypadku), nawet bez błędów pamięci podręcznej, choć. Ale z hyperthreading, dostajesz tylko każdy inny cykl pasma problemu front-end, chyba że drugi wątek jest zablokowany. Jeśli nie konkuruje zbytnio o load I p0/1, mniej połączonych domen UOPs pozwoli tej pętli działać szybciej podczas współdzielenia rdzenia. (np. może drugi hyper-thread działa dużo port5 / port6 i sklep uops?)

Ponieważ un-lamination dzieje się po UOP-cache, Twoja wersja nie zajmuje dodatkowego miejsca w pamięci podręcznej uop. Disp32 z każdym uop jest w porządku i nie zajmuje dodatkowego miejsca. Ale większy rozmiar kodu oznacza, że UOP-cache jest mniej prawdopodobne, aby pakować tak efektywnie, ponieważ trafisz granice 32B, zanim linie pamięci podręcznej uop będą wypełnione częściej. (Właściwie mniejszy kod też nie gwarantuje lepszego. Mniejsze instrukcje mogą prowadzić do wypełnienia linii pamięci podręcznej uop i konieczności jednego wpisu w kolejna linia przed przekroczeniem granicy 32B.) Ta mała pętla może działać z bufora loopback (LSD), więc na szczęście UOP-cache nie jest czynnikiem.


Po pętli: Efektywne czyszczenie jest trudną częścią efektywnej wektoryzacji dla małych tablic, które mogą nie być wielokrotnością współczynnika rozwinięcia, a zwłaszcza szerokości wektora]}
    ...
    jb

    ;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
    ;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.

    ; reduce down to a single vector, with a tree of dependencies
    vaddps          ymm1, ymm2, ymm1
    vaddps          ymm3, ymm4, ymm3
    vaddps          ymm5, ymm6, ymm5
    vaddps          ymm7, ymm8, ymm7

    vaddps          ymm0, ymm3, ymm1
    vaddps          ymm1, ymm7, ymm5

    vaddps          ymm0, ymm1, ymm0

    ; horizontal within that vector, low_half += high_half until we're down to 1
    vextractf128    xmm1, ymm0, 1
    vaddps          xmm0, xmm0, xmm1
    vmovhlps        xmm1, xmm0, xmm0        
    vaddps          xmm0, xmm0, xmm1
    vmovshdup       xmm1, xmm0
    vaddss          xmm0, xmm1
    ; this is faster than 2x vhaddps

    vzeroupper    ; important if returning to non-AVX-aware code after using ymm regs.
    ret           ; with the scalar result in xmm0

Aby uzyskać więcej informacji na temat sumy poziomej na końcu, zobacz najszybszy sposób wykonania sumy poziomej wektora zmiennoprzecinkowego na x86. The two Shuffle 128B, których używałem, nie wymagają nawet natychmiastowego bajtu kontrolnego, więc zapisuje 2 bajty o rozmiarze kodu w porównaniu z bardziej oczywistym shufps. (I 4 bajty o rozmiarze kodu vs. vpermilps, ponieważ ten kod zawsze potrzebuje 3-bajtowego prefiksu VEX, a także natychmiastowego). AVX 3-operand rzeczy jest bardzo ładne porównanie SSE, zwłaszcza gdy pisząc w C z intrinsics więc nie można tak łatwo wybrać zimny rejestr movhlps do.

 35
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-18 06:54:55