Dlaczego Clang optymalizuje dalej x * 1.0, ale NIE x + 0.0?

125

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

user541686
źródło
2
Które flagi optymalizacji są obecnie aktywne?
Nie będę istnieć Idonotexist
1
@IwillnotexistIdonotexist: właśnie użyłem -O3, ale nie wiem, jak sprawdzić, co się aktywuje.
user541686
2
Byłoby interesujące zobaczyć, co się stanie, jeśli dodasz -ffast-math do wiersza poleceń.
plugwash
static double arr[N]nie jest dozwolone w C; constzmienne nie liczą się jako wyrażenia stałe w tym języku
MM
1
[Wstaw złośliwy komentarz o tym, że C nie jest C ++, mimo że już go
wywołałeś

Odpowiedzi:

164

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.

IEEE 754 § 6.3. Bit znaku

Gdy dane wejściowe lub wynik to NaN, ten standard nie interpretuje znaku NaN. Należy jednak zauważyć, że operacje na łańcuchach bitowych - copy, negate, abs, copySign - określają bit znaku wyniku NaN, czasami oparty na bicie znaku operandu NaN. Na predykat logiczny totalOrder ma również wpływ bit znaku operandu NaN. Dla wszystkich innych operacji ten standard nie określa bitu znaku wyniku NaN, nawet jeśli istnieje tylko jeden wejściowy NaN lub gdy NaN jest wytwarzany z nieprawidłowej operacji.

Gdy ani dane wejściowe, ani wynik nie są NaN, znak iloczynu lub ilorazu jest wyłącznym LUB znaków argumentów; znak sumy lub różnicy x - y traktowanej jako suma x + (−y) różni się co najwyżej od jednego ze znaków addendów; a znak wyniku konwersji, operacji kwantyzacji, operacji roundTo-Integral i roundToIntegralExact (patrz 5.3.1) jest znakiem pierwszego lub jedynego operandu. Reguły te mają zastosowanie nawet wtedy, gdy argumenty lub wyniki są zerowe lub nieskończone.

Gdy suma dwóch operandów z przeciwnymi znakami (lub różnica dwóch argumentów z podobnymi znakami) jest równa dokładnie zero, znak tej sumy (lub różnicy) powinien wynosić +0 we wszystkich atrybutach kierunku zaokrąglania z wyjątkiem roundTowardNegative; pod tym atrybutem znak dokładnej sumy zerowej (lub różnicy) powinien wynosić -0. Jednak x + x = x - (−x) zachowuje ten sam znak co x, nawet gdy x jest równe zero.

Przypadek dodawania

W domyślnym trybie zaokrąglania (Round-to-Nearest, Ties-to-Even) , widzimy, że x+0.0daje to x, Z WYJĄTKIEM kiedy xjest -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.0nie jest bitowo identyczny z oryginałem -0.0i -0.0jest 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

  • nie jest -0.0 , to xsamo w sobie jest dopuszczalną wartością wyjściową.
  • jest -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śli x:

  • jest (pod) normalną liczbą, x*1.0 == xzawsze.
  • jest +/- infinity, to wynik jest +/- infinitytego samego znaku.
  • jest NaN, to zgodnie z

    IEEE 754 § 6.2.3 Propagacja NaN

    Operacja, która propaguje operand NaN do swojego wyniku i ma pojedynczy NaN jako dane wejściowe, powinna generować NaN z ładunkiem wejściowym NaN, jeśli można to przedstawić w formacie docelowym.

    co oznacza, że wykładnik i mantysa (choć nie znakiem) od NaN*1.0zalecane do niezmienione od wejścia NaN. Znak jest nieokreślony zgodnie z §6.3p1 powyżej, ale implementacja może określić, że jest identyczna ze źródłem NaN.

  • jest +/- 0.0, to wynikiem jest a 0ze swoim bitem znaku XOR z bitem znaku 1.0, zgodnie z §6.3p2. Ponieważ bit znaku wynosi 1.0to 0, wartość wyjściowa pozostaje niezmieniona w stosunku do wejścia. Zatem x*1.0 == xnawet wtedy , gdy xjest (ujemne) zero.

Przypadek odejmowania

W domyślnym trybie zaokrąglania odejmowanie x-0.0jest również brakiem możliwości, ponieważ jest równoważne z x + (-0.0). Jeśli xtak

  • jest NaN, to §6.3p1 i §6.2.3 mają zastosowanie w podobny sposób jak do dodawania i mnożenia.
  • jest +/- infinity, to wynik jest +/- infinitytego samego znaku.
  • jest (pod) normalną liczbą, x-0.0 == xzawsze.
  • jest -0.0zatem 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.0w rezultacie (-0.0) + (-0.0), bo -0.0różni się oznaczeniem od żadnego z załączników, a +0.0różni się znakiem od dwóch z załączników, z naruszeniem tej klauzuli.
  • jest +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.0braku operacji i x == x-0.0tautologii.

Optymalizacje zmieniające wartość

Standard IEEE 754-2008 ma następujący interesujący cytat:

IEEE 754 § 10.4 Znaczenie dosłowne i optymalizacje zmieniające wartość

[…]

Między innymi następujące transformacje zmieniające wartość zachowują dosłowne znaczenie kodu źródłowego:

  • Zastosowanie właściwości tożsamości 0 + x, gdy x nie jest zerem i nie jest sygnalizującym NaN, a wynik ma taki sam wykładnik jak x.
  • Zastosowanie właściwości tożsamości 1 × x, gdy x nie jest sygnalizującym NaN, a wynik ma ten sam wykładnik co x.
  • Zmiana ładunku lub bitu znaku cichego NaN.
  • […]

Ponieważ wszystkie NaN i wszystkie nieskończoności mają ten sam wykładnik, a poprawnie zaokrąglony wynik x+0.0i x*1.0dla skończonych xma dokładnie taką samą wielkość jak xich 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 -> xstaje się dozwolona, ​​ale x-0.0 -> xjest zabroniona.

Aby uniemożliwić GCC przyjmowanie domyślnych trybów i zachowań zaokrąglania, flagę eksperymentalną -frounding-mathmoż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.0to nie nieco identyczne aby xdla wszystkich xw ramach tych zasad, ale x*1.0 mogą być wybierane tak : Mianowicie, kiedy

  1. Przestrzegaj zalecenia, aby przekazywać niezmieniony ładunek, xgdy jest to NaN.
  2. Pozostaw niezmieniony bit znaku wyniku NaN przez * 1.0.
  3. Rozkazu do XOR bit znaku podczas iloraz / produktu, gdy xjest nie NaN.

Aby włączyć optymalizację niebezpieczną dla IEEE-754 (x+0.0) -> x, flaga -ffast-mathmusi zostać przekazana do Clang lub GCC.

Nie będę istniał Idonotistnie
źródło
2
Uwaga: co, jeśli jest to sygnalizujący NaN? (Właściwie myślałem, że to mógł być w jakiś sposób powód, ale tak naprawdę nie wiedziałem jak, więc zapytałem.)
user541686
6
@Mehrdad: Załącznik F, (opcjonalna) część normy C, która określa zgodność C z IEEE 754, wyraźnie nie obejmuje sygnalizacji NaN. (C11 F.2.1., Pierwszy wiersz: „Ta specyfikacja nie definiuje zachowania sygnalizacyjnych NaN.”) Implementacje, które deklarują zgodność z załącznikiem F, mogą robić, co chcą, z sygnalizacją NaN. Standard C ++ ma własną obsługę IEEE 754, ale cokolwiek to jest (nie jestem zaznajomiony), wątpię, czy określa również zachowanie sygnalizacji NaN.
user2357112 obsługuje Monikę
2
@Mehrdad: sNaN wywołuje niezdefiniowane zachowanie zgodnie ze standardem (ale prawdopodobnie jest dobrze zdefiniowane przez platformę), więc kompilator tutaj jest dozwolony.
Joshua
1
@ user2357112: Możliwość wychwytywania błędów jako efektu ubocznego dla nieużywanych w inny sposób obliczeń generalnie przeszkadza w optymalizacji; jeśli wynik obliczenia jest czasami ignorowany, kompilator może z pożytkiem odroczyć obliczenie, dopóki nie będzie wiedział, czy wynik zostanie użyty, ale jeśli obliczenie dałoby ważny sygnał, może to być złe.
supercat
2
Och, spójrz, pytanie, które słusznie dotyczy zarówno C, jak i C ++, na które w przypadku obu języków trafna odpowiedź jest odniesieniem do jednego standardu. Czy to sprawi, że ludzie będą mniej narzekać na pytania oznaczone tagami C i C ++, nawet jeśli pytanie dotyczy wspólnego języka? Niestety, nie sądzę.
Kyle Strand
35

x += 0.0nie jest NOOP, jeśli xjest -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.

user2357112 obsługuje Monikę
źródło
2
Właściwie opublikowałem to po tym, jak właśnie przeczytałem, dlaczego x += 0.0nie 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 ...
user541686
Biorąc pod uwagę skłonność języków zorientowanych obiektowo do wywoływania efektów ubocznych, wyobrażam sobie, że byłoby trudno mieć pewność, że optymalizator nie zmienia rzeczywistego zachowania.
Robert Harvey
To może być powód, skoro long longw efekcie działa optymalizacja (zrobiłem to z gcc, który zachowuje się tak samo przynajmniej dla double )
e2-e4
2
@ ringø: long longjest typem integralnym, a nie typem IEEE754.
MSalters
1
A co x -= 0, czy to to samo?
Viktor Mellgren