Golang с плавающей запятой точность float32 vs float64
Я написал программу для демонстрации ошибки с плавающей запятой в Go:
func main() {
a := float64(0.2)
a += 0.1
a -= 0.3
var i int
for i = 0; a < 1.0; i++ {
a += a
}
fmt.Printf("After %d iterations, a = %e\n", i, a)
}
Он печатает:
After 54 iterations, a = 1.000000e+00
Это соответствует поведению той же самой программы, написанной на C (с использованием типа double
)
Однако, если вместо этого используется float32
, программа застревает в бесконечном цикле! Если вы измените программу C, чтобы использовать float
вместо double
, она печатает
After 27 iterations, a = 1.600000e+00
Почему при использовании float32
?
программа Go не имеет того же выхода, что и программа C,
Ответы
Ответ 1
Согласитесь с ANisus, идите, поступайте правильно. Что касается С, я не уверен в его предположении.
Стандарт C не диктует, но большинство реализаций libc преобразует десятичное представление в ближайший float (по крайней мере, для соответствия IEEE-754 2008 или ISO 10967), поэтому я не думаю, что это наиболее вероятное объяснение.
Существует несколько причин, по которым поведение программы C может отличаться... Особенно, некоторые промежуточные вычисления могут выполняться с избыточной точностью (двойной или длинный двойной).
Самая вероятная вещь, о которой я могу думать, - это если вы написали 0.1 вместо 0.1f в C.
В этом случае у вас может быть избыточная точность при инициализации
(вы суммируете float a + double 0.1 = > float преобразуется в double, затем результат преобразуется обратно в float)
Если я имитирую эти операции
float32(float32(float32(0.2) + float64(0.1)) - float64(0.3))
Затем я нахожу что-то около 1.1920929e-8f
После 27 итераций эти суммы равны 1.6f
Ответ 2
Используя math.Float32bits
и math.Float64bits
, вы можете увидеть, как Go представляет различные десятичные значения в виде двоичного значения IEEE 754:
Детская площадка: https://play.golang.org/p/ZqzdCZLfvC
Результат:
float32(0.1): 00111101110011001100110011001101
float32(0.2): 00111110010011001100110011001101
float32(0.3): 00111110100110011001100110011010
float64(0.1): 0011111110111001100110011001100110011001100110011001100110011010
float64(0.2): 0011111111001001100110011001100110011001100110011001100110011010
float64(0.3): 0011111111010011001100110011001100110011001100110011001100110011
Если вы преобразуете эти двоичные представления в десятичные значения и выполните свой цикл, вы увидите, что для float32 начальное значение a
будет:
0.20000000298023224
+ 0.10000000149011612
- 0.30000001192092896
= -7.4505806e-9
отрицательное значение, которое никогда не может суммироваться до 1.
Итак, почему C ведет себя иначе?
Если вы посмотрите на двоичный шаблон (и немного знаете о том, как представлять двоичные значения), вы увидите, что Go округляет последний бит, а я предполагаю, что C просто обрезает его вместо этого.
Таким образом, в некотором смысле, хотя ни Go, ни C не могут точно представлять 0.1 в плавающей запятой, Go использует значение, наиболее близкое к 0,1:
Go: 00111101110011001100110011001101 => 0.10000000149011612
C(?): 00111101110011001100110011001100 => 0.09999999403953552
Редактировать:
Я опубликовал вопрос о том, как C обрабатывает константы с плавающей точкой, и из ответа кажется, что любая реализация стандарта C может делать то же самое. Реализация, с которой вы пробовали, просто сделала это иначе, чем Go.