C++: tajemniczo ogromne przyspieszenie od posiadania jednego operanda w rejestrze

Próbowałem uzyskać wyobrażenie o wpływie posiadania tablicy w pamięci podręcznej L1 W porównaniu z pamięcią, synchronizując procedurę, która skaluje i sumuje elementy tablicy za pomocą następującego kodu (jestem świadomy, że powinienem po prostu skalować wynik przez 'a' na końcu; chodzi o to, aby zarówno mnożenie, jak i dodawanie w pętli - jak dotąd kompilator nie domyślił się, aby uwzględnić 'a'): {]}

double sum(double a,double* X,int size)
{
    double total = 0.0;
    for(int i = 0;  i < size; ++i)
    {
        total += a*X[i];
    }
    return total;
}

#define KB 1024
int main()
{
    //Approximately half the L1 cache size of my machine
    int operand_size = (32*KB)/(sizeof(double)*2);
    printf("Operand size: %d\n", operand_size);
    double* X = new double[operand_size];
    fill(X,operand_size);

    double seconds = timer();
    double result;
    int n_iterations = 100000;
    for(int i = 0; i < n_iterations; ++i)
    {
        result = sum(3.5,X,operand_size);
        //result += rand();  
    }
    seconds = timer() - seconds; 

    double mflops = 2e-6*double(n_iterations*operand_size)/seconds;
    printf("Vector size %d: mflops=%.1f, result=%.1f\n",operand_size,mflops,result);
    return 0;
}

Zauważ, że procedury timer() i fill() nie są uwzględniane dla zwięzłości; ich pełne źródło może znajdziesz tutaj, jeśli chcesz uruchomić kod:

Http://codepad.org/agPWItZS

Tutaj robi się ciekawie. To jest wyjście:
Operand size: 2048
Vector size 2048: mflops=588.8, result=-67.8

Jest to wydajność całkowicie niebuforowana, pomimo faktu, że wszystkie elementy x powinny być przechowywane w pamięci podręcznej między iteracjami pętli. Patrząc na kod assembly wygenerowany przez:

g++ -O3 -S -fno-asynchronous-unwind-tables register_opt_example.cpp

Zauważam jedną dziwność w pętli funkcji sum:

L55:
    movsd   (%r12,%rax,8), %xmm0
    mulsd   %xmm1, %xmm0
    addsd   -72(%rbp), %xmm0
    movsd   %xmm0, -72(%rbp)
    incq    %rax
    cmpq    $2048, %rax
    jne L55

Instrukcja:

    addsd   -72(%rbp), %xmm0
    movsd   %xmm0, -72(%rbp)

Wskaż, że jest zapisanie wartości "total" w sum () na stosie oraz odczyt i zapis przy każdej iteracji pętli. Zmodyfikowałem zespół tak, aby ten operand był przechowywany w rejestrze a:

...
addsd   %xmm0, %xmm3
...

Ta mała zmiana tworzy ogromny wzrost wydajności:

Operand size: 2048
Vector size 2048: mflops=1958.9, result=-67.8

Tl; dr Moje pytanie brzmi: dlaczego zastąpienie dostępu do pojedynczej lokalizacji pamięci rejestrem przyspiesza kod tak bardzo, biorąc pod uwagę, że pojedyncza lokalizacja powinna być przechowywana w pamięci podręcznej L1? Jakie czynniki architektoniczne to możliwe? Wydaje się bardzo dziwne, że wielokrotne pisanie jednej lokalizacji stosu całkowicie zniszczyłoby skuteczność pamięci podręcznej.

Dodatek

Moja wersja gcc to:

Target: i686-apple-darwin10
Configured with: /var/tmp/gcc/gcc-5646.1~2/src/configure --disable-checking --enable-werror --prefix=/usr --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.2/ --with-slibdir=/usr/lib --build=i686-apple-darwin10 --with-gxx-include-dir=/include/c++/4.2.1 --program-prefix=i686-apple-darwin10- --host=x86_64-apple-darwin10 --target=i686-apple-darwin10
Thread model: posix
gcc version 4.2.1 (Apple Inc. build 5646) (dot 1)

Mój procesor to:

Intel Xeon X5650

Author: Mysticial, 2013-03-27

3 answers

Jest to prawdopodobnie kombinacja dłuższego łańcucha zależności, wraz z błędną interpretacją obciążenia*.


Dłuższy Łańcuch Zależności:

Najpierw identyfikujemy krytyczne ścieżki zależności. Następnie przyjrzymy się latencji instrukcji dostarczonej przez: http://www.agner.org/optimize/instruction_tables.pdf (strona 117)

W wersji nieoptymalizowanej krytyczna ścieżka zależności To:

  • addsd -72(%rbp), %xmm0
  • movsd %xmm0, -72(%rbp)

Wewnętrznie, prawdopodobnie dzieli się na:

  • obciążenie (2 cykle)
  • addsd (3 cykle)
  • store (3 cykle)

Jeśli spojrzymy na wersję zoptymalizowaną, to po prostu:

  • addsd (3 cykle)

Więc masz 8 cykli vs.3 cykle. Prawie 3.

Nie jestem pewien, jak wrażliwa jest linia procesorów Nehalem do przechowywania zależności obciążenia i jak dobrze to robiforwarding . Ale rozsądne jest wierzyć, że nie jest zero.


Błąd w Load-store:

Współczesne procesory wykorzystują przewidywania na więcej sposobów, jakie można sobie wyobrazić. Najbardziej znanym z nich jest prawdopodobnie Przewidywanie gałęzi . Jednym z mniej znanych jest przewidywanie obciążenia.

Gdy procesor widzi obciążenie, natychmiast załaduje je przed zakończeniem wszystkich oczekujących zapisów. Zakłada, że te zapisy nie będą sprzeczne z załadowanymi wartościami.

Jeśli wcześniejszy zapis okaże się sprzeczny z obciążeniem, to obciążenie musi zostać ponownie wykonane, a obliczenia wycofane do punktu obciążenia. (w taki sam sposób, w jaki błędne interpretacje gałęzi cofają się)

Jak to jest istotne tutaj:

Nie trzeba dodawać, że nowoczesne procesory będą w stanie wykonywać wiele iteracji tej pętli jednocześnie. Procesor będzie więc próbował wykonać load (addsd -72(%rbp), %xmm0), zanim zakończy Store (movsd %xmm0, -72(%rbp)) z poprzedniej iteracji.

Wynik? Poprzedni sklep koliduje z ładowaniem - w ten sposób błędna interpretacja i cofnięcie.

*zauważ, że nie jestem pewien nazwy "Przewidywanie obciążenia". Czytałem o tym tylko w dokumentach i nie dali mu nazwy.

 57
Author: Mysticial,
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-05-23 12:02:02

Domyślam się, że problem nie leży w pamięci podręcznej / dostępie do pamięci, ale w procesorze(wykonanie kodu). Jest tu kilka widocznych wąskich gardeł.

Numery wydajności były oparte na pudełkach, których używałem (sandybridge lub westmere)

Maksymalna wydajność dla matematyki skalarnej to 2,7 Ghz X2 FLOPS / Clock x2 ponieważ procesor może jednocześnie dodawać i mnożyć. Teoretyczna sprawność kodu to 0.6/(2.7*2) = 11%

Potrzebna przepustowość: 2 dwuosobowe na ( + ) i (x) - > 4bytes / Flop 4 bajty * 5.4 GFLOPS = 21.6 GB/s

Jeśli wiesz, że został odczytany niedawno prawdopodobnie w L1( 89GB / s), L2 (42GB/s) lub L3(24GB/s), Więc możemy wykluczyć pamięć podręczną B/w

Pamięć susbsystem wynosi 18,9 GB / s, więc nawet w pamięci głównej, Maksymalna wydajność powinna zbliżać się do 18,9/21,6 GB / s = 87,5%

  • może chcieć załadować żądania (poprzez rozwijanie) tak wcześnie, jak to możliwe

Nawet przy wykonaniu spekulacyjnym, tot + = a * X [i] dodawanie będzie serializowane, ponieważ tot(n) musi być eval ' d zanim tot (n+1) może być rozpoczęty

Pierwsza pętla rozwiń
move I by 8 ' s and do

{//your func
    for( int i = 0; i < size; i += 8 ){
        tot += a * X[i];
        tot += a * X[i+1];
        ...
        tot += a * X[i+7];
    }
    return tot
}

Użyj wielu akumulatorów
Spowoduje to zerwanie zależności i pozwoli nam uniknąć przeciągania się potoku dodawania

{//your func//
    int tot,tot2,tot3,tot4;
    tot = tot2 = tot3 = tot4 = 0
    for( int i = 0; i < size; i += 8 ) 
        tot  += a * X[i];
        tot2 += a * X[i+1];
        tot3 += a * X[i+2];
        tot4 += a * X[i+3];
        tot  += a * X[i+4];
        tot2 += a * X[i+5];
        tot3 += a * X[i+6];
        tot4 += a * X[i+7];
    }
    return tot + tot2 + tot3 + tot4;
}

UPDATE Po uruchomieniu tego na SandyBridge box mam dostęp do: (2.7 GHZ SandyBridge with-O2-march=native-mtune=native

Oryginalny kod:

Operand size: 2048  
Vector size 2048: mflops=2206.2, result=61.8  
2.206 / 5.4 = 40.8%

Poprawiony Kod:

Operand size: 2048  
Vector size 2048: mflops=5313.7, result=61.8  
5.3137 / 5.4 = 98.4%  
 16
Author: UpAndAdam,
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
2013-04-05 23:04:04

Nie mogę tego odtworzyć, ponieważ mój kompilator (gcc 4.7.2) trzyma total w rejestrze.

Podejrzewam, że główny powód spowolnienia nie ma związku z buforem L1, ale raczej wynika z zależności między danymi w

movsd   %xmm0, -72(%rbp)

I obciążenie kolejnej iteracji:

addsd   -72(%rbp), %xmm0
 8
Author: NPE,
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
2013-03-27 17:56:21