Dlaczego ten fragment kodu
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0.1f; // <--
y[i] = y[i] - 0.1f; // <--
}
}
uruchomić ponad 10 razy szybciej niż następujący bit (identyczny, chyba że zaznaczono inaczej)?
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0; // <--
y[i] = y[i] - 0; // <--
}
}
podczas kompilacji z Visual Studio 2010 SP1. Poziom optymalizacji było -02
z sse2
włączona. Nie testowałem z innymi kompilatorami.
0
,0f
,0d
, lub nawet(int)0
w sytuacji, gdydouble
jest to potrzebne.Odpowiedzi:
Witamy w świecie zdominowanych zmiennoprzecinkowych ! Mogą siać spustoszenie w wydajności!
Liczby normalne (lub subnormalne) są rodzajem hackowania, aby uzyskać dodatkowe wartości bardzo zbliżone do zera z reprezentacji zmiennoprzecinkowej. Operacje na znormalizowanym zmiennoprzecinkowym mogą być dziesiątki do setek razy wolniejsze niż na znormalizowanym zmiennoprzecinkowym. Wynika to z faktu, że wiele procesorów nie radzi sobie z nimi bezpośrednio i musi je przechwycić i rozwiązać za pomocą mikrokodu.
Jeśli wydrukujesz liczby po 10 000 iteracjach, zobaczysz, że zbiegły się one w różne wartości w zależności od tego,
0
czy0.1
są używane.Oto kod testowy skompilowany na x64:
Wynik:
Zwróć uwagę, że w drugim przebiegu liczby są bardzo bliskie zeru.
Numery zdormalizowane są na ogół rzadkie, dlatego większość procesorów nie próbuje ich obsługiwać skutecznie.
Aby zademonstrować, że ma to wszystko wspólnego z liczbami zdenormalizowanymi, jeśli wyzerujemy wartości normalne do zera , dodając to na początku kodu:
Wtedy wersja z
0
nie jest już 10 razy wolniejsza i faktycznie staje się szybsza. (Wymaga to kompilacji kodu z włączoną obsługą SSE).Oznacza to, że zamiast używać tych dziwnych, prawie zerowych wartości o mniejszej precyzji, po prostu zaokrąglamy do zera.
Czasy pracy: Core i7 920 @ 3,5 GHz:
Ostatecznie nie ma to nic wspólnego z liczbą całkowitą czy zmiennoprzecinkową. Symbol
0
lub0.1f
jest konwertowany / przechowywany w rejestrze poza obiema pętlami. To nie ma wpływu na wydajność.źródło
+ 0.0f
nie optymalizuje. Gdybym musiał zgadywać, mogłoby+ 0.0f
to mieć skutki uboczne, gdyby okazało się, żey[i]
jest to sygnałNaN
lub coś w tym rodzaju ... Mógłbym się jednak mylić.Użycie
gcc
i zastosowanie różnicy do wygenerowanego zestawu daje tylko tę różnicę:Ten
cvtsi2ssq
jest rzeczywiście 10 razy wolniejszy.Najwyraźniej
float
wersja używa rejestru XMM załadowanego z pamięci, podczas gdyint
wersja konwertujeint
wartość rzeczywistą 0 nafloat
użyciecvtsi2ssq
instrukcji, co zajmuje dużo czasu. Przekazywanie-O3
do gcc nie pomaga. (gcc wersja 4.2.1.)(Używanie
double
zamiastfloat
nie ma znaczenia, z wyjątkiem tego, że zmienia sięcvtsi2ssq
w acvtsi2sdq
.)Aktualizacja
Niektóre dodatkowe testy pokazują, że niekoniecznie jest to
cvtsi2ssq
instrukcja. Po wyeliminowaniu (użycie aint ai=0;float a=ai;
i użyciea
zamiast0
) różnica prędkości pozostaje. Więc @Mysticial ma rację, denormalizowane zmiennoprzecinkowe robią różnicę. Można to zobaczyć, testując wartości pomiędzy0
i0.1f
. Punktem zwrotnym w powyższym kodzie jest w przybliżeniu0.00000000000000000000000000000001
moment, w którym pętle nagle trwają 10 razy dłużej.Zaktualizuj << 1
Mała wizualizacja tego interesującego zjawiska:
Wyraźnie widać, że wykładnik (ostatnie 9 bitów) zmienia się na najniższą wartość, gdy zaczyna się denormalizacja. W tym momencie proste dodawanie staje się 20 razy wolniejsze.
Równoważną dyskusję na temat ARM można znaleźć w pytaniu Przepełnienie stosu Denormalizowany zmiennoprzecinkowy w Objective-C? .
źródło
-O
nie naprawiają tego, ale-ffast-math
tak. (Używam tego cały czas, IMO narożne przypadki, w których powoduje problemy z precyzją, nie powinny i tak-ffast-math
linkami dodatkowego kodu startowego, który ustawia FTZ (kolor na zero) i DAZ (denormalne są zero) w MXCSR, więc procesor nigdy nie musi przyjmować wolnej pomocy mikrokodu dla normalnych.Wynika to ze znormalizowanego użycia zmiennoprzecinkowego. Jak się go pozbyć i stracić wydajność? Po przeszukaniu Internetu w poszukiwaniu sposobów zabijania liczb normalnych wydaje się, że nie ma jeszcze „najlepszego” sposobu na zrobienie tego. Znalazłem te trzy metody, które mogą najlepiej działać w różnych środowiskach:
Może nie działać w niektórych środowiskach GCC:
Może nie działać w niektórych środowiskach Visual Studio: 1
Wydaje się działać zarówno w GCC, jak i Visual Studio:
Kompilator Intel ma opcje domyślnego wyłączania denormałów w nowoczesnych procesorach Intel. Więcej informacji tutaj
Przełączniki kompilatora.
-ffast-math
,-msse
lub-mfpmath=sse
wyłączy denormale i przyspieszy kilka innych rzeczy, ale niestety zrobi też wiele innych przybliżeń, które mogą uszkodzić twój kod. Przetestuj dokładnie! Odpowiednikiem szybkiej matematyki dla kompilatora Visual Studio jest,/fp:fast
ale nie byłem w stanie potwierdzić, czy to również wyłącza denormale. 1źródło
W gcc możesz włączyć FTZ i DAZ za pomocą:
użyj także przełączników gcc: -msse -mfpmath = sse
(odpowiadające kredyty Carlowi Hetheringtonowi [1])
[1] http://carlh.net/plugins/denormals.php
źródło
fesetround()
zfenv.h
(zdefiniowany dla C99) na inny, bardziej przenośny sposób zaokrąglania ( linux.die.net/man/3/fesetround ) (ale to wpłynie na wszystkie operacje PF, nie tylko subnormals )Komentarz Dana Neely'a powinien zostać rozszerzony na odpowiedź:
To nie stała stała
0.0f
jest zdenormalizowana lub powoduje spowolnienie, to wartości zbliżają się do zera przy każdej iteracji pętli. Gdy zbliżają się coraz bardziej do zera, potrzebują większej precyzji do reprezentacji i stają się denormalizowane. To sąy[i]
wartości. (Zbliżają się do zera, ponieważx[i]/z[i]
dla wszystkich jest mniejsza niż 1,0i
).Zasadniczą różnicą między wolnymi i szybkimi wersjami kodu jest instrukcja
y[i] = y[i] + 0.1f;
. Gdy tylko ta linia zostanie wykonana po każdej iteracji pętli, dodatkowa precyzja w liczbach zmiennoprzecinkowych zostanie utracona, a denormalizacja potrzebna do przedstawienia tej precyzji nie jest już potrzebna. Następnie operacje zmiennoprzecinkowey[i]
pozostają szybkie, ponieważ nie są zdenormalizowane.Dlaczego dodajesz dodatkową precyzję
0.1f
? Ponieważ liczby zmiennoprzecinkowe mają tylko tyle cyfr znaczących. Załóżmy, że masz wystarczająco dużo pamięci masowej dla trzech cyfr znaczących, potem0.00001 = 1e-5
, i0.00001 + 0.1 = 0.1
, przynajmniej dla tego przykładu formacie pływaka, ponieważ nie ma miejsca do przechowywania najmniej znaczący bit w0.10001
.Krótko mówiąc,
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
nie jest to żadna operacja, o której możesz pomyśleć.Mystical również to powiedział : zawartość pływaków ma znaczenie, a nie tylko kod asemblera.
źródło