Точность плавающей точки Golang float32 против 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 = %en", i, a)
}

он печатает:

After 54 iterations, a = 1.000000e+00

это соответствует поведению той же программы, написанной на C (используя double type)

, если float32 используется вместо этого, программа застревает в бесконечном цикле! Если вы измените программу C на использование float вместо double, он печатает
After 27 iterations, a = 1.600000e+00

почему программа Go не имеет того же выхода, что и программа C при использовании float32?

2 ответов


согласен с анисом, go делает правильную вещь. Что касается C, я не убежден его догадкой.

стандарт C не диктует, но большинство реализаций libc преобразует десятичное представление в ближайший float (по крайней мере, в соответствии с IEEE-754 2008 или ISO 10967), поэтому я не думаю, что это наиболее вероятное объяснение.

существует несколько причин, по которым поведение программы C может отличаться... Особенно, некоторые промежуточные вычисления могут быть выполняется с избыточной точностью (double или long double).

самое вероятное, что я могу придумать, это если когда-либо вы написали 0.1 вместо 0.1 f В C.
В этом случае вы можете вызвать избыточную точность в инициализации
(вы суммируете float a+double 0.1 => поплавок преобразуется в double, затем результат преобразуется обратно в float)

если я эмулирую эти операции

float32(float32(float32(0.2) + float64(0.1)) - float64(0.3))

затем я нахожу что-то около 1.1920929 e-8f

после 27 итерации, это составляет 1.6 f


используя 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

Edit:

Я написал вопрос о том, как C обрабатывает константы float, и из ответа кажется, что любая реализация стандарта C может делать либо то, либо другое. Реализация, которую вы попробовали, просто сделала это по-другому, чем Go.