2013-05-16 16 views
8

Niedawno czytałem post: Double or Nothing from GOTW by Herb Sutter jestem trochę mylić z wyjaśnieniem następującego programu:Zrozumienie Guru tygodnia # 67: Double or Nothing

int main() 
{ 
    double x = 1e8; 
    while(x > 0) 
    { 
     --x; 
    } 
} 

Załóżmy, że ten kod jest uruchamiany 1 sekunda w jakiejś maszynie. Zgadzam się z tym, że kod taki jak ten jest głupi.

Jednak za wyjaśnienie problemu, jeśli zmienimy x z float na double, to w niektórych kompilatorach będzie utrzymywać komputer w nieskończoności. Wyjaśnienie opiera się na następującym cytacie ze standardu.

Cytowanie z sekcji 3.9.1/8 z normą ++ C:

Istnieją trzy typy zmiennym punkcie: pływak, dwu- i długości dwukrotnie. Typ double zapewnia co najmniej taką samą precyzję, jak float, a double double typu zapewnia co najmniej taką samą precyzję, jak double. Zbiór wartości typu float jest podzbiorem zestawu wartości typu double; zestaw wartości typu double jest podzbiorem zestawu wartości typu double double.

Pytanie o kod to:

Jak długo można się spodziewać, że do podjęcia, jeśli zmienisz "double" do "float"? Czemu?

Oto wyjaśnienie podane:

to prawdopodobnie będzie albo około 1 sekundę (na konkretnym pływaków realizacja może być nieco szybciej, tak szybko, lub nieco wolniej niż w deblu), albo na zawsze , w zależności od tego, czy float może dokładnie reprezentować wszystkie wartości całkowite od 0 do 1e8 włącznie.

Powyższy cytat ze standardu oznacza, że ​​mogą istnieć wartości, które mogą być reprezentowane przez podwójne, ale które nie mogą być reprezentowane przez zmienną. W szczególności, na niektórych popularnych platformach i kompilatorach, double może dokładnie reprezentować wszystkie wartości całkowite w [0,1e8], ale float nie może.

Co jeśli float nie może dokładnie reprezentować wszystkich wartości całkowitych od 0 do 1e8? Następnie zmodyfikowany program rozpocznie odliczanie, ale ostatecznie osiągnie wartość N, która nie może być reprezentowana i dla której N-1 == N (z powodu niewystarczającej precyzji zmiennoprzecinkowej) ... i

Moje pytanie brzmi:

Jeśli float nie jest nawet w stanie reprezentować 1e8, to powinniśmy mieć przepełnienie już po zainicjowaniu float x = 1e8; jak to możliwe, że komputer będzie działał wiecznie?

Próbowałem prosty przykład tutaj (choć nie double ale int)

#include <iostream> 

int main() 
{ 
    int a = 4444444444444444444; 
    std::cout << "a " << a << std::endl; 
    return 0; 
} 
It outputs: a -1357789412 

Oznacza to, że jeśli kompilator nie jest w stanie reprezentować daną liczbę z int typu, spowoduje to przepełnienie.

Czy źle odczytałem? Jaki punkt, który przeoczyłem? Czy zmiana nieokreślonego zachowania x z double na float?

Dziękujemy!

Odpowiedz

9

Kluczowym słowem jest "dokładnie".

float może reprezentować 1e8, nawet dokładnie, chyba że masz typ dziwaka float. Ale to nie znaczy, że może on reprezentować wszystkie mniejsze wartości dokładnie, na przykład, zwykle 2^25+1 = 33554433, który potrzebuje 26 bitów precyzji, nie może być dokładnie reprezentowany w float (zazwyczaj ma on 23 + 1 bitów precyzji), ani nie może 2^25-1 = 33554431, który potrzebuje 25 bitów precyzji.

Oba te liczby są następnie przedstawiane jako 2^25 = 33554432, a następnie

33554432.0f - 1 == 33554432.0f 

będzie pętli. (Najpierw uderzysz w pętlę, ale ta ma ładną reprezentację dziesiętną;)

W arytmetyce całkowitej masz x - 1 != x dla wszystkich x, ale nie w arytmetykach zmiennoprzecinkowych.

Należy pamiętać, że pętla może również kończyć się, nawet jeśli float ma tylko zwykłe 23 + 1 bity precyzji, ponieważ standard umożliwia wykonywanie obliczeń zmiennoprzecinkowych z większą dokładnością niż typ, a jeśli obliczenia są wykonywane z wystarczająco większą precyzją (np. zwykle double z 52 bitami + 1), każde odjęcie zmieni się na x.

+0

dziękuję za miłą informację, ale moje pytanie brzmi, dlaczego pętla while zacznie się nawet, jeśli mamy przepełnienie podczas inicjalizacji? – taocp

+2

@taocp Nie masz przepełnienia podczas inicjalizacji. Formaty zmiennoprzecinkowe mają stale niższą * dokładność *, gdy przechodzisz do wyższych wartości, aż osiągniesz + 1. # INF. Operacje w nieskończoności, & # NAN są legalne, a nie nieokreślone. Mogą jednak mieć nieoczekiwane rezultaty. Spójrz na standard [IEEE-754] (https://en.wikipedia.org/wiki/IEEE_floating_point) –

+0

@ ndeterminerminately sequenced ah, wielkie dzięki za to. – taocp

0

Spróbuj tej prostej modyfikacji, która wykrywa wartość kolejnych wartości x.

#include <iostream> 
using namespace std; 

int main() 
{ 
    float x = 1e8; 
    while(x > 0) 
    { 
     cout << x << endl; 
     --x; 
    } 
} 

W niektórych implementacjach pływaka widać, że wartości pływaka tkwią w 1E8, lub w tym regionie. Wynika to ze sposobu, w jaki liczba liczb przechowuje float. Float nie może (ani też nie może być reprezentowana w nieco ograniczonym zakresie) reprezentować wszystkie możliwe wartości dziesiętne, więc gdy mamy do czynienia z bardzo dużymi wartościami w float, mamy w zasadzie dziesiętną, zwiększoną do pewnej mocy. Cóż, jeśli ta dziesiętna wartość kończy się wartością, w której ostatni bit spada, oznacza to, że zostanie zaokrąglona w górę. To, na czym skończysz, to wartość, która zmniejsza się (następnie tworzy kopię zapasową) do siebie.