Подтвердить что ты не робот

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,
4b9b3361

Ответ 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.