Wykonuję pewne prace krytyczne dla wydajności w C ++ i obecnie używamy obliczeń całkowitych do problemów, które są z natury zmiennoprzecinkowe, ponieważ „jest szybsze”. Powoduje to wiele irytujących problemów i dodaje dużo irytującego kodu.
Pamiętam, jak czytałem o tym, jak obliczenia zmiennoprzecinkowe były tak powolne przez około 386 dni, kiedy wierzę (IIRC), że istniał opcjonalny współprocesor. Ale z pewnością w dzisiejszych czasach, przy wykładniczo bardziej złożonych i wydajnych procesorach, nie ma różnicy w szybkości, jeśli wykonujesz obliczenia zmiennoprzecinkowe lub całkowite? Zwłaszcza, że faktyczny czas obliczeń jest niewielki w porównaniu z czymś takim, jak spowodowanie zablokowania rurociągu lub pobranie czegoś z pamięci głównej?
Wiem, że poprawną odpowiedzią jest test porównawczy na sprzęcie docelowym. Jaki byłby dobry sposób na przetestowanie tego? Napisałem dwa malutkie programy w C ++ i porównałem ich czas pracy z „czasem” w Linuksie, ale rzeczywisty czas wykonywania jest zbyt zmienny (nie pomaga, gdy pracuję na serwerze wirtualnym). Oprócz spędzenia całego dnia na wykonywaniu setek testów porównawczych, tworzeniu wykresów itp., Czy jest coś, co mogę zrobić, aby uzyskać rozsądny test względnej prędkości? Jakieś pomysły lub przemyślenia? Czy całkowicie się mylę?
Programy, których użyłem w następujący sposób, nie są w żaden sposób identyczne:
#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ękuję!
Edycja: Platforma, na której mi zależy, to zwykła x86 lub x86-64 działająca na komputerach stacjonarnych z systemem Linux i Windows.
Edycja 2 (wklejona z komentarza poniżej): Obecnie mamy obszerną bazę kodu. Naprawdę spotkałem się z uogólnieniem, że „nie wolno używać liczby zmiennoprzecinkowej, ponieważ obliczanie liczb całkowitych jest szybsze” - i szukam sposobu (jeśli to w ogóle prawda), aby obalić to uogólnione założenie. Zdaję sobie sprawę, że niemożliwe byłoby przewidzenie dokładnego wyniku dla nas bez wykonania całej pracy i późniejszego sprofilowania.
W każdym razie dziękuję za wszystkie doskonałe odpowiedzi i pomoc. Zapraszam do dodania czegoś jeszcze :).
źródło
addl
zastąpionafadd
na przykład). Jedynym sposobem na uzyskanie naprawdę dobrego pomiaru jest zdobycie podstawowej części swojego prawdziwego programu i przedstawienie jego różnych wersji. Niestety może to być dość trudne bez dużego wysiłku. Być może poinformowanie nas o docelowym sprzęcie i kompilatorze pomogłoby ludziom przynajmniej dać ci wcześniejsze doświadczenie, itp. Jeśli chodzi o używanie liczb całkowitych, podejrzewam, że mógłbyś stworzyć rodzajfixed_point
klasy szablonów, która ogromnie ułatwiłaby taką pracę.float
zwiększa prędkość, ale zwykledouble
nie.Odpowiedzi:
Niestety, mogę tylko udzielić odpowiedzi „to zależy” ...
Z mojego doświadczenia wynika, że wydajność ma wiele, wiele zmiennych ... zwłaszcza między matematyką całkowitą i zmiennoprzecinkową. Różni się znacznie w zależności od procesora (nawet w tej samej rodzinie, na przykład x86), ponieważ różne procesory mają różne długości „potoków”. Ponadto niektóre operacje są ogólnie bardzo proste (takie jak dodawanie) i mają przyspieszoną trasę przez procesor, a inne (takie jak dzielenie) trwają znacznie, znacznie dłużej.
Inną dużą zmienną jest miejsce, 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, skąd można je szybko wysłać do procesora. Bardzo, bardzo powolna operacja zmiennoprzecinkowa, która ma już dane w pamięci podręcznej, będzie wielokrotnie szybsza niż operacja na liczbach całkowitych, w przypadku której należy skopiować liczbę całkowitą z pamięci systemowej.
Zakładam, że zadajesz to pytanie, ponieważ pracujesz nad aplikacją krytyczną dla wydajności. Jeśli tworzysz dla architektury x86 i potrzebujesz dodatkowej wydajności, możesz chcieć skorzystać z rozszerzeń SSE. Może to znacznie przyspieszyć arytmetykę zmiennoprzecinkową o pojedynczej precyzji, ponieważ ta sama operacja może być wykonywana na wielu danych jednocześnie, a ponadto istnieje oddzielny * bank rejestrów dla operacji SSE. (Zauważyłem, że w twoim drugim przykładzie użyłeś "float" zamiast "double", co sprawia, że myślę, że używasz matematyki pojedynczej precyzji).
* Uwaga: używanie starych instrukcji MMX w rzeczywistości spowolniłoby programy, ponieważ te stare instrukcje faktycznie korzystały z tych samych rejestrów co FPU, co uniemożliwia jednoczesne użycie FPU i MMX.
źródło
double
wersja podstawowa dla x86-64) ma spakowane -precision FP. Przy tylko dwóch 64-bitowychdouble
s na rejestr potencjalne przyspieszenie jest mniejsze niż wfloat
przypadku kodu, który dobrze wektoryzuje. Skalujfloat
idouble
używaj rejestrów XMM na x86-64, ze starszą wersją x87 używaną tylko dlalong double
. (Więc @ Dan: nie, rejestry MMX nie kolidują z normalnymi rejestrami FPU, ponieważ normalna FPU na x86-64 jest jednostką SSE. MMX byłby bezcelowy, ponieważ jeśli możesz wykonać SIMD w liczbach całkowitych, potrzebujesz 16 bajtówxmm0..15
zamiast 8 bajtówmm0..7
, a nowoczesne procesory mają gorszą przepustowość MMX niż SSE.)Na przykład (mniejsze liczby są szybsze),
64-bitowy procesor Intel Xeon X5550 @ 2,67 GHz, gcc 4.1.2
-O3
32-bitowy dwurdzeniowy procesor AMD Opteron (tm) 265 @ 1,81 GHz, gcc 3.4.6
-O3
Jak zauważył Dan , nawet po znormalizowaniu częstotliwości zegara (co może być mylące samo w sobie w projektach potokowych), wyniki będą się znacznie różnić w zależności od architektury procesora (indywidualna wydajność ALU / FPU , a także rzeczywista liczba jednostek ALU / FPU dostępnych na rdzeń w projektach superskalarnych, który wpływa na to, ile niezależnych operacji może być wykonywanych równolegle - ten ostatni czynnik nie jest wykonywany przez poniższy kod, ponieważ wszystkie poniższe operacje są sekwencyjnie zależne.)
Wzorzec działania FPU / ALU dla biednych:
źródło
volatile
aby się upewnić. Na Win64, FPU jest nieużywany i MSVC nie wygeneruje kod dla niego, więc kompiluje użyciumulss
idivss
instrukcje xmm tam, które są 25x szybciej niż FPU w Win32. Maszyna testowa to Core i5 M 520 @ 2,40 GHzv
bardzo szybko osiągną 0 lub +/- inf, co może, ale nie musi, być (teoretycznie) traktowane jako specjalny przypadek / fastpatheed przez niektóre implementacje fpu.v
). W najnowszych projektach Intela dzielenie nie jest w ogóle potokowe (divss
/divps
ma opóźnienie 10-14 cykli i taką samą wzajemną przepustowość).mulss
jednak jest to opóźnienie 5 cykli, ale może wydać jeden w każdym cyklu. (Lub dwa na cykl w Haswell, ponieważ port 0 i port 1 mają mnożnik FMA).Prawdopodobnie istnieje znacząca różnica w rzeczywistej prędkości między matematyką stałoprzecinkową i zmiennoprzecinkową, ale teoretyczna przepustowość ALU w porównaniu z FPU jest całkowicie nieistotna. Zamiast tego liczba rejestrów całkowitych i zmiennoprzecinkowych (rejestrów rzeczywistych, a nie nazw rejestrów) w twojej architekturze, które nie są w inny sposób używane przez twoje obliczenia (np. Do sterowania pętlą), liczba elementów każdego typu, które mieszczą się w wierszu pamięci podręcznej , optymalizacje możliwe, biorąc pod uwagę różną semantykę dla matematyki całkowitej i zmiennoprzecinkowej - te efekty będą dominować. Zależności danych twojego algorytmu odgrywają tutaj znaczącą rolę, więc żadne ogólne porównanie nie pozwoli przewidzieć luki w wydajności twojego problemu.
Na przykład dodawanie liczb całkowitych jest przemienne, więc jeśli kompilator widzi pętlę podobną do użytej do testu porównawczego (zakładając, że dane losowe zostały przygotowane wcześniej, aby nie przesłaniały wyników), może rozwinąć pętlę i obliczyć częściowe sumy za pomocą brak zależności, a następnie dodaj je po zakończeniu pętli. Ale w przypadku zmiennoprzecinkowych kompilator musi wykonać operacje w tej samej kolejności, o którą prosiłeś (masz tam punkty sekwencji, więc kompilator musi zagwarantować ten sam wynik, co nie pozwala na zmianę kolejności), więc istnieje silna zależność każdego dodawania od wynik poprzedniego.
Prawdopodobnie zmieścisz również więcej operandów całkowitych w pamięci podręcznej naraz. Tak więc wersja stałoprzecinkowa może przewyższać wersję zmiennoprzecinkową o rząd wielkości nawet na maszynie, w której FPU ma teoretycznie większą przepustowość.
źródło
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 należy najpierw rozwiązać. Czy nie można uruchomić programu na komputerze z powodu małego zestawu problemów?
Ogólnie rzecz biorąc, próba zadań FP z arytmetyką liczb całkowitych jest przepisem na powolne.
źródło
timespec_t
lub coś podobnego. Zapisz czas na początku i na końcu pętli i zrób różnicę. Następnie przenieśrand
generowanie danych z pętli. Upewnij się, że algorytm pobiera wszystkie dane z tablic i umieszcza wszystkie dane w tablicach. To samo pobiera twój rzeczywisty algorytm i pobiera konfigurację, malloc, drukowanie wyników, wszystko oprócz przełączania zadań i przerywania pętli profilowania.TIL To się zmienia (bardzo). Oto kilka wyników przy użyciu kompilatora gnu (btw sprawdziłem też kompilując na maszynach, gnu g ++ 5.4 z xeniala jest piekielnie dużo szybsze niż 4.6.3 z linaro na precyzyjnym)
Intel i7 4700MQ xenial
Intel i3 2370M ma podobne wyniki
Intel (R) Celeron (R) 2955U (Chromebook Acer C720 z xenialem)
DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (działający niezawodny)
Procesor AMD Opteron (tm) 4122 (precyzyjny)
Używa kodu z http://pastebin.com/Kx8WGUfg as
benchmark-pc.c
Uruchomiłem wiele przebiegów, ale wydaje się, że ogólne liczby są takie same.
Jednym godnym uwagi wyjątkiem wydaje się być ALU mul vs FPU mul. Dodawanie i odejmowanie wydają się banalnie różne.
Oto powyższe w formie wykresu (kliknij, aby wyświetlić pełny rozmiar, niższy jest szybszy i lepszy):
Zaktualizuj, aby dostosować się do @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64-bit (zastosowano wszystkie poprawki do 2018-03-13) Procesor AMD Opteron (tm) 4122 (precyzyjny, dzielony hosting DreamHost) Intel Xeon E5-2630L v2 @ 2,4 GHz (Trusty 64-bitowy, DigitalOcean VPS)źródło
benchmark-pc
mierzy jakąś kombinację przepustowości i opóźnienia? W Twoim Haswell (i7 4700MQ) mnożenie liczb całkowitych wynosi 1 na przepustowość zegara, 3 cykle opóźnienia, ale liczba całkowita add / sub to 4 na przepustowość zegara, 1 cykl latencji ( agner.org/optimize ). Tak więc przypuszczalnie jest dużo pętli, które osłabiają te liczby, aby add i mul wypadły tak blisko (długi add: 0,824088 vs. długi mul: 1,017164). (gcc domyślnie nie rozwija pętli, z wyjątkiem pełnego rozwijania bardzo niskich zliczeń iteracji).int
, tylkoshort
ilong
? Na Linux x86-64short
wynosi 16 bity (i tym samym jest spowolnienie częściowym rejestru, w niektórych przypadkach), podczaslong
ilong long
są zarówno typu 64-bitowych. (Może jest przeznaczony dla systemu Windows, gdzie x86-64 nadal używa 32-bitowegolong
? A może jest przeznaczony do trybu 32-bitowego). W systemie Linux x32 ABI ma 32-bitowylong
w trybie 64-bitowym , więc jeśli masz zainstalowane biblioteki , użyjgcc -mx32
do kompilatora dla ILP32. Lub po prostu użyj-m32
i spójrz nalong
liczby.addps
zamiast rejestrów xmmaddss
, aby zrobić 4 FP dodaje równolegle w jednej instrukcji, która jest tak szybka jak skalaraddss
. (Użyj,-march=native
aby zezwolić na użycie dowolnych instrukcji obsługiwanych przez procesor, a nie tylko linii bazowej SSE2 dla x86-64).Dwie kwestie do rozważenia -
Nowoczesny sprzęt może nakładać się na instrukcje, wykonywać je równolegle i zmieniać ich kolejność, aby jak najlepiej wykorzystać sprzęt. Ponadto każdy znaczący program zmiennoprzecinkowy prawdopodobnie będzie miał również znaczącą pracę na liczbach całkowitych, nawet jeśli oblicza indeksy tylko w tablicach, licznikach pętli itp., Więc nawet jeśli masz wolną instrukcję zmiennoprzecinkową, może on działać na oddzielnym sprzęcie pokrywał się z częścią pracy na liczbach całkowitych. Chodzi mi o to, że nawet jeśli instrukcje zmiennoprzecinkowe są powolne niż instrukcje całkowite, ogólny program może działać szybciej, ponieważ może wykorzystywać więcej sprzętu.
Jak zawsze, jedynym sposobem na upewnienie się jest utworzenie profilu programu.
Po drugie, większość procesorów w dzisiejszych czasach ma instrukcje SIMD dla zmiennoprzecinkowych, które mogą operować na wielu wartościach zmiennoprzecinkowych w tym samym czasie. Na przykład możesz załadować 4 liczby zmiennoprzecinkowe do jednego rejestru SSE i wykonać 4 mnożenia na nich wszystkich równolegle. Jeśli możesz przepisać części swojego kodu, aby używać instrukcji SSE, wydaje się, że będzie to szybsze niż wersja z liczbami całkowitymi. Visual C ++ udostępnia wbudowane funkcje kompilatora, które to umożliwiają. Więcej informacji można znaleźć pod adresem http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx .
źródło
Wersja zmiennoprzecinkowa będzie znacznie wolniejsza, jeśli nie zostanie wykonana żadna operacja. Ponieważ wszystkie dodania są sekwencyjne, procesor nie będzie w stanie zrównoleglenie sumowania. Opóźnienie będzie krytyczne. Opóźnienie dodawania FPU wynosi zazwyczaj 3 cykle, podczas gdy dodawanie liczby całkowitej to 1 cykl. Jednak rozdzielacz dla pozostałego operatora będzie prawdopodobnie częścią krytyczną, ponieważ nie jest w pełni potokowany na nowoczesnych procesorach. więc zakładając, że instrukcja dziel / reszta zajmie większość czasu, różnica wynikająca z opóźnienia dodawania będzie niewielka.
źródło
O ile nie piszesz kodu, który będzie wywoływany miliony razy na sekundę (np. Rysowanie linii na ekranie w aplikacji graficznej), arytmetyka liczb całkowitych i zmiennoprzecinkowych rzadko jest wąskim gardłem.
Zwykłym pierwszym krokiem do pytań dotyczących wydajności jest profilowanie kodu, aby zobaczyć, gdzie naprawdę spędza się czas wykonywania. Polecenie linux do tego to
gprof
.Edytować:
Chociaż przypuszczam, że zawsze możesz zaimplementować algorytm rysowania linii przy użyciu liczb całkowitych i liczb zmiennoprzecinkowych, wywołaj go dużą liczbę razy i zobacz, czy to robi różnicę:
http://en.wikipedia.org/wiki/Bresenham's_algorithm
źródło
Obecnie operacje na liczbach całkowitych są zwykle nieco szybsze niż operacje zmiennoprzecinkowe. Jeśli więc możesz wykonać obliczenia z tymi samymi operacjami na liczbach całkowitych i zmiennoprzecinkowych, użyj liczby całkowitej. JEDNAK mówisz "Powoduje to wiele irytujących problemów i dodaje dużo irytującego kodu". Wygląda na to, że potrzebujesz więcej operacji, ponieważ używasz arytmetyki liczb całkowitych zamiast liczb zmiennoprzecinkowych. W takim przypadku zmiennoprzecinkowy będzie działał szybciej, ponieważ
gdy będziesz potrzebować więcej operacji na liczbach całkowitych, prawdopodobnie będziesz potrzebować znacznie więcej, więc niewielka przewaga szybkości jest więcej niż pochłaniana przez dodatkowe operacje
kod zmiennoprzecinkowy jest prostszy, co oznacza, że pisanie kodu jest szybsze, co oznacza, że jeśli jest on krytyczny pod względem szybkości, można poświęcić więcej czasu na optymalizację kodu.
źródło
Uruchomiłem test, który po prostu dodał 1 do liczby zamiast rand (). Wyniki (na x86-64) były następujące:
źródło
Opierając się na tym, och, tak niezawodnym "czymś, co słyszałem", w dawnych czasach obliczenia liczb całkowitych były około 20 do 50 razy szybsze niż zmiennoprzecinkowe, a obecnie jest mniej niż dwa razy szybsze.
źródło