Obecnie nie powinno być problemu z użyciem kompilatora C ++ 11, który zawiera bibliotekę matematyczną C99 / C ++ 11. Ale wtedy pojawia się pytanie: jaką funkcję zaokrąglania wybierasz?
C99 / C ++ 11 round()
często nie jest tak naprawdę funkcją zaokrąglania, którą chcesz . Wykorzystuje funky tryb zaokrąglania, który zaokrągla wartość od 0 jako remis w przypadkach w połowie drogi ( +-xxx.5000
). Jeśli szczególnie chcesz ten tryb zaokrąglania lub celujesz w implementację C ++, która round()
jest szybsza niż rint()
, użyj go (lub naśladuj jego zachowanie z jedną z pozostałych odpowiedzi na to pytanie, która uznała go za wartościowy i dokładnie odtworzyła ten konkretny zaokrąglanie).
round()
Zaokrąglanie różni się od domyślnej zaokrąglenia IEEE754 do najbliższego trybu, nawet jako rozstrzygnięcie remisu . Najbliższy - nawet unika statystycznego błędu w średniej wielkości liczb, ale powoduje odchylenie w kierunku liczb parzystych.
Istnieją dwie funkcje zaokrąglania biblioteki matematycznej, które używają bieżącego domyślnego trybu zaokrąglania: std::nearbyint()
i std::rint()
obie zostały dodane w C99 / C ++ 11, więc są dostępne w każdej chwili std::round()
. Jedyną różnicą jest to, że nearbyint
nigdy nie podnosi FE_INEXACT.
Preferuj rint()
ze względu na wydajność : zarówno gcc, jak i clang wstawiają go łatwiej, ale gcc nigdy nie inline nearbyint()
(nawet z -ffast-math
)
gcc / clang dla x86-64 i AArch64
Umieściłem niektóre funkcje testowe w Eksploratorze kompilatorów Matta Godbolta , gdzie można zobaczyć źródło + wyjście asm (dla wielu kompilatorów). Więcej informacji na temat czytania wyjście kompilatora, zobacz ten Q & A i Matta CppCon2017 Dyskusja: „Co ma moje Compiler dla mnie zrobił ostatnio? Odblokowanie pokrywy kompilatora ” ,
W kodzie FP zwykle wstawianie małych funkcji jest zwykle dużą wygraną. Zwłaszcza w systemach innych niż Windows, w których standardowa konwencja wywoływania nie ma rejestrów zachowanych połączeń, więc kompilator nie może przechowywać żadnych wartości FP w rejestrach XMM w całym call
. Więc nawet jeśli tak naprawdę nie znasz asm, nadal możesz łatwo sprawdzić, czy jest to tylko wywołanie ogonowe funkcji bibliotecznej, czy też jest ono powiązane z jedną lub dwiema instrukcjami matematycznymi. Wszystko, co wpisuje się w jedną lub dwie instrukcje, jest lepsze niż wywołanie funkcji (dla tego konkretnego zadania na x86 lub ARM).
Na x86 wszystko, co wpisuje się w SSE4.1, roundsd
może automatycznie wektoryzować w SSE4.1 roundpd
(lub AVX vroundpd
). (Konwersje FP-> liczby całkowite są również dostępne w postaci spakowanej karty SIMD, z wyjątkiem FP-> 64-bitowa liczba całkowita, która wymaga AVX512.)
Zaokrąglenie do int
/ long
/ long long
:
Masz tutaj dwie opcje: użyj lrint
(jak rint
ale zwraca long
lub long long
dla llrint
) lub użyj funkcji zaokrąglania FP-> FP, a następnie przekonwertuj na typ całkowity w zwykły sposób (z obcięciem). Niektóre kompilatory optymalizują jeden sposób lepiej niż drugi.
long l = lrint(x);
int i = (int)rint(x);
Zauważ, że najpierw int i = lrint(x)
konwertuje float
lub double
-> long
, a następnie obcina liczbę całkowitą int
. To robi różnicę dla liczb całkowitych poza zasięgiem: Niezdefiniowane zachowanie w C ++, ale dobrze zdefiniowane dla instrukcji x86 FP -> int (które wyemituje kompilator, chyba że zobaczy UB w czasie kompilacji podczas ciągłej propagacji, to jest to wolno tworzyć kod, który pęka, jeśli zostanie kiedykolwiek wykonany).
Na x86 konwersja liczb całkowitych FP->, która przepełnia liczbę całkowitą, daje INT_MIN
lub LLONG_MIN
(wzorzec bitowy 0x8000000
lub odpowiednik 64-bitowy, tylko z ustawionym bitem znaku). Intel nazywa to „liczbą całkowitą nieokreśloną”. (Zobacz na cvttsd2si
ręczne wprowadzanie , instrukcja SSE2, który konwertuje (z obcinania) skalarne dwukrotnie do podpisanego całkowitej. Jest ona dostępna z 32-bitową lub 64-bitową docelowego Integer (tylko w trybie 64-bitowym). Jest też cvtsd2si
(konwersja z aktualnym zaokrąglania tryb), co chcielibyśmy, aby kompilator wyemitował, ale niestety bez gcc i clang nie zrobi tego -ffast-math
.
Uważaj również, że FP do / z unsigned
int / long jest mniej wydajny na x86 (bez AVX512). Konwersja do wersji 32-bitowej bez znaku na komputerze 64-bitowym jest dość tania; wystarczy przekonwertować na 64-bitowy podpisany i obciąć. Ale poza tym jest znacznie wolniejszy.
x86 clang z / bez -ffast-math -msse4.1
: (int/long)rint
inlines to roundsd
/ cvttsd2si
. (pominięto optymalizację do cvtsd2si
). lrint
wcale nie inline.
x86 gcc6.xi wcześniejsze bez -ffast-math
: inline w żaden sposób
- x86 gcc7 bez
-ffast-math
: (int/long)rint
zaokrągla i konwertuje osobno (z 2 całkowitymi instrukcjami SSE4.1 jest włączony, w przeciwnym razie z wiązką kodu wstawioną dla rint
bez roundsd
). lrint
nie inline.
x86 gcc z -ffast-math
: wszystkie sposoby do cvtsd2si
(optymalne) , nie ma potrzeby SSE4.1.
AArch64 gcc6.3 bez -ffast-math
: wstawia (int/long)rint
do 2 instrukcji. lrint
nie inline
- AArch64 gcc6.3 z
-ffast-math
: (int/long)rint
kompiluje do połączenia z lrint
. lrint
nie inline. Może to być pominięta optymalizacja, chyba że dwie instrukcje, które otrzymujemy, -ffast-math
są bardzo wolne.
std::cout << std::fixed << std::setprecision(0) << -0.9
na przykład.