Dlaczego Clang optymalizuje pętlę w tym kodzie
#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
ale nie pętla w tym kodzie?
#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
(Oznaczam zarówno C, jak i C ++, ponieważ chciałbym wiedzieć, czy odpowiedź jest inna dla każdego).
c++
c
optimization
floating-point
clang
user541686
źródło
źródło
-O3
, ale nie wiem, jak sprawdzić, co się aktywuje.static double arr[N]
nie jest dozwolone w C;const
zmienne nie liczą się jako wyrażenia stałe w tym językuOdpowiedzi:
Norma IEEE 754-2008 dla arytmetyki zmiennoprzecinkowej oraz norma ISO / IEC 10967 dotycząca arytmetyki niezależnej od języka (LIA), część 1, odpowiadają, dlaczego tak jest.
Przypadek dodawania
W domyślnym trybie zaokrąglania (Round-to-Nearest, Ties-to-Even) , widzimy, że
x+0.0
daje tox
, Z WYJĄTKIEM kiedyx
jest-0.0
: W takim przypadku mamy sumę dwóch operandów z przeciwnymi znakami, których suma wynosi zero, i § 6.3 akapit 3 zasady, które tworzy ten dodatek+0.0
.Ponieważ
+0.0
nie jest bitowo identyczny z oryginałem-0.0
i-0.0
jest to uzasadniona wartość, która może wystąpić jako dane wejściowe, kompilator jest zobowiązany do umieszczenia kodu, który przekształci potencjalne zera ujemne na+0.0
.Podsumowanie: w domyślnym trybie zaokrąglania w
x+0.0
, jeślix
-0.0
, tox
samo w sobie jest dopuszczalną wartością wyjściową.-0.0
, to wartość wyjściowa musi być+0.0
, która nie jest bitowa identyczna z-0.0
.Przypadek mnożenia
W domyślnym trybie zaokrąglania taki problem nie występuje w przypadku
x*1.0
. Jeślix
:x*1.0 == x
zawsze.+/- infinity
, to wynik jest+/- infinity
tego samego znaku.jest
NaN
, to zgodnie zco oznacza, że wykładnik i mantysa (choć nie znakiem) od
NaN*1.0
są zalecane do niezmienione od wejściaNaN
. Znak jest nieokreślony zgodnie z §6.3p1 powyżej, ale implementacja może określić, że jest identyczna ze źródłemNaN
.+/- 0.0
, to wynikiem jest a0
ze swoim bitem znaku XOR z bitem znaku1.0
, zgodnie z §6.3p2. Ponieważ bit znaku wynosi1.0
to0
, wartość wyjściowa pozostaje niezmieniona w stosunku do wejścia. Zatemx*1.0 == x
nawet wtedy , gdyx
jest (ujemne) zero.Przypadek odejmowania
W domyślnym trybie zaokrąglania odejmowanie
x-0.0
jest również brakiem możliwości, ponieważ jest równoważne zx + (-0.0)
. Jeślix
takNaN
, to §6.3p1 i §6.2.3 mają zastosowanie w podobny sposób jak do dodawania i mnożenia.+/- infinity
, to wynik jest+/- infinity
tego samego znaku.x-0.0 == x
zawsze.-0.0
zatem przez §6.3p2 " [...] znak sumy lub różnicy x - y uważanej za sumę x + (−y) różni się od co najwyżej jednego ze znaków addendów; ”. To zmusza nas do przypisania-0.0
w rezultacie(-0.0) + (-0.0)
, bo-0.0
różni się oznaczeniem od żadnego z załączników, a+0.0
różni się znakiem od dwóch z załączników, z naruszeniem tej klauzuli.+0.0
, to obniża się w przypadku dodawania(+0.0) + (-0.0)
badanego powyżej w przypadku dodania , który z §6.3p3 jest wykluczone, aby dać+0.0
.Ponieważ we wszystkich przypadkach wartość wejściowa jest legalna jako wynik, dopuszczalne jest rozważenie
x-0.0
braku operacji ix == x-0.0
tautologii.Optymalizacje zmieniające wartość
Standard IEEE 754-2008 ma następujący interesujący cytat:
Ponieważ wszystkie NaN i wszystkie nieskończoności mają ten sam wykładnik, a poprawnie zaokrąglony wynik
x+0.0
ix*1.0
dla skończonychx
ma dokładnie taką samą wielkość jakx
ich wykładnik jest taki sam.SNaNs
Sygnalizacja NaN to wartości pułapki zmiennoprzecinkowe; Są to specjalne wartości NaN, których użycie jako operandu zmiennoprzecinkowego powoduje wyjątek nieprawidłowej operacji (SIGFPE). Gdyby pętla wyzwalająca wyjątek została zoptymalizowana, oprogramowanie nie zachowywałoby się już tak samo.
Jednak, jak wskazuje user2357112 w komentarzach , standard C11 wyraźnie pozostawia niezdefiniowane zachowanie sygnalizacji NaN (
sNaN
), więc kompilator może założyć, że nie występują, a zatem wyjątki, które podnoszą, również nie występują. Standard C ++ 11 pomija opis zachowania przy sygnalizowaniu NaN, a zatem również pozostawia go niezdefiniowanym.Tryby zaokrąglania
W alternatywnych trybach zaokrąglania dopuszczalne optymalizacje mogą ulec zmianie. Na przykład w trybie Round-to-Negative-Infinity optymalizacja
x+0.0 -> x
staje się dozwolona, alex-0.0 -> x
jest zabroniona.Aby uniemożliwić GCC przyjmowanie domyślnych trybów i zachowań zaokrąglania, flagę eksperymentalną
-frounding-math
można przekazać do GCC.Wniosek
Clang i GCC , nawet w
-O3
, pozostają zgodne z IEEE-754. Oznacza to, że musi przestrzegać powyższych zasad standardu IEEE-754.x+0.0
to nie nieco identyczne abyx
dla wszystkichx
w ramach tych zasad, alex*1.0
mogą być wybierane tak : Mianowicie, kiedyx
gdy jest to NaN.* 1.0
.x
jest nie NaN.Aby włączyć optymalizację niebezpieczną dla IEEE-754
(x+0.0) -> x
, flaga-ffast-math
musi zostać przekazana do Clang lub GCC.źródło
x += 0.0
nie jest NOOP, jeślix
jest-0.0
. Optymalizator i tak mógłby usunąć całą pętlę, ponieważ wyniki nie są używane. Ogólnie rzecz biorąc, trudno powiedzieć, dlaczego optymalizator podejmuje decyzje, które podejmuje.źródło
x += 0.0
nie jest to operacja, ale pomyślałem, że prawdopodobnie nie jest to powód, ponieważ cała pętla powinna być zoptymalizowana tak czy inaczej. Mogę to kupić, po prostu nie jest tak przekonujący, jak się spodziewałem ...long long
w efekcie działa optymalizacja (zrobiłem to z gcc, który zachowuje się tak samo przynajmniej dla double )long long
jest typem integralnym, a nie typem IEEE754.x -= 0
, czy to to samo?