Próbowałem sprawdzić, gdzie float
traci zdolność do dokładnego reprezentowania dużych liczb całkowitych. Więc napisałem ten mały fragment:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
Ten kod wydaje się działać ze wszystkimi kompilatorami, z wyjątkiem clang. Clang generuje prostą nieskończoną pętlę. Godbolt .
Czy to jest dozwolone? Jeśli tak, czy jest to kwestia QoI?
c++
floating-point
clang
geza
źródło
źródło
gcc
wykonuje tę samą optymalizację nieskończonych pętli, jeśli-Ofast
zamiast tego kompilujesz , więc jest to optymalizacjagcc
uznawana za niebezpieczną, ale może to zrobić.ucomiss xmm0,xmm0
porównuje się(float)i
ze sobą. To była twoja pierwsza wskazówka, że twoje źródło C ++ nie oznacza tego, co myślisz, że robi. Czy twierdzisz, że masz tę pętlę do wydrukowania / zwrotu16777216
? Z jakim kompilatorem / wersją / opcjami to było? Ponieważ byłby to błąd kompilatora. gcc poprawnie optymalizuje twój kodjnp
jako gałąź pętli ( godbolt.org/z/XJYWeu ): kontynuuj pętlę tak długo, jak długo operandy!=
nie były NaN.-ffast-math
opcja, która jest domyślnie włączona przez,-Ofast
która pozwala GCC na stosowanie niebezpiecznych optymalizacji zmiennoprzecinkowych, a tym samym generowanie tego samego kodu co Clang. MSVC zachowuje się dokładnie w ten sam sposób: bez/fp:fast
niego generuje zbiór kodu, który skutkuje nieskończoną pętlą; z/fp:fast
, wysyła pojedyncząjmp
instrukcję. Zakładam, że bez jawnego włączenia niebezpiecznych optymalizacji FP te kompilatory rozłączają się z wymaganiami IEEE 754 dotyczącymi wartości NaN. Raczej interesujące, że Clang nie. Jego analizator statyczny jest lepszy. @ 12345ieee(float) i
różni się od wartości matematyczneji
, to wynik (wartość zwrócona wreturn
instrukcji) będzie wynosił 16 777 217, a nie 16 777 216.Odpowiedzi:
Jak zauważył @Angew ,
!=
operator potrzebuje tego samego typu po obu stronach.(float)i != i
skutkuje również promocją RHS do float, więc mamy(float)i != (float)i
.g ++ również generuje nieskończoną pętlę, ale nie optymalizuje pracy od wewnątrz. Możesz zobaczyć, że konwertuje int-> float zi
cvtsi2ss
robi,ucomiss xmm0,xmm0
aby porównać(float)i
ze sobą. (To była twoja pierwsza wskazówka, że twoje źródło C ++ nie oznacza tego, co myślisz, że podobało się odpowiedź @ Angew.)x != x
jest prawdziwy tylko wtedy, gdy jest „nieuporządkowany”, ponieważx
był NaN. (INFINITY
porównuje równe sobie w matematyce IEEE, ale NaN nie.NAN == NAN
jest fałszem,NAN != NAN
jest prawdą).gcc7.4 i starsze poprawnie optymalizują twój kod
jnp
jako gałąź pętli ( https://godbolt.org/z/fyOhW1 ): zachowaj pętlę tak długo, jak długo operandyx != x
nie były NaN. (gcc8 i późniejsze również sprawdzająje
, czy nie doszło do wyrwania się z pętli, nie optymalizując na podstawie faktu, że zawsze będzie to prawda dla każdego wejścia innego niż NaN). x86 FP porównuje ustawione PF na nieuporządkowanym.A tak przy okazji, oznacza to, że optymalizacja clang jest również bezpieczna : po prostu musi CSE
(float)i != (implicit conversion to float)i
być taka sama i udowodnić, żei -> float
nigdy nie jest NaN dla możliwego zakresuint
.(Chociaż biorąc pod uwagę, że ta pętla uderzy w UB przepełnienia ze znakiem, może wyemitować dosłownie każdy asm, którego chce, w tym
ud2
niedozwoloną instrukcję lub pustą nieskończoną pętlę, niezależnie od tego, czym faktycznie była treść pętli.) Ale ignorowanie UB przepełnienia ze znakiem , ta optymalizacja jest nadal w 100% legalna.GCC nie udaje się zoptymalizować treści pętli, nawet jeśli
-fwrapv
przepełnienie liczb całkowitych ze znakiem jest dobrze zdefiniowane (jako zawijanie dopełniacza 2). https://godbolt.org/z/t9A8t_Nawet włączenie
-fno-trapping-math
nie pomaga. (Domyślnym ustawieniem GCC jest niestety włączenie,-ftrapping-math
mimo że jego implementacja w GCC jest zepsuta / błędna). Konwersja int-> float może spowodować niedokładny wyjątek FP (dla liczb zbyt dużych, aby były dokładnie reprezentowane), więc w przypadku wyjątków, które mogą zostać zdemaskowane, rozsądne jest nie zoptymalizować korpus pętli. (Ponieważ konwersja16777217
do typu float może mieć obserwowalny efekt uboczny, jeśli niedokładny wyjątek zostanie zdemaskowany).Ale w przypadku
-O3 -fwrapv -fno-trapping-math
100% pominiętej optymalizacji nie kompilowanie tego do pustej nieskończonej pętli. Bez#pragma STDC FENV_ACCESS ON
stanu lepkich flag, które rejestrują zamaskowane wyjątki FP, nie można zaobserwować efektu ubocznego kodu. Nieint
->float
konwersja może skutkować NaN, więcx != x
nie może być prawdą.Wszystkie te kompilatory optymalizują pod kątem implementacji C ++, które używają pojedynczej precyzji IEEE 754 (binary32)
float
i 32-bitowejint
.Bugfixed
(int)(float)i != i
pętla musiałaby UB na implementacje C ++ z wąskiej 16-bitowejint
i / lub szerszyfloat
, ponieważ chcesz trafić podpisał liczbą całkowitą przepełnienie przed UB osiągnięciu pierwszego liczbę całkowitą, która nie była dokładnie przedstawianego jakofloat
.Ale UB w ramach innego zestawu wyborów zdefiniowanych w ramach implementacji nie ma żadnych negatywnych konsekwencji podczas kompilacji dla implementacji takiej jak gcc lub clang z x86-64 System V ABI.
Przy okazji, możesz statycznie obliczyć wynik tej pętli z
FLT_RADIX
iFLT_MANT_DIG
, zdefiniowane w<climits>
. A przynajmniej możesz w teorii, jeślifloat
faktycznie pasuje do modelu IEEE float, a nie innego rodzaju reprezentacji liczb rzeczywistych, takich jak Posit / unum.Nie jestem pewien, jak bardzo standard ISO C ++ wpływa na
float
zachowanie i czy format, który nie był oparty na wykładniku o stałej szerokości i polach istotności, byłby zgodny ze standardami.W komentarzach:
Czy twierdzisz, że masz tę pętlę do wydrukowania / zwrotu
16777216
?Aktualizacja: ponieważ ten komentarz został usunięty, myślę, że nie. Prawdopodobnie OP po prostu cytuje
float
przed pierwszą liczbą całkowitą, której nie można dokładnie przedstawić jako 32-bitowejfloat
. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, czyli to, co chcieli zweryfikować za pomocą tego błędnego kodu.Wersja z naprawionymi błędami oczywiście wypisuje
16777217
pierwszą liczbę całkowitą, której nie można dokładnie przedstawić, zamiast wartości poprzedzającej.(Wszystkie wyższe wartości zmiennoprzecinkowe są dokładnymi liczbami całkowitymi, ale są to wielokrotności 2, następnie 4, następnie 8 itd. Dla wartości wykładników wyższych niż szerokość istotności. Można przedstawić wiele wyższych wartości całkowitych, ale 1 jednostka na ostatnim miejscu (znaczenia) jest większe niż 1, więc nie są one ciągłymi liczbami całkowitymi. Największa liczba skończona
float
ma nieco mniej niż 2 ^ 128, co jest zbyt duże, aby było parzysteint64_t
).Gdyby jakiś kompilator zakończył oryginalną pętlę i wydrukował to, byłby to błąd kompilatora.
źródło
frapw
, ale jestem pewien, że GCC 10-ffinite-loops
zostało zaprojektowane do takich sytuacji.Zwróć uwagę, że operator wbudowany
!=
wymaga , aby jego operandy były tego samego typu i w razie potrzeby osiągnie to za pomocą promocji i konwersji. Innymi słowy, twój stan jest równoważny z:(float)i != (float)i
To nigdy nie powinno zawieść, więc kod w końcu się przepełni
i
, powodując niezdefiniowane zachowanie programu. Dlatego możliwe jest każde zachowanie.Aby poprawnie sprawdzić, co chcesz sprawdzić, prześlij wynik z powrotem do
int
:if ((int)(float)i != i)
źródło
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
jest tam oczywiste UB(int)(float)i != i
to UB? Jak to podsumowujesz? Tak, zależy to od właściwości zdefiniowanych w implementacji (ponieważfloat
nie jest wymagane, aby być IEEE754 binary32), ale w każdej implementacji jest dobrze zdefiniowane, chyba żefloat
może dokładnie reprezentować wszystkie pozytywneint
wartości , więc otrzymujemy przepełnienie UB ze całkowitym. ( en.cppreference.com/w/cpp/types/climits definiujeFLT_RADIX
iFLT_MANT_DIG
określa to). Ogólnie rzecz biorąc, rzeczy zdefiniowane w druku, jakstd::cout << sizeof(int)
nie jest UB ...reinterpret_cast<int>(float)
nie jest dokładnie UB, to tylko błąd składni / źle sformułowany. Byłoby miło, gdyby ta składnia pozwalała na przebijanie typu floatint
jako alternatywa dlamemcpy
(co jest dobrze zdefiniowane), alereinterpret_cast<>
myślę, że działa tylko na typach wskaźników.x != x
jest prawdą. Zobacz na żywo na coliru . W C też.