2009-07-18 12 views
25

Dekadę lub dwa lata temu warto było napisać kod liczbowy, aby uniknąć używania mnożenia i dzielenia, a zamiast tego używać dodawania i odejmowania. Dobrym przykładem jest użycie forward differences do oceny krzywej wielomianowej zamiast bezpośredniego obliczania wielomianu.Jaka jest względna szybkość dodawania zmiennoprzecinkowego, a nie zmiennoprzecinkowa, mnożenie

Czy tak jest nadal, czy też współczesne architektury komputerów są zaawansowane do punktu, w którym *,/nie są już wielokrotnie wolniejsze niż +, -?

Dla szczególów, interesuje mnie skompilowany kod C/C++ działający na nowoczesnych typowych układach x86 z rozbudowanym pokładowym sprzętem zmiennoprzecinkowym, a nie małym mikro próbującym zrobić FP w oprogramowaniu. Rozumiem, że pipelining i inne ulepszenia architektoniczne wykluczają określoną liczbę cykli, ale nadal chciałbym uzyskać przydatną intuicję.

Odpowiedz

20

Zależy również od zestawu instrukcji. Twój procesor będzie miał w każdej chwili kilka jednostek obliczeniowych, a uzyskasz maksymalną przepustowość, jeśli wszystkie będą cały czas wypełnione. Zatem wykonywanie pętli mul jest równie szybkie jak wykonywanie pętli lub dodaje - ale to samo nie obowiązuje, jeśli wyrażenie staje się bardziej złożone.

na przykład wykorzystać tę pętlę:

for(int j=0;j<NUMITER;j++) { 
    for(int i=1;i<NUMEL;i++) { 
    bla += 2.1 + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; 
    } 
} 

dla NUMITER = 10^7 NUMEL = 10^2, obydwie macierze zainicjowana do niewielkiej liczby dodatnie (Nan jest znacznie mniejsza), odbywa 6,0 sekundy przy użyciu wskaźnika podwaja się na 64-bitowym proc. Gdybym zastąpić pętlę z

bla += 2.1 * arr1[i] + arr2[i] + arr3[i] * arr4[i] ; 

To trwa tylko 1,7 sekundy ... więc skoro my „przesadził” dodatki, na muls były zasadniczo wolny; a pomniejszenie dodatków pomogło. To get bardziej skomplikowane:

bla += 2.1 + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; 

- sama mul/dodaj dystrybucję, ale teraz stała dodaje się zamiast mnoży się - trwa 3,7 sekundy. Twój procesor jest prawdopodobnie zoptymalizowany do wydajniejszego wykonywania typowych obliczeń numerycznych; więc iloczyn iloczynu ilości mulów i skalowanych sum jest tak dobry, jak to tylko możliwe; dodawanie stałych nie jest tak powszechne, więc jest wolniej ...

bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; /*someval == 2.1*/ 

ponownie zajmuje 1,7 sekundy.

bla += someval + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; /*someval == 2.1*/ 

(taki sam jak pętla początkowa, ale bez drogiego stałego dodawania: 2.1 sekundy)

bla += someval * arr1[i] * arr2[i] * arr3[i] * arr4[i] ; /*someval == 2.1*/ 

(głównie muls, ale dodatkowo: 1,9 sek)

więc w zasadzie; Trudno powiedzieć, które z nich jest szybsze, ale jeśli chcesz uniknąć wąskich gardeł, ważniejsze jest posiadanie rozsądnej mieszanki, unikanie NaN lub INF, unikanie dodawania stałych. Niezależnie od tego, co robisz, upewnij się, że testujesz i testujesz różne ustawienia kompilatora, ponieważ często małe zmiany mogą po prostu sprawić różnicę.

Niektóre więcej przypadków:

bla *= someval; // someval very near 1.0; takes 2.1 seconds 
bla *= arr1[i] ;// arr1[i] all very near 1.0; takes 66(!) seconds 
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; // 1.6 seconds 
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, 2.2 seconds 
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, floats 2.2 seconds 
bla += someval * arr1[i]* arr2[i];// 0.9 in x64, 1.6 in x86 
bla += someval * arr1[i];// 0.55 in x64, 0.8 in x86 
bla += arr1[i] * arr2[i];// 0.8 in x64, 0.8 in x86, 0.95 in CLR+x64, 0.8 in CLR+x86 
+1

Zestaw instrukcji to dobry punkt, mam ludzi, z którymi pracuję, którzy nalegają, aby 200-punktowy procesor DSP wykonał 600 stałych punktów DSP. Nie wykonują absolutnie żadnych skomplikowanych operacji na pętli i spędzają więcej czasu na przetwarzaniu I/O niż na obliczeniach. Szybszy stały procesor punktowy wygrywa w oparciu o ogólną kombinację instrukcji, ale ludzie sądzą, że jednostki FP są magią, a nie HW implementacją struktury danych. – NoMoreZealots

+0

Ach tak, magiczna aplikacja ;-) - to niefortunne. –

+1

ładne wyjaśnienie dzięki intuicyjnym przykładom! –

1

Nie mogę znaleźć ostatecznego odniesienia, ale obszerne eksperymenty mówią mi, że obecnie mnożenie pływaka ma prawie taką samą prędkość jak dodawanie i odejmowanie, natomiast podział nie jest (ale nie "wiele razy" wolniejszy). Możesz uzyskać intuicję, której pragniesz, wykonując własne eksperymenty - pamiętaj, aby wygenerować losowe liczby (miliony) z góry, przeczytać je przed rozpoczęciem pomiaru czasu i użyć liczników wydajności procesora (bez żadnego innego procesu uruchomionego, ponieważ tak jak można ich powstrzymać) w celu dokładnego pomiaru!

-1

Istnieje prawdopodobnie niewielka różnica w czasie między mnożeniem a dodawaniem. z drugiej strony podział jest nadal znacznie wolniejszy niż mnożenie ze względu na jego rekurencyjny charakter. na nowoczesnych architekturach architektury x86, instrukcje sse powinny być brane pod uwagę podczas wykonywania operacji zmiennoprzecinkowej zamiast korzystania z fpu. Chociaż dobry kompilator C/C++ powinien dać ci możliwość użycia sse zamiast fpu.

1

Różnica prędkości */vs + - zależy od architektury procesora. Ogólnie rzecz biorąc, w szczególności w przypadku architektury x86, różnica prędkości stała się mniejsza w przypadku nowoczesnych procesorów. * powinno być blisko +, gdy masz wątpliwości: po prostu eksperymentuj. Jeśli masz naprawdę ciężki problem z wieloma operacjami FP, rozważ także użycie procesora graficznego (GeForce, ...), który działa jako procesor wektorowy.

7

Najlepszym sposobem, aby odpowiedzieć na to pytanie, jest napisanie testu porównawczego/profilu przetwarzania, który należy wykonać. Empiryczne powinno być używane teoretycznie, kiedy tylko jest to możliwe. Zwłaszcza gdy jest to łatwe do osiągnięcia.

Jeśli już znasz różne implementacje matematyki, które musisz zrobić, możesz napisać kilka różnych transfermacji kodu matematyki i sprawdzić, gdzie osiąga najwyższą wydajność. Umożliwi to procesorowi/kompilatorowi generowanie różnych strumieni wykonawczych w celu wypełnienia rurociągów procesora i daje konkretną odpowiedź na twoją odpowiedź.

Jeśli interesuje Cię wykonanie instrukcji typu DIV/MUL/ADD/SUB, możesz nawet wrzucić do jakiegoś wbudowanego zestawu, aby dokładnie kontrolować, które warianty tych instrukcji są wykonywane. Musisz jednak upewnić się, że utrzymujesz ruch jednostek wykonawczych o wielu ekranach, aby uzyskać dobry obraz wydajności, z jaką system jest w stanie.

Wykonanie czegoś podobnego pozwoliłoby porównać wydajność wielu wersji procesora, uruchamiając na nich ten sam program, a także umożliwiając uwzględnienie różnic na płycie głównej.

Edit:

Podstawowa architektura a + - jest identyczna. Logicznie biorą w tym samym czasie do obliczenia. * z drugiej strony wymaga wielu warstw, zwykle zbudowanych z "pełnych adderów", aby ukończyć pojedynczą operację.Zapewnia to, że podczas gdy * może być wydany do rurociągu w każdym cyklu, będzie miał większe opóźnienie niż obwód dodawania/odejmowania. Operacja fp/zazwyczaj jest implementowana za pomocą metody aproksymacji, która iteracyjnie zbiega się w kierunku prawidłowej odpowiedzi w czasie. Tego rodzaju przybliżenia są zwykle realizowane poprzez mnożenie. Tak więc dla punktu zmiennoprzecinkowego można ogólnie założyć, że podział zajmie więcej czasu, ponieważ niepraktyczne jest "rozwinięcie" multiplikacji (która jest już dużym obwodem i sama w sobie) do potoku wielu układów mnożnika. Mimo to wydajność danego systemu najlepiej mierzyć za pomocą testów.

16

W teorii informacji jest tutaj:

Intel®64 and IA-32 Architectures Optimization Reference Manual, APPENDIX C INSTRUCTION LATENCY AND THROUGHPUT

Dla każdego procesora one liście, opóźnieniach na FMUL jest bardzo zbliżony do tego z FADD lub FDIV. Na niektórych starszych procesorach FDIV jest 2-3 razy wolniejszy, podczas gdy na nowszych procesorach jest taki sam jak FMUL.

Ostrzeżenia:

  1. Dokument I rzeczywiście związana mówi, że nie można polegać na tych liczb w realnym życiu, ponieważ procesor będzie robić to, co chce robić to szybciej, jeśli jest on poprawny.

  2. Istnieje duża szansa, że ​​Twój kompilator zdecyduje się użyć jednego z wielu nowszych zestawów instrukcji, które mają liczbę mnogą/dzielenie zmiennoprzecinkowe.

  3. To jest skomplikowany dokument, który powinien być odczytany przez autorów kompilacji i mógłbym go źle zrozumieć. Tak jak nie wiem, dlaczego liczba opóźnień FDIV jest zupełnie nieobecna w niektórych procesorach.

+1

Bardzo fajny dokument. Myślę, że jedną rzeczą, która pozostaje spójna (i ten dokument pokazuje to), jest to, że podział jest nadal znacznie wolniejszy niż mnożenie, dodawanie i odejmowanie. Z wyglądu tego dokumentu, opóźnienie podziału o podwójnej precyzji jest 10 razy wolniejsze niż mnożenie. Tak więc, na przykład, uważam, że wywołanie x = y * 0.5 powinno być szybsze niż wywołanie x = y/2. –

+0

@SteveWortham Czy możesz wskazać stronę, na której znalazłeś informację o tym, że fdiv jest 10 razy wolniejsze niż fmul? – 0fnt

+0

@ user247077 - Nie pamiętam. To było kilka lat temu. Jednak w tym dokumencie znajdują się wykresy, które odwołują się do opóźnień wielu różnych poleceń. A FMUL jest zdecydowanie szybszy niż FDIV na tych wykresach. Następnie są DIV r64 i MUL r64 na stronie C-33, które mają dużą przerwę między nimi w opóźnieniu. W zeszłym roku mogłem trafić w te instrukcje (lub odpowiednik AMD), gdy stworzyłem 64-bitową aplikację do porównania różnicy wydajności między mnożeniem i dzieleniem ... http://swortham.blogspot.com/2011/10/how - znacznie szybszy-jest-mnożenie-than.html –