浮点数呈现背后的奥秘

Mystery behind presentation of Floating Point numbers

提问人:Robert 提问时间:8/30/2018 最后编辑:Robert 更新时间:8/13/2020 访问量:1040

问:

我正在为我的应用程序测试一些简单的解决方案,我遇到了一些问题出现在我脑海中的情况...... “为什么一个浮点数在JSON中正确表示(正如我所期望的那样),而另一个则没有......?”

在这种情况下,从字符串到十进制,然后从数字到JSON的转换:“98.39”从人类的角度来看是完全可以预测的,但数字:“98.40”看起来并不那么漂亮......

我的问题是,有人可以向我解释一下,为什么从字符串到十进制的转换对于一个浮点数来说就像我所期望的那样工作,但对于另一个浮点数却不是。

我有很多关于浮点数错误的问题,但我无法弄清楚过程是如何从 字符串 ->...基于二进制的转换 stuff...-> 到 Double 在这两种情况下具有不同的精度。


我的游乐场代码:

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    //Encode Person Struct as Data
    let encodedData = try? JSONEncoder().encode(price)

    //Create JSON
    var json: Any?
    if let data = encodedData {
        json = try? JSONSerialization.jsonObject(with: data, options: [])
    }

    //Print JSON Object
    if let json = json {
        print("Person JSON:\n" + String(describing: json) + "\n")
    }
}

let stringPriceOK =     "98.39"
let stringPriceNotOK =  "98.40"
let stringPriceNotOK2 = "98.99"

printJSON(from: stringPriceOK)
printJSON(from: stringPriceNotOK)
printJSON(from: stringPriceNotOK2)
/*
 ------------------------------------------------
 // OUTPUT:
 Person JSON:
 {
 amount = "98.39";
 }

 Person JSON:
 {
 amount = "98.40000000000001";
 }

 Person JSON:
 {
 amount = "98.98999999999999";
 }
 ------------------------------------------------
 */

我正在寻找/试图弄清楚逻辑单元执行了哪些步骤进行转换: “98.39” -> 十进制 -> 字符串 - 结果为“98.39”,转换链相同: “98.40” -> 十进制 -> 字符串 - 结果为“98.400000000000001”

非常感谢所有回复!

SWIFT 小数 精度 浮点精度

评论

2赞 Sergey Kalinichenko 8/30/2018
相关:这并不完全是重复的,但它所解释的概念正是创造“神秘”的原因。
3赞 kkiermasz 8/30/2018
学习浮点运算的一些基础知识。Oracle 文档示例
1赞 Eric Postpischil 8/30/2018
@kkiermasz:浮点运算的基础知识并不能揭示这里发生了什么。98.39 和 98.40 在二进制浮点数中都不能完全表示。当从十进制转换为二进制时,两者都是四舍五入的。然而,一个只显示四位数字,另一个显示 16 位数字。这是关于默认格式的一些实现选择的结果,而不仅仅是浮点的性质。
0赞 Eric Postpischil 8/30/2018
我假设 JSON 编码器使用诸如“将数字转换为具有 16 位有效数字的十进制,然后删除尾随零”之类的算法来格式化浮点数。对于 93.89,最接近的可表示值为 98.3900000000000005684341886080801486968994140625。当转换为 16 位十进制数字时,结果为“98.3900000000000000”。当删除尾随零时,结果为“93.39”。对于 98.40,最接近的可表示值为 98.400000000000005684341886080801486968994140625。转换会生成“98.400000000000001”,并且没有要删除的尾随零。
0赞 Eric Postpischil 8/30/2018
(阅读上述内容时,请注意 98.390000000000000005684341886080801486968994140625 和 98.4000000000000005684341886080801486968994140625 中存在不同数量的零,尽管尾随数字是相同的。在后一个数字中,非零提前一个位置开始。

答:

4赞 Simon Byrne 8/30/2018 #1

似乎在某些时候,JSON表示形式将值存储为二进制浮点数。

具体而言,最接近 98.40 的 (IEEE binary64) 值为 98.400000000000005684341886080801486968994140625,当四舍五入到 16 位有效数字时,该值为 98.40000000000001。double

为什么是 16 个有效数字?这是一个很好的问题,因为 16 位有效数字不足以唯一标识所有浮点值,例如 和 16 位有效数字相同,但对应于不同的值。奇怪的是,你的代码现在打印出来了0.0561830666499347760.05618306664993478

["amount": 0.056183066649934998]

对于两者,这是 17 个有效数字,但实际上是一个完全错误的值,最后相差 32 个单位。我不知道那里发生了什么。

有关二进制十进制转换所需位数的更多详细信息,请参阅 https://www.exploringbinary.com/number-of-digits-required-for-round-trip-conversions/

评论

0赞 rob mayoff 8/30/2018
它是 16 位数字,因为用于格式化双精度。据推测,将其更改为更好的格式可能会破坏现有程序。NSNumber%0.16g
0赞 Simon Byrne 8/30/2018
@robmayoff知道是什么导致了错误的值吗?
0赞 rob mayoff 8/30/2018
我无法重现该问题。
0赞 old_timer 8/30/2018 #2
#include <stdio.h>
int main ( void )
{
    float f;
    double d;

    f=98.39F;
    d=98.39;

    printf("%f\n",f);
    printf("%lf\n",d);
    return(1);
}
98.389999
98.390000

正如西蒙指出的那样,这根本不是一个谜。这就是计算机的工作方式,您正在使用 base 2 机器来做 base 10 的事情。就像 1/3 是一个非常简单的数字,但在以 10 为基数时它是 0.3333333。永远,既不准确也不漂亮,但在 base 3 中,它会像 0.1 一样漂亮干净。例如,以 10 为基数的数字与以 2 为基数 1/10 的数字并不相配。

float fun0 ( void )
{
    return(98.39F);
}
double fun1 ( void )
{
    return(98.39);
}
00000000 <fun0>:
   0:   e59f0000    ldr r0, [pc]    ; 8 <fun0+0x8>
   4:   e12fff1e    bx  lr
   8:   42c4c7ae    sbcmi   ip, r4, #45613056   ; 0x2b80000

0000000c <fun1>:
   c:   e59f0004    ldr r0, [pc, #4]    ; 18 <fun1+0xc>
  10:   e59f1004    ldr r1, [pc, #4]    ; 1c <fun1+0x10>
  14:   e12fff1e    bx  lr
  18:   c28f5c29    addgt   r5, pc, #10496  ; 0x2900
  1c:   405898f5    ldrshmi r9, [r8], #-133 ; 0xffffff7b

42c4c7ae  single
405898f5c28f5c29  double

0 10000101 10001001100011110101110
0 10000000101 1000100110001111010111000010100011110101110000101001

10001001100011110101110
1000100110001111010111000010100011110101110000101001

只看它们之间的尾数,这不会解析为一个确切的数字,所以然后四舍五入和带有更多四舍五入的格式化打印开始发挥作用......

4赞 rob mayoff 8/30/2018 #3

这纯粹是打印自己的人工制品。NSNumber

JSONSerialization在 Objective-C 中实现,并使用 Objective-C 对象(、、、等)来表示它从 JSON 中反序列化的值。由于 JSON 包含一个以小数点作为键值的裸数字,因此将其解析为 a 并将其包装在 .NSDictionaryNSArrayNSStringNSNumber"amount"JSONSerializationdoubleNSNumber

这些 Objective-C 类中的每一个都实现了一个打印自身的方法。description

返回的对象是 . 通过向 A 发送方法将其转换为 A。 通过发送到其每个键和值(包括键的值)来实现。JSONSerializationNSDictionaryString(describing:)NSDictionaryStringdescriptionNSDictionarydescriptiondescriptionNSNumber"amount"

使用说明符格式化值的实现。(我使用反汇编程序进行了检查。关于说明符,C 标准说NSNumberdescriptiondoubleprintf%0.16gg

最后,除非使用 # 标志,否则将从结果的小数部分中删除任何尾随零,如果没有剩余的小数部分,则删除小数点宽字符。

最接近 98.39 的双倍数正好是 98.3900 0000 0000 0005 6843 4188 6080 8014 8696 8994 1406 25。因此,将其格式化为(请参阅标准,了解为什么它是 14,而不是 16),这给出了 ,然后砍掉了尾随的零,给出 .%0.16g%0.14f"98.39000000000000""98.39"

最接近 98.40 的双倍正好是 98.4000 0000 0000 0056 8434 1886 0808 0148 6968 9941 4062 5。因此,将其格式化为 ,这给出了(由于四舍五入),并且没有尾随的零可以砍掉。%0.16g%0.14f"98.40000000000001"

这就是为什么当你打印 的结果时,你会得到很多 98.40 的小数位,但 98.39 只有两位数字。JSONSerialization.jsonObject(with:options:)

如果从 JSON 对象中提取数量并将它们转换为 Swift 的本机类型,然后打印这些 s,则会得到更短的输出,因为实现了一种更智能的格式化算法,该算法打印最短的字符串,在解析时会生成完全相同的字符串。DoubleDoubleDoubleDouble

试试这个:

import Foundation

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    let data = try! JSONEncoder().encode(price)
    let jsonString = String(data: data, encoding: .utf8)!
    let jso = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
    let nsNumber = jso["amount"] as! NSNumber
    let double = jso["amount"] as! Double

    print("""
    Original string: \(string)
        json: \(jsonString)
        jso: \(jso)
        amount as NSNumber: \(nsNumber)
        amount as Double: \(double)

    """)
}

printJSON(from: "98.39")
printJSON(from: "98.40")
printJSON(from: "98.99")

结果:

Original string: 98.39
    json: {"amount":98.39}
    jso: ["amount": 98.39]
    amount as NSNumber: 98.39
    amount as Double: 98.39

Original string: 98.40
    json: {"amount":98.4}
    jso: ["amount": 98.40000000000001]
    amount as NSNumber: 98.40000000000001
    amount as Double: 98.4

Original string: 98.99
    json: {"amount":98.99}
    jso: ["amount": 98.98999999999999]
    amount as NSNumber: 98.98999999999999
    amount as Double: 98.99

请注意,在所有情况下,实际的 JSON(在标记为 的行上)和 Swift 版本都使用最少的数字。使用 (labeled 和 ) 的行对某些值使用额外的数字。json:Double-[NSNumber description]jso:amount as NSNumber: