Пример в Gotw 67
Есть пример в http://www.gotw.ca/gotw/067.htm
int main()
{
double x = 1e8;
//float x = 1e8;
while( x > 0 )
{
--x;
}
}
Когда вы меняете double на float, это бесконечный цикл в VS2008. Согласно объяснению Готв:
Что если float не может точно представлять все целочисленные значения от 0 до 1e8? Затем измененная программа начнет обратный отсчет, но в конечном итоге достигнет значения N, которое невозможно представить, и для которого N-1 == N (из-за недостаточной точности с плавающей запятой)... и тогда цикл останется застрявшим на этом значении, пока машина, на которой работает программа, не выйдет из строя.
Из того, что я понимаю, число с плавающей запятой IEEE754 имеет одинарную точность (32 бита), а диапазон значений с плавающей запятой должен составлять +/- 3,4e +/- 38, и он должен иметь значащие 7 цифр.
Но я до сих пор не понимаю, как именно это происходит: "в конечном итоге достичь значения N, которое невозможно представить и для которого N-1 == N (из-за недостаточной точности с плавающей запятой)". Может кто-нибудь попытаться объяснить это немного?
Немного дополнительной информации: когда я использую double x = 1e8, он завершается примерно через 1 секунду, когда я меняю его на float x = 1e8, он работает намного дольше (все еще работает через 5 минут), даже если я изменяю его на float x = 1e7;
Это закончилось примерно за 1 секунду.
Моя среда тестирования - VS2008.
Кстати, я НЕ спрашиваю объяснение основного формата IEEE 754, поскольку я уже это понимаю.
Спасибо
4 ответа
Ну, в качестве аргумента, давайте предположим, что у нас есть процессор, который представляет число с плавающей запятой с 7 значащими десятичными цифрами, и мантисса с, скажем, 2 десятичными цифрами. Так что теперь число 1e8 будет храниться как
1.000 000 e 08
(где "." и "e" не должны быть сохранены.)
Итак, теперь вы хотите вычислить "1e8 - 1". 1 представляется как
1.000 000 e 00
Теперь, чтобы выполнить вычитание, мы сначала делаем вычитание с бесконечной точностью, а затем нормализуем так, чтобы первая цифра перед "." находится между 1 и 9, и, наконец, округляется до ближайшего представимого значения (скажем, с безубыточностью). Результат бесконечной точности "1e8 - 1" равен
0.99 999 999 e 08
или нормализованный
9.9 999 999 e 07
Как можно видеть, результат с бесконечной точностью нуждается в еще одной цифре в значении, чем то, что фактически обеспечивает наша архитектура; следовательно, нам нужно округлить (и заново нормализовать) бесконечно точный результат до 7 значащих цифр, что приведет к
1.000 000 e 08
Следовательно, вы получите "1e8 - 1 == 1e8", и ваш цикл никогда не завершится.
Теперь, на самом деле, вы используете бинарные поплавки IEEE 754, которые немного отличаются, но принцип примерно такой же.
Операция x--
(в данном случае) эквивалентно x = x - 1
, Это означает, что первоначальная стоимость x
взят, 1
вычитается (с использованием бесконечной точности, согласно IEEE 754-1985), а затем результат округляется до следующего значения float
пространство значений.
Округленный результат для чисел 1.0e8f + i
дается для i in [-10;10]
ниже:
-10: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-9: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-8: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-7: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-6: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-5: 9.9999992E7 (binary +|10011001|01111101011110000011111)
-4: 1.0E8 (binary +|10011001|01111101011110000100000)
-3: 1.0E8 (binary +|10011001|01111101011110000100000)
-2: 1.0E8 (binary +|10011001|01111101011110000100000)
-1: 1.0E8 (binary +|10011001|01111101011110000100000)
0: 1.0E8 (binary +|10011001|01111101011110000100000)
1: 1.0E8 (binary +|10011001|01111101011110000100000)
2: 1.0E8 (binary +|10011001|01111101011110000100000)
3: 1.0E8 (binary +|10011001|01111101011110000100000)
4: 1.0E8 (binary +|10011001|01111101011110000100000)
5: 1.00000008E8 (binary +|10011001|01111101011110000100001)
6: 1.00000008E8 (binary +|10011001|01111101011110000100001)
7: 1.00000008E8 (binary +|10011001|01111101011110000100001)
8: 1.00000008E8 (binary +|10011001|01111101011110000100001)
9: 1.00000008E8 (binary +|10011001|01111101011110000100001)
10: 1.00000008E8 (binary +|10011001|01111101011110000100001)
Так что вы можете видеть, что 1.0e8f
а также 1.0e8f + 4
и некоторые другие числа имеют такое же представление. Поскольку вы уже знакомы с деталями форматов IEEE 754-1985 с плавающей запятой, вы также знаете, что оставшиеся цифры должны быть округлены.
Что касается "достижения" значения, которое невозможно представить, я думаю, что Херб включал возможность довольно эзотерических представлений с плавающей точкой.
С любыми обычными представлениями с плавающей запятой вы либо начнете с такого значения (т.е. застряли на первом значении), либо окажетесь где-то в непрерывном диапазоне целых чисел с центром в районе нуля, который может быть представлен точно, так что обратный отсчет будет успешным.
Для IEEE 754 32-битное представление, как правило, float
в C++ имеет 23-битную мантиссу, в то время как 64-битное представление, как правило, double
в C++, имеет 52 бит мантиссы. Это означает, что с double
Вы можете по крайней мере точно представить целые числа в диапазоне -(2^52-1) ... 2^52-1. Я не совсем уверен, можно ли расширить диапазон еще одним фактором 2. У меня немного кружится голова, когда я думаю об этом.:-)
Ура & hth.,
Каков результат n - 1, если n - 1 и n имеют одинаковое представление из-за приблизительной природы чисел с плавающей запятой?