Obliczenia zmiennoprzecinkowe vs integer na nowoczesnym sprzęcie
Wykonuję pewną krytyczną pracę w C++ i obecnie używamy obliczeń całkowitych dla problemów, które są z natury zmiennoprzecinkowe, ponieważ "są szybsze". Powoduje to wiele irytujących problemów i dodaje wiele irytującego kodu.
Teraz, pamiętam jak czytałem o tym, jak obliczenia zmiennoprzecinkowe były tak powolne około około 386 dni, gdzie wierzę (IIRC), że był opcjonalny współprocesor. Ale z pewnością w dzisiejszych czasach z wykładniczo bardziej złożonymi i potężne procesory nie ma różnicy w "prędkości", jeśli wykonujesz obliczenia zmiennoprzecinkowe lub całkowite? Zwłaszcza, że rzeczywisty czas obliczeń jest mały w porównaniu do czegoś takiego jak spowodowanie wstrzymania rurociągu lub pobranie czegoś z pamięci głównej?
Wiem, że poprawną odpowiedzią jest testowanie na docelowym sprzęcie, jaki byłby dobry sposób, aby to przetestować? Napisałem dwa malutkie programy C++ i porównałem ich czas wykonania z "time" na Linuksie, ale rzeczywisty czas wykonania jest zbyt zmienny (nie pomaga mi działa na serwerze wirtualnym). Bez spędzania całego dnia na uruchamianiu setek benchmarków, robieniu wykresów itp. czy jest coś, co mogę zrobić, aby uzyskać rozsądny test prędkości względnej? Jakieś pomysły lub przemyślenia? Całkowicie się mylę?
Programy, których używałem w następujący sposób, nie są identyczne w żaden sposób:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
int accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += rand( ) % 365;
}
std::cout << accum << std::endl;
return 0;
}
Program 2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
float accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( rand( ) % 365 );
}
std::cout << accum << std::endl;
return 0;
}
Z góry dzięki!
Edit: platforma, na której mi zależy, to zwykły x86 lub x86-64 działający na pulpicie Linux i Windows maszyny.
Edit 2 (wklejony z komentarza poniżej): mamy obecnie rozbudowaną bazę kodu. Naprawdę mam pochodzić przeciwko uogólnieniu, że "nie wolno używać float, ponieważ obliczenia integer jest szybsze" - i szukam sposobu (jeśli jest to w ogóle prawda), aby obalić to uogólnione założenie. Zdaję sobie sprawę, że nie da się przewidzieć dokładnego wyniku bez wykonywania całej pracy i profilowania jej później.
W każdym razie, dzięki za wszystkie doskonałe odpowiedzi i pomocy. Zapraszam do dodawania czegokolwiek innego :).
11 answers
Niestety, mogę tylko dać ci odpowiedź "to zależy"...
Z mojego doświadczenia wynika, że wydajność jest bardzo duża...zwłaszcza między liczbami całkowitymi i zmiennoprzecinkowymi. Różni się znacznie w zależności od procesora (nawet w obrębie tej samej rodziny, jak np. x86), ponieważ różne procesory mają różne długości "rurociągów". Ponadto niektóre operacje są na ogół bardzo proste (np. dodawanie) i mają przyspieszoną drogę przez procesor, a inne (np. podział) przyjmują dużo, dużo dłużej.
Druga duża zmienna jest miejscem, w którym znajdują się dane. Jeśli masz tylko kilka wartości do dodania, wszystkie dane mogą znajdować się w pamięci podręcznej, gdzie można je szybko wysłać do procesora. Bardzo, bardzo wolna operacja zmiennoprzecinkowa, która ma już dane w pamięci podręcznej, będzie wielokrotnie szybsza niż operacja integer, w której liczba całkowita musi być skopiowana z pamięci systemowej.
Zakładam, że zadajesz to pytanie, ponieważ pracujesz nad wydajnością krytyczną podanie. Jeśli pracujesz nad architekturą x86 i potrzebujesz dodatkowej wydajności, warto rozważyć użycie rozszerzeń SSE. Może to znacznie przyspieszyć arytmetykę zmiennoprzecinkową z pojedynczą precyzją, ponieważ ta sama operacja może być wykonana na wielu danych jednocześnie, plus istnieje oddzielny * bank rejestrów dla operacji SSE. (Zauważyłem, że w drugim przykładzie użyłeś "float" zamiast "double", co sprawia, że myślę, że używasz matematyki z pojedynczą precyzją).
*uwaga: Używanie starych instrukcji MMX faktycznie spowolniłoby programy, ponieważ te stare instrukcje faktycznie używały tych samych rejestrów co FPU, co uniemożliwiało używanie zarówno FPU, jak i MMX w tym samym czasie.
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-12-19 21:03:42
Na przykład (mniejsze liczby są szybsze),
64-bit Intel Xeon X5550 @ 2.67 GHz, gcc 4.1.2 -O3
short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]
32-bitowy Dwurdzeniowy Procesor AMD Opteron(tm) 265 @ 1.81 GHz, gcc 3.4.6 -O3
short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]
Jak zauważył Dan , nawet po normalizacji częstotliwości zegara (co może być mylące samo w sobie w projektach pipelinowanych), wyniki będą się bardzo różnić w zależności od architektury procesora (Indywidualne ALU/FPU wydajność, jako podobnie jak rzeczywista liczba Alu/FPUdostępna na rdzeń w superscalarze, która wpływa na to, ile niezależnych operacji może wykonać równolegle -- ten ostatni czynnik nie jest wykonywany przez poniższy kod, ponieważ wszystkie poniższe operacje są kolejno zależne.)
Poor man ' s FPU/ALU Operation benchmark:
#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>
double
mygettime(void) {
# ifdef _WIN32
struct _timeb tb;
_ftime(&tb);
return (double)tb.time + (0.001 * (double)tb.millitm);
# else
struct timeval tv;
if(gettimeofday(&tv, 0) < 0) {
perror("oops");
}
return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}
template< typename Type >
void my_test(const char* name) {
Type v = 0;
// Do not use constants or repeating values
// to avoid loop unroll optimizations.
// All values >0 to avoid division by 0
// Perform ten ops/iteration to reduce
// impact of ++i below on measurements
Type v0 = (Type)(rand() % 256)/16 + 1;
Type v1 = (Type)(rand() % 256)/16 + 1;
Type v2 = (Type)(rand() % 256)/16 + 1;
Type v3 = (Type)(rand() % 256)/16 + 1;
Type v4 = (Type)(rand() % 256)/16 + 1;
Type v5 = (Type)(rand() % 256)/16 + 1;
Type v6 = (Type)(rand() % 256)/16 + 1;
Type v7 = (Type)(rand() % 256)/16 + 1;
Type v8 = (Type)(rand() % 256)/16 + 1;
Type v9 = (Type)(rand() % 256)/16 + 1;
double t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v += v0;
v -= v1;
v += v2;
v -= v3;
v += v4;
v -= v5;
v += v6;
v -= v7;
v += v8;
v -= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
// the loop completely
printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
t1 = mygettime();
for (size_t i = 0; i < 100000000; ++i) {
v /= v0;
v *= v1;
v /= v2;
v *= v3;
v /= v4;
v *= v5;
v /= v6;
v *= v7;
v /= v8;
v *= v9;
}
// Pretend we make use of v so compiler doesn't optimize out
// the loop completely
printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}
int main() {
my_test< short >("short");
my_test< long >("long");
my_test< long long >("long long");
my_test< float >("float");
my_test< double >("double");
return 0;
}
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
2018-09-25 08:41:45
Istnieje prawdopodobnie znacząca różnica w rzeczywistej prędkości między matematyką stacjonarną i zmiennoprzecinkową, ale teoretyczna przepustowość ALU vs FPU jest całkowicie nieistotna. Zamiast tego, Liczba rejestrów całkowitych i zmiennoprzecinkowych (rejestrów rzeczywistych, a nie nazw rejestrów) na Twojej architekturze, które nie są używane w innych obliczeniach (np. do sterowania pętlą), liczba elementów każdego typu, które mieszczą się w linii pamięci podręcznej, możliwe optymalizacje biorąc pod uwagę różne semantyki dla liczb całkowitych vs. zmiennoprzecinkowych matematyki -- te efekty będą dominować. Zależności danych Twojego algorytmu odgrywają tutaj znaczącą rolę, więc żadne ogólne porównanie nie przewidzi luki w wydajności Twojego problemu.
Na przykład, dodawanie liczb całkowitych jest przemienne, więc jeśli kompilator widzi pętlę taką jak ta użyta w benchmarku (zakładając, że losowe DANE zostały przygotowane wcześniej, aby nie przesłaniały wyników), może rozwinąć pętlę i obliczyć sumy częściowe za pomocą bez zależności, a następnie dodać je po zakończeniu pętli. Ale w przypadku zmiennoprzecinkowych, kompilator musi wykonywać operacje w tej samej kolejności, o którą prosiłeś(masz tam punkty sekwencji, więc kompilator musi zagwarantować ten sam wynik, co uniemożliwia zmianę kolejności), więc istnieje silna zależność każdego dodawania od wyniku poprzedniego.
Prawdopodobnie zmieścisz więcej liczb całkowitych w pamięci podręcznej na raz. Tak więc wersja ze stałym punktem może przewyższyć wersję float o rząd wielkości nawet na maszynie, w której FPU ma teoretycznie wyższą przepustowość.
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
2010-03-31 05:20:48
Dodawanie jest znacznie szybsze niż rand
, więc twój program jest (szczególnie) bezużyteczny.
Musisz zidentyfikować hotspoty wydajności i stopniowo modyfikować swój program. Wygląda na to, że masz problemy ze środowiskiem programistycznym, które trzeba najpierw rozwiązać. Czy niemożliwe jest uruchomienie programu na komputerze dla małego zestawu problemów?
Ogólnie rzecz biorąc, próba zadania FP z arytmetyką całkowitą jest przepisem na powolne.
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
2010-03-31 03:24:05
Dopóki to się nie zmieni (dużo). Oto kilka wyników przy użyciu kompilatora gnu (btw sprawdzałem również kompilując na maszynach, gnu g++ 5.4 od xeniala jest o wiele szybszy niż 4.6.3 od linaro na precise)
Intel i7 4700MQ xenial
short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173
Intel i3 2370M ma podobne wyniki
short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484
Intel (R) Celeron (R )2955U (Acer C720 Chromebook z systemem xenial)
short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226
DigitalOcean 1GB Droplet intel(R) Xeon(R) CPU E5-2630L v2 (running trusty)
short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649
AMD Procesor Opteron (tm) 4122 (precyzyjny)
short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536
To używa kodu z http://pastebin.com/Kx8WGUfg jako benchmark-pc.c
g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c
Przeprowadziłem wiele podań, ale wygląda na to, że liczby ogólne są takie same.
Jeden zauważalny wyjątek wydaje się być Alu mul vs FPU mul. Dodawanie i odejmowanie wydają się trywialnie różne.
Oto powyższy w formie wykresu (kliknij na pełny rozmiar, niżej jest szybciej i "preferent"): {]}
Update to accommodate @Peter Cordes
Https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu xenial 64-bit (wszystkie poprawki do 2018-03-13 zastosowane) short add: 0.773049
short sub: 0.789793
short mul: 0.960152
short div: 3.273668
int add: 0.837695
int sub: 0.804066
int mul: 0.960840
int div: 3.281113
long add: 0.829946
long sub: 0.829168
long mul: 0.960717
long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
float add: 1.081649
float sub: 1.080351
float mul: 1.323401
float div: 1.984582
double add: 1.081079
double sub: 1.082572
double mul: 1.323857
double div: 1.968488
Procesor AMD Opteron (tm) 4122 (precyzyjny, DreamHost shared-hosting)
short add: 1.235603
short sub: 1.235017
short mul: 1.280661
short div: 5.535520
int add: 1.233110
int sub: 1.232561
int mul: 1.280593
int div: 5.350998
long add: 1.281022
long sub: 1.251045
long mul: 1.834241
long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
float add: 2.307852
float sub: 2.305122
float mul: 2.298346
float div: 4.833562
double add: 2.305454
double sub: 2.307195
double mul: 2.302797
double div: 5.485736
Intel Xeon E5-2630L v2 @ 2.4 GHz (zaufany 64-bit, DigitalOcean VPS)
short add: 1.040745
short sub: 0.998255
short mul: 1.240751
short div: 3.900671
int add: 1.054430
int sub: 1.000328
int mul: 1.250496
int div: 3.904415
long add: 0.995786
long sub: 1.021743
long mul: 1.335557
long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
float add: 1.572640
float sub: 1.532714
float mul: 1.864489
float div: 2.825330
double add: 1.535827
double sub: 1.535055
double mul: 1.881584
double div: 2.777245
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
2018-03-13 18:08:21
Dwa punkty do rozważenia -
Nowoczesny sprzęt może nakładać instrukcje, wykonywać je równolegle i zmieniać ich kolejność, aby jak najlepiej wykorzystać sprzęt. Ponadto, każdy znaczący program zmiennoprzecinkowy może mieć również znaczącą pracę całkowitą, nawet jeśli oblicza tylko indeksy na tablice, licznik pętli itp. więc nawet jeśli masz wolną instrukcję zmiennoprzecinkową, może ona być uruchomiona na oddzielnym bitie sprzętowym nakładającym się na Część pracy całkowitej. Chodzi mi o to, że nawet jeśli instrukcje zmiennoprzecinkowe są powolne niż te całkowite, ogólny program może działać szybciej, ponieważ może wykorzystać więcej sprzętu.
Jak zawsze, jedynym sposobem na upewnienie się jest profilowanie rzeczywistego programu.
Druga kwestia polega na tym, że większość procesorów w dzisiejszych czasach ma instrukcje SIMD dla zmiennoprzecinkowych, które mogą działać na wielu wartościach zmiennoprzecinkowych w tym samym czasie. Na przykład można załadować 4 pływaki do jednego rejestru SSE i wykonać 4 mnożenia na nich wszystkich równolegle. Jeśli możesz przepisać części kodu, aby używać instrukcji SSE, to wydaje się, że będzie to szybsze niż wersja integer. Visual c++ dostarcza wbudowane funkcje kompilatora, zobacz http://msdn.microsoft.com/en-us/library/x5c07e2a(V = VS. 80). aspx dla niektórych informacji.
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
2010-03-31 08:11:44
Przeprowadziłem test, który właśnie dodał 1 do liczby zamiast rand (). Wyniki (na x86-64) były następujące:
- krótki: 4.260 s
- int: 4.020 s
- long long: 3.350 s
- float: 7.330 s
- double: 7.210 s
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
2010-03-31 04:47:24
O ile nie piszesz kodu, który będzie wywoływany miliony razy na sekundę (np. rysowanie linii na ekran w aplikacji graficznej), wąskim gardłem rzadko jest liczba całkowita a arytmetyka zmiennoprzecinkowa.
Zwykle pierwszym krokiem do pytań dotyczących wydajności jest profilowanie kodu, aby zobaczyć, gdzie naprawdę spędzony jest czas. Polecenie linuksowe to gprof
.
Edit:
Chociaż przypuszczam, że zawsze można zaimplementować algorytm rysowania linii za pomocą liczby całkowite i zmiennoprzecinkowe, nazwij to dużą liczbą razy i sprawdź, czy to robi różnicę:
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
2010-03-31 05:05:50
Wersja zmiennoprzecinkowa będzie znacznie wolniejsza, jeśli nie ma operacji pozostałej. Ponieważ wszystkie dodawania są sekwencyjne, procesor nie będzie w stanie wykonać równoległego podsumowania. Opóźnienie będzie krytyczne. Opóźnienie dodawania FPU wynosi zwykle 3 cykle, podczas gdy integer add to 1 cykl. Jednak dzielnik dla operatora reszty będzie prawdopodobnie częścią krytyczną, ponieważ nie jest w pełni pipelinowany na nowoczesnych procesorach. tak więc, zakładając, że instrukcja divide / Rest pochłonie większość czasu, różnica z powodu opóźnienia dodania będzie niewielka.
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
2014-12-22 16:31:18
Obecnie operacje liczb całkowitych są zwykle nieco szybsze niż operacje zmiennoprzecinkowe. Jeśli więc możesz wykonać obliczenia z tymi samymi operacjami w liczbie całkowitej i zmiennoprzecinkowej, użyj liczby całkowitej. Jednak mówisz "to powoduje wiele irytujących problemów i dodaje wiele irytującego kodu". Wygląda na to, że potrzebujesz więcej operacji, ponieważ używasz arytmetyki całkowitej zamiast zmiennoprzecinkowej. W takim przypadku zmiennoprzecinkowy będzie działał szybciej, ponieważ
-
Jak najszybciej więcej operacji integer, prawdopodobnie potrzebujesz o wiele więcej, więc niewielka przewaga prędkości jest więcej niż zjedzona przez dodatkowe operacje
Kod zmiennoprzecinkowy jest prostszy, co oznacza, że pisanie kodu jest szybsze, co oznacza, że jeśli szybkość jest krytyczna, możesz poświęcić więcej czasu na optymalizację kodu.
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
2018-05-28 18:16:49
Bazując na tym oh-so-niezawodnym "czymś, co słyszałem", w dawnych czasach obliczanie liczb całkowitych było około 20 do 50 razy szybsze niż zmiennoprzecinkowe, a obecnie jest mniej niż dwa razy szybsze.
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
2010-03-31 03:24:18