Пример в 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 имеют одинаковое представление из-за приблизительной природы чисел с плавающей запятой?

Другие вопросы по тегам