Obliczenia zmiennoprzecinkowe i całkowite na nowoczesnym sprzęcie

100

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 :).

maxpenguin
źródło
8
To, co masz teraz jako test, jest trywialne. Prawdopodobnie jest też bardzo mała różnica w montażu ( addlzastąpiona faddna 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ć rodzaj fixed_pointklasy szablonów, która ogromnie ułatwiłaby taką pracę.
GManNickG
1
Wciąż istnieje wiele architektur, które nie mają dedykowanego sprzętu zmiennoprzecinkowego - niektóre znaczniki wyjaśniające systemy, na których Ci zależy, pomogą Ci uzyskać lepsze odpowiedzi.
Carl Norum
3
Uważam, że sprzęt w moim HTC Hero (Android) nie ma FPU, ale sprzęt w Google NexusOne (Android) ma. jaki jest twój cel komputery stacjonarne / serwery? netbooki (możliwe ARM + Linux)? telefony?
SteelBytes
5
Jeśli chcesz szybkiego FP na x86, spróbuj skompilować z optymalizacją i generowaniem kodu SSE. SSE (bez względu na wersję) może przynajmniej wykonać float dodawanie, odejmowanie i mnożenie w jednym cyklu. Funkcje dzielenia, modyfikowania i wyższych zawsze będą działać wolno. Zauważ również, że floatzwiększa prędkość, ale zwykle doublenie.
Mike D.
1
Liczba całkowita o stałym punkcie przybliża FP przy użyciu wielu operacji na liczbach całkowitych, aby zapobiec przepełnieniu wyników. Jest to prawie zawsze wolniejsze niż użycie niezwykle wydajnych jednostek FPU, które można znaleźć w nowoczesnych procesorach do komputerów stacjonarnych. np. MAD, stałoprzecinkowy dekoder mp3, jest wolniejszy niż libmpg123 i chociaż jest dobrej jakości jak na dekoder stałoprzecinkowy, libmpg123 nadal ma mniejszy błąd zaokrąglenia. wezm.net/technical/2008/04/mp3-decoder-libraries- w porównaniu do testów porównawczych PPC G5.
Peter Cordes,

Odpowiedzi:

35

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.

Dan
źródło
8
A na niektórych procesorach matematyka FP może być szybsza niż matematyka liczb całkowitych. Procesor Alpha miał instrukcję dzielenia FP, ale nie liczbę całkowitą, więc dzielenie liczb całkowitych musiało być wykonane w oprogramowaniu.
Gabe
Czy SSEx przyspieszy także arytmetykę podwójnej precyzji? Przepraszam, nie jestem zbyt zaznajomiony z SSE
Johannes Schaub - litb
1
@ JohannesSchaub-litb: SSE2 ( doublewersja podstawowa dla x86-64) ma spakowane -precision FP. Przy tylko dwóch 64-bitowych doubles na rejestr potencjalne przyspieszenie jest mniejsze niż w floatprzypadku kodu, który dobrze wektoryzuje. Skaluj floati doubleużywaj rejestrów XMM na x86-64, ze starszą wersją x87 używaną tylko dla long 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ów xmm0..15zamiast 8 bajtów mm0..7, a nowoczesne procesory mają gorszą przepustowość MMX niż SSE.)
Peter Cordes
1
Ale instrukcje liczb całkowitych MMX i SSE * / AVX2 konkurują ze sobą o te same jednostki wykonawcze, więc używanie obu naraz prawie nigdy nie jest przydatne. Po prostu użyj szerszych wersji XMM / YMM, aby wykonać więcej pracy. Korzystanie z liczby całkowitej SIMD i FP w tym samym czasie konkuruje o te same rejestry, ale x86-64 ma ich 16. Ale całkowite limity przepustowości oznaczają, że nie można wykonać dwa razy więcej pracy, używając równolegle jednostek wykonawczych typu integer i FP.
Peter Cordes
49

Na przykład (mniejsze liczby są szybsze),

64-bitowy procesor 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 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:

#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;
}
vladr
źródło
8
dlaczego wymieszałeś mult i div? Czy nie powinno być interesujące, jeśli mult jest być może (lub oczekiwano?) Znacznie szybszy niż div?
Kyss Tao
13
Mnożenie jest znacznie szybsze niż dzielenie zarówno w przypadku liczb całkowitych, jak i zmiennoprzecinkowych. Wydajność dzielenia zależy również od wielkości liczb. Zwykle zakładam, że podział jest ~ 15 razy wolniejszy.
Sogartar
4
pastebin.com/Kx8WGUfg Wziąłem twój test porównawczy i oddzieliłem każdą operację do własnej pętli i dodałem, volatileaby się upewnić. Na Win64, FPU jest nieużywany i MSVC nie wygeneruje kod dla niego, więc kompiluje użyciu mulssi divssinstrukcje xmm tam, które są 25x szybciej niż FPU w Win32. Maszyna testowa to Core i5 M 520 @ 2,40 GHz
James Dunne
4
@JamesDunne bądź ostrożny, ponieważ operacje fp vbardzo szybko osiągną 0 lub +/- inf, co może, ale nie musi, być (teoretycznie) traktowane jako specjalny przypadek / fastpatheed przez niektóre implementacje fpu.
vladr,
3
Ten „wzorzec” nie ma równoległości danych dla wykonywania poza kolejnością, ponieważ każda operacja jest wykonywana za pomocą tego samego akumulatora ( v). W najnowszych projektach Intela dzielenie nie jest w ogóle potokowe ( divss/ divpsma opóźnienie 10-14 cykli i taką samą wzajemną przepustowość). mulssjednak 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).
Peter Cordes,
23

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ść.

Ben Voigt
źródło
4
+1 za wskazanie, jak naiwne testy porównawcze mogą dawać pętle zerowe z powodu niezrolowanych operacji na stałych liczbach całkowitych. Ponadto kompilator może całkowicie odrzucić pętlę (liczbę całkowitą lub FP), jeśli wynik nie jest faktycznie używany.
vladr
Wniosek jest taki: należy wywołać funkcję mającą zmienną zapętlającą jako argument. Ponieważ myślę, że żaden kompilator nie byłby w stanie zobaczyć, że funkcja nic nie robi i że wywołanie można zignorować. Ponieważ istnieje narzut wywołania, tylko różnice czasu == (czas zmiennoprzecinkowy - czas całkowity) będą istotne.
GameAlchemist
@GameAlchemist: Wiele kompilatorów eliminuje wywołania pustych funkcji, jako efekt uboczny wstawiania. Musisz dołożyć wszelkich starań, aby temu zapobiec.
Ben Voigt
OP brzmiał tak, jakby mówił o używaniu liczb całkowitych do rzeczy, w których FP byłoby bardziej naturalnym dopasowaniem, więc potrzeba więcej kodu całkowitego, aby osiągnąć ten sam wynik co kod FP. W takim przypadku po prostu użyj FP. Na przykład na sprzęcie z FPU (np. Desktopowy procesor CPU) dekodery MP3 ze stałoprzecinkowymi liczbami całkowitymi są wolniejsze (i nieco więcej błędów zaokrągleń) niż dekodery zmiennoprzecinkowe. Implementacje kodeków ze stałym punktem istnieją głównie po to, aby działać na uproszczonych procesorach ARM bez sprzętu FP, tylko wolno emulowany FP.
Peter Cordes,
jeden przykład dla pierwszego punktu: na x86-64 z AVX-512 jest tylko 16 rejestrów GP, ale 32 rejestry zmm, więc skalarna matematyka zmiennoprzecinkowa może być szybsza
phuclv
18

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.

Potatoswatter
źródło
Tak, a także konwersję z randowej liczby całkowitej na zmiennoprzecinkową w wersji zmiennoprzecinkowej. Jakieś pomysły na lepszy sposób, aby to przetestować?
maxpenguin
1
Jeśli próbujesz profilować prędkość, spójrz na POSIX timespec_tlub coś podobnego. Zapisz czas na początku i na końcu pętli i zrób różnicę. Następnie przenieś randgenerowanie 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.
Mike D.
3
@maxpenguin: pytanie brzmi, co testujesz. Artem założył, że zajmujesz się grafiką, Carl zastanawiał się, czy korzystasz z wbudowanej platformy bez FP, przypuszczałem, że kodujesz naukę dla serwera. Nie można generalizować ani „pisać” testów porównawczych. Testy porównawcze są pobierane z rzeczywistej pracy wykonywanej przez program. Jedną z rzeczy, które mogę wam powiedzieć, jest to, że nie zachowa "zasadniczo tej samej szybkości", jeśli dotknie się elementu krytycznego dla wydajności w programie, cokolwiek to jest.
Potatoswatter
dobra uwaga i dobra odpowiedź. Obecnie posiadamy 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 pomoc.
maxpenguin
18

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

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 (Chromebook Acer C720 z xenialem)

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 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (działający niezawodny)

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

Procesor AMD 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

Używa kodu z http://pastebin.com/Kx8WGUfg asbenchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc 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):

Wykres powyższych danych

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)
    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, dzielony hosting DreamHost)
    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 (Trusty 64-bitowy, 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
MrMesees
źródło
gcc5 może automatycznie wektoryzuje coś, czego nie zrobił gcc4.6? Czy benchmark-pcmierzy 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).
Peter Cordes,
A tak przy okazji, dlaczego nie testuje int, tylko shorti long? Na Linux x86-64 shortwynosi 16 bity (i tym samym jest spowolnienie częściowym rejestru, w niektórych przypadkach), podczas longi long longsą zarówno typu 64-bitowych. (Może jest przeznaczony dla systemu Windows, gdzie x86-64 nadal używa 32-bitowego long? A może jest przeznaczony do trybu 32-bitowego). W systemie Linux x32 ABI ma 32-bitowy longw trybie 64-bitowym , więc jeśli masz zainstalowane biblioteki , użyj gcc -mx32do kompilatora dla ILP32. Lub po prostu użyj -m32i spójrz na longliczby.
Peter Cordes,
I naprawdę powinieneś sprawdzić, czy twój kompilator cokolwiek automatycznie zwektoryzował. np. używając addpszamiast rejestrów xmm addss, aby zrobić 4 FP dodaje równolegle w jednej instrukcji, która jest tak szybka jak skalar addss. (Użyj, -march=nativeaby zezwolić na użycie dowolnych instrukcji obsługiwanych przez procesor, a nie tylko linii bazowej SSE2 dla x86-64).
Peter Cordes,
@cincodenada proszę zostawić wykresy pokazujące pełne 15 z boku, ponieważ są wtedy ilustracją wydajności.
MrMesees
@PeterCordes Postaram się jutro spojrzeć, dziękuję za pracowitość.
MrMesees
7

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 .

jcoder
źródło
Należy zauważyć, że na Win64 instrukcje FPU nie są już generowane przez kompilator MSVC. Liczba zmiennoprzecinkowa zawsze używa tam instrukcji SIMD. Powoduje to dużą rozbieżność szybkości między Win32 i Win64 dotyczącą flopów.
James Dunne,
5

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.

Goran D.
źródło
4

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

Artem Sokolov
źródło
2
Aplikacje naukowe wykorzystują FP. Jedyną zaletą FP jest to, że precyzja jest niezmienna od skali. To jest jak notacja naukowa. Jeśli znasz już skalę liczb (np. Że długość linii jest liczbą pikseli), FP jest pomijane. Ale zanim zaczniesz rysować linię, to nieprawda.
Potatoswatter
4

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.

gnasher729
źródło
Jest tu wiele dzikich spekulacji, nieuwzględniających żadnych wtórnych efektów obecnych w sprzęcie, które często dominują w czasie obliczeń. Niezły punkt wyjścia, ale należy go sprawdzić w każdej aplikacji poprzez profilowanie, a nie nauczać go jako ewangelii.
Ben Voigt
3

Uruchomiłem test, który po prostu dodał 1 do liczby zamiast rand (). Wyniki (na x86-64) były następujące:

  • krótki: 4,260 s
  • int: 4.020s
  • długi długi: 3,350 s
  • pływak: 7,330 s
  • podwójna: 7,210 s
dan04
źródło
1
Źródło, opcje kompilacji i metoda pomiaru czasu? Jestem trochę zaskoczony wynikami.
GManNickG
Ta sama pętla co OP z „rand ()% 365” zastąpionym przez „1”. Brak optymalizacji. Czas użytkownika od polecenia „czas”.
dan04
13
Kluczem jest „brak optymalizacji”. Nigdy nie profilujesz z wyłączoną optymalizacją, zawsze profilujesz w trybie „zwolnij”.
Dean Harding
2
W tym przypadku jednak, wyłączenie optymalizacji wymusza wystąpienie op i jest wykonywane celowo - pętla ma na celu wydłużenie czasu do rozsądnej skali pomiaru. Użycie stałej 1 usuwa koszt rand (). Wystarczająco inteligentny kompilator optymalizujący zobaczy 1 dodane 100 000 000 razy bez wyjścia z pętli i po prostu doda 100000000 w jednej operacji. Taki sposób obchodzi cały cel, prawda?
Stan Rogers
7
@Stan, uczyń zmienną lotną. Nawet inteligentny kompilator optymalizujący powinien wtedy uwzględniać wiele operacji.
vladr
0

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.

James Curran
źródło
1
Proszę rozważyć ponowne spojrzenie na to, oferując coś więcej niż opinię (zwłaszcza biorąc pod uwagę, że opinia wydaje się być
sprzeczna
1
@MrMesees Chociaż ta odpowiedź nie jest zbyt przydatna, powiedziałbym, że jest zgodna z przeprowadzonymi przez Ciebie testami. A ciekawostki historyczne prawdopodobnie też są w porządku.
Jonatan Öström
Jako ktoś, kto kiedyś pracował z 286, mogę potwierdzić; "Tak były!"
David H Parry