不失精度地打印双精度数

35

如何将double类型打印到流中,以便在读取时不会丢失精度?

我尝试过:

std::stringstream ss;

double v = 0.1 * 0.1;
ss << std::setprecision(std::numeric_limits<T>::digits10) << v << " ";

double u;
ss >> u;
std::cout << "precision " << ((u == v) ? "retained" : "lost") << std::endl;

这并没有像我预期的那样起作用。

但我可以增加精度(这让我感到惊讶,因为我认为digits10是必需的最大精度)。

ss << std::setprecision(std::numeric_limits<T>::digits10 + 2) << v << " ";
                                                 //    ^^^^^^ +2

这与有效数字的数量有关,其中前两个数字在(0.01)中不计算。

所以有没有人考虑过精确表示浮点数? 我需要在流上执行的确切操作是什么魔法咒语?

经过一些实验:

问题出在我的原始版本上。在小数点后面的字符串中存在非显着数字,影响了精度。

因此,为了补偿这一点,我们可以使用科学计数法进行补偿:

ss << std::scientific
   << std::setprecision(std::numeric_limits<double>::digits10 + 1)
   << v;
这仍然没有解释为什么需要+1。
另外,如果我以更高精度打印数字,那么我就会得到更多的精度!
std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits10) << v << "\n";
std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits10 + 1) << v << "\n";
std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits) << v << "\n";

它的结果是:

1.000000000000000e-02
1.0000000000000002e-02
1.00000000000000019428902930940239457413554200000000000e-02

根据下面 @Stephen Canon 的回答:

我们可以使用 printf() 格式化控制字符 "%a" 或 "%A" 精确打印出结果。在 C++ 中,我们可以通过使用 fixed 和 scientific 修饰符来实现这一点(参见 n3225: 22.4.2.2.2p5 表格88)。

std::cout.flags(std::ios_base::fixed | std::ios_base::scientific);
std::cout << v;

目前我已经定义了:

template<typename T>
std::ostream& precise(std::ostream& stream)
{
    std::cout.flags(std::ios_base::fixed | std::ios_base::scientific);
    return stream;
}

std::ostream& preciselngd(std::ostream& stream){ return precise<long double>(stream);}
std::ostream& precisedbl(std::ostream& stream) { return precise<double>(stream);}
std::ostream& preciseflt(std::ostream& stream) { return precise<float>(stream);}

下一步:我们如何处理NaN/Inf?


当你输出到 ss 时,为什么在 v 后面包含一个空格? - chrisaycock
没有原因。剪切和粘贴错误。 - Martin York
1
除了那些具有二进制分母的值,总会存在精度损失。问题应该是“需要保持多少精度?” - Thomas Matthews
@Thomas Matthews:我还没有被说服这是真的。我看不出你不能精确地存储一个双精度数并检索它的原因。如果您有一些证明这一点的参考资料,我很乐意阅读。浮点数并不是模糊的值,它有一个确切的值。也许我需要打印一个二进制版本(还不确定),但我仍然认为它可以在十进制中完成。 - Martin York
根据SO上的许多浮点数问题,从非二进制小数转换为内部表示时会存在精度损失。这就是我所指的。知道会有损失后,目标应该是尽量减少损失。 - Thomas Matthews
1
@Rüdiger Stevens:FPU(x86)和SSE计算结果差异的原因不是由于不同的标准,它们都符合IEEE754标准,而是因为前者将中间结果以10字节格式存储,具有64个有效数字的尾数,而后者则以标准双精度(有时是单精度)形式存储,具有1+52(1+22)位数字。具有许多有效数字的结果将由FPU比SSE更好地处理。 - Olof Forshell
8个回答

17

说 "浮点数不准确" 是不正确的,尽管我承认这是一个有用的简化。如果在现实生活中我们使用八进制或十六进制,那么这里的人们就会说 "十进制小数包是不准确的,为什么有人会发明它们呢?"

问题在于整数值在不同进制之间可以精确转换,但小数值则不能,因为它们代表了整数步长的分数部分,而只有其中很少一部分被使用。

浮点算术在技术上是完全准确的。每个计算都有一个且仅有一个可能的结果。存在一个问题,即大多数 十进制小数具有重复的二进制表示。实际上,在序列0.01、0.02、…… 0.99中,仅有3个值具有精确的二进制表示(0.25、0.50和0.75)。有96个值重复,因此显然不能精确地表示它们。

现在,有许多方法可以写入和读回浮点数而不会失去任何位。思想是避免尝试用十进制小数来表达二进制数。

  • 以二进制形式编写。如今,每个人都实现了IEEE-754格式,只要选择一个字节顺序并仅写入或读取该字节顺序,那么这些数字就是可移植的。
  • 将它们写成64位整数值。在这里,您可以使用通常的十进制(因为您表示的是64位别名整数,而不是52位小数部分)。

您还可以只写更多的十进制小数位数。这是否精确到每一位将取决于转换库的质量,我不确定我会指望这里 (从软件中) 有完全的准确性。但是任何错误都将非常小,并且原始数据肯定没有低位的信息。(物理和化学的任何常数都未知到52个位数,也从未测量过地球上的任何距离精度高达52位。)但对于需要自动比较位对位准确性的备份或还原,这显然不是理想的选择。


2
@MSN:不,"正确"是正确的。浮点数经常不准确,但许多整数算法也是如此。完全可以在浮点数中编写精确的算法(事实上,这是我得到报酬的主要部分)。 - Stephen Canon
@Stephen,嗯,这也与讨论无关。我想说,更“正确”的说法是上下文定义准确性,而不是表示形式。 - MSN

14

如果你不想失去精度,请不要以十进制形式打印浮点数值。即使你打印足够的数字来完全表示该数字,但并非所有实现都能正确地将浮点数范围内的十进制字符串进行舍入转换,因此可能仍会失去精度。

取而代之的是,使用十六进制浮点数。在C中:

printf("%a\n", yourNumber);

C++0x提供了iostreams的操作符,可以实现相同的功能(在某些平台上,使用std::hex修饰符具有相同的效果,但这不是可移植的假设)。

使用十六进制浮点数有几个好处。

首先,打印出的值始终是精确的。以这种方式格式化值时,不会发生舍入错误。除了增加精度之外,这意味着使用良好调整的I/O库可以更快地读写此类值。它们还需要较少的位数来精确表示值。


这种类型限定符在所有运行时中都存在吗?我在Visual C++中有它,但有些参考资料没有。http://www.cplusplus.com/reference/clibrary/cstdio/printf/ - ThomasMcLeod
2
“%a”指示符已经在C标准中存在了11年;任何仍不支持它的平台都不能真正声称自己是“C”。hexfloat是在C++0x中添加的(我相信——我不是C++专家),因此它的使用可能会更少地可移植。 - Stephen Canon
您可以通过指定固定和科学格式来获取%a格式化程序。 - Martin York
在Visual Studio 2013中,您可以打印它,但无法扫描它:https://stackoverflow.com/questions/55930726/sscanf-fails-to-read-double-hex/55939590#55939590 - Patrick Parker

11

我对这个问题感兴趣,因为我正在尝试将我的数据序列化/反序列化为JSON格式。

我认为我有一个更清晰的解释(少了一些摆手)为什么17个十进制数字足以无损地重构原始数字:

enter image description here

想象3条数轴:
1. 原始二进制数
2. 四舍五入的十进制表示
3. 重构的数字(与#1相同,因为两者都是二进制)

当你转换为十进制时,从图形上看,您选择第2个数轴上距离第1个最近的刻度线。同样,当您从四舍五入的十进制值重构原始值时也是如此。

我关键的观察是,为了允许精确重构,必须使十进制步长(量子)<基本2量子。否则,必然会出现红色显示的错误重构。

以基础2表示法的指数为0的特定情况为例。那么,基本2量子将是2 ^ -52〜= 2.22 * 10 ^ -16。小于此的最接近的基本10量子为10 ^ -16。现在我们知道所需的基本10量子,需要多少位才能编码所有可能的值?鉴于我们仅考虑指数= 0的情况,我们需要表示的值的动态范围为[1.0,2.0)。因此,需要17个数字(小数部分需要16个数字,整数部分需要1个数字)。

对于指数不为0的情况,我们可以使用相同的逻辑:

    exponent    base2 quant.   base10 quant.  dynamic range   digits needed
    ---------------------------------------------------------------------
    1              2 ^ -51        10^-16         [2, 4)           17
    2              2 ^ -50        10^-16         [4, 8)           17
    3              2 ^ -49        10^-15         [8, 16)          17
    ... 
 
32 2的-20次方 10的-7次方 [2的32次方,2的33次方) 17
1022 9.98e291 1.0e291 [4.49e307,8.99e307) 17

这张表格虽然不详尽,但表明了17位数字足以满足需求。

希望您喜欢我的解释。


感谢点赞。我得到了进一步的见解。基数为10的量子必须<=基数为2的量子,因为这是唯一保证在基数为2的数线上每个点的最近基数为10的刻度都在半步之内的方法!这确保了精确转换。 - Yale Zhang

9
在C++20中,您将能够使用std::format来完成这个任务:
std::stringstream ss;
double v = 0.1 * 0.1;
ss << std::format("{}", v);
double u;
ss >> u;
assert(v == u);

默认的浮点数格式是最短的十进制表示法,具有往返保证。与使用 max_digits10 的精度(不适用于往返于十进制的 digits10)从 std::numeric_limits 相比,这种方法的优点在于它不会打印不必要的数字。
同时,您可以使用{fmt}库, 该库是基于std::format的。例如(godbolt):
fmt::print("{}", 0.1 * 0.1);

输出(假设使用IEEE754 double):

0.010000000000000002

{fmt}使用Dragonbox算法进行快速的二进制浮点数转十进制数。除了给出最短的表示形式外,它比常见的标准库printf和iostreams实现快20-30倍

免责声明:我是{fmt}和C++20 std::format的作者。


它不会打印不必要的数字,那么为什么输出不是“0.01”呢? - Patrick Parker
@PatrickParker,因为这会失去精度,而这不是OP想要的。g使用printf的默认精度6,这就是为什么它在这里不合适。 - vitaut
1
谢谢。我忽略了一个显而易见的问题,就是算术运算引入了那个小偏差。 - Patrick Parker

7
一个double类型的数字具有52位二进制数字或15.95位十进制数字的精度。请参见http://en.wikipedia.org/wiki/IEEE_754-2008。要记录double类型数字的完整精度,至少需要16位十进制数字。[但请参见下面的第四个编辑]。
顺便说一下,这意味着有效数字。
回答OP的编辑:
您的浮点数转换为十进制字符串运行时输出的数字比实际显著数字多得多。一个double类型只能容纳52位有效数字(实际上是53位,如果计算没有存储的“隐藏”1的话)。这意味着分辨率不超过2 ^ -53 = 1.11e-16。
例如:1 + 2 ^ -52 = 1.0000000000000002220446049250313 . . . .
这些小数位,.0000000000000002220446049250313 . . . . 是double类型的最小二进制“步长”,当转换为十进制时。 double类型内部的“步长”是:
.0000000000000000000000000000000000000000000000000001 在二进制中。 请注意,二进制步长是精确的,而十进制步长是不精确的。
因此,上面的十进制表示,
1.0000000000000002220446049250313 . . .
是一个不精确的二进制数字:
1.0000000000000000000000000000000000000000000000000001。 第三次编辑: double类型的下一个可能值,在精确的二进制中为:
1.0000000000000000000000000000000000000000000000000010
在十进制中转换为不精确的数字为:
1.0000000000000004440892098500626 . . . .
因此,所有这些额外的十进制数字实际上并不重要,它们只是基本转换工件。 第四次编辑: 尽管double类型最多存储16位有效的十进制数字,有时需要17位十进制数字来表示该数字。原因与数字切片有关。
如上所述,double类型内部有52 + 1个二进制数字。 “+1”是假定的前导1,既不存储也不重要。对于整数而言,这52个二进制数字形成介于0和2^53 - 1之间的数字。需要多少位十进制数字来存储这样的数字?嗯,log_10 (2^53 - 1)约为15.95。因此,最多需要16位十进制数字。让我们将它们标记为d_0到d_15。
现在考虑IEEE浮点数也有一个二进制指数。当我们将指数增加2时会发生什么?我们已经将我们的52位数字乘以了4,无论它是什么。现在,我们的52个二进制数字不再与我们的十进制数字d_0到d_15完全对齐,而是在d_16中表示一些重要的二进制数字。然而,由于我们乘以小于10的某个数,我们仍然在d_0中表示一些重要的二进制数字。因此,我们的15.95个十进制数字现在占据了d_1到d_15,以及d_0的一些高位和d_16的一些低位。这就是为什么有时需要17个十进制数字来表示IEEE双精度的原因。
第五次编辑
修正数字错误。

当我使用科学计数法和精度时,它正如您所描述的那样工作。 (numeric_limits<double> :: digits10 + 1) == 16。在我的原始代码中,这表示没有丢失精度。但是,当我打印出53个数字时,它表明我使用的精度比我使用的更高(请参见上面的编辑)。我不理解这种差异。 - Martin York

4
最简单的方法(对于IEEE 754双精度)来保证往返转换是始终使用17个有效数字。但这样做的缺点是有时会包含不必要的噪音数字(0.1 →“0.10000000000000001”)。
一个适合我的方法是用15位精度的sprintf格式化数字,然后检查atof是否将其转换回原始值。如果不能,请尝试16位。如果还不行,就使用17位。
你可以尝试使用David Gay的算法(在Python 3.1中用于实现float.__repr__)。

2
在“尝试15、16、17”过程中存在一个有趣的异常,可能会跳过一个往返的16位字符串--请参阅我的文章http://www.exploringbinary.com/the-shortest-decimal-string-that-round-trips-may-not-be-the-nearest/。 - Rick Regan

3
感谢ThomasMcLeod指出表格计算中的错误。
仅有少数情况下,使用15、16或17位数字可以保证往返转换。数字15.95来自于将2^53(1个隐式位和52个有效数字/尾数)进行取整,得到一个介于10^15到10^16之间的整数(更接近于10^16)。
考虑具有指数为0的双精度值x,即它落在浮点范围1.0 <= x < 2.0内。隐式位将标记x的2^0部分。显著位的最高位将表示比指数小一级(从0开始)<=> -1 <=> 2^-1或0.5组成部分。
接下来是0.25位,然后是0.125、0.0625、0.03125、0.015625等(见下表)。因此,值1.5将由两个组成部分相加表示:隐式位表示1.0和最高显著位表示0.5。
这说明从隐式位向下,您有52个额外的显式位来表示可能的组成部分,其中最小的组成部分为0(指数)-52(显著位的显式位)=-52 <=> 2^-52,根据下表,这比15.95个有效数字要多得多(确切地说是37)。换句话说,在2^0范围内,不等于1.0本身的最小数字是2^0 + 2^-52,它是1.0加上接近2^-52(下方)的数 =(确切地说)1.0000000000000002220446049250313080847263336181640625,这个值我认为有53个有效数字。使用17位格式“精度”,该数字将显示为1.0000000000000002,这取决于正确转换的库。
因此,“在17位数字中进行往返转换”可能并不是一个有效的概念。
2^ -1 = 0.5000000000000000000000000000000000000000000000000000
2^ -2 = 0.2500000000000000000000000000000000000000000000000000
2^ -3 = 0.1250000000000000000000000000000000000000000000000000
2^ -4 = 0.0625000000000000000000000000000000000000000000000000
2^ -5 = 0.0312500000000000000000000000000000000000000000000000
2^ -6 = 0.0156250000000000000000000000000000000000000000000000
2^ -7 = 0.0078125000000000000000000000000000000000000000000000
2^ -8 = 0.0039062500000000000000000000000000000000000000000000
2^ -9 = 0.0019531250000000000000000000000000000000000000000000
2^-10 = 0.0009765625000000000000000000000000000000000000000000
2^-11 = 0.0004882812500000000000000000000000000000000000000000
2^-12 = 0.0002441406250000000000000000000000000000000000000000
2^-13 = 0.0001220703125000000000000000000000000000000000000000
2^-14 = 0.0000610351562500000000000000000000000000000000000000
2^-15 = 0.0000305175781250000000000000000000000000000000000000
2^-16 = 0.0000152587890625000000000000000000000000000000000000
2^-17 = 0.0000076293945312500000000000000000000000000000000000
2^-18 = 0.0000038146972656250000000000000000000000000000000000
2^-19 = 0.0000019073486328125000000000000000000000000000000000
2^-20 = 0.0000009536743164062500000000000000000000000000000000
2^-21 = 0.0000004768371582031250000000000000000000000000000000
2^-22 = 0.0000002384185791015625000000000000000000000000000000
2^-23 = 0.0000001192092895507812500000000000000000000000000000
2^-24 = 0.0000000596046447753906250000000000000000000000000000
2^-25 = 0.0000000298023223876953125000000000000000000000000000
2^-26 = 0.0000000149011611938476562500000000000000000000000000
2^-27 = 0.0000000074505805969238281250000000000000000000000000
2^-28 = 0.0000000037252902984619140625000000000000000000000000
2^-29 = 0.0000000018626451492309570312500000000000000000000000
2^-30 = 0.0000000009313225746154785156250000000000000000000000
2^-31 = 0.0000000004656612873077392578125000000000000000000000
2^-32 = 0.0000000002328306436538696289062500000000000000000000
2^-33 = 0.0000000001164153218269348144531250000000000000000000
2^-34 = 0.0000000000582076609134674072265625000000000000000000
2^-35 = 0.0000000000291038304567337036132812500000000000000000
2^-36 = 0.0000000000145519152283668518066406250000000000000000
2^-37 = 0.0000000000072759576141834259033203125000000000000000
2^-38 = 0.0000000000036379788070917129516601562500000000000000
2^-39 = 0.0000000000018189894035458564758300781250000000000000
2^-40 = 0.0000000000009094947017729282379150390625000000000000
2^-41 = 0.0000000000004547473508864641189575195312500000000000
2^-42 = 0.0000000000002273736754432320594787597656250000000000
2^-43 = 0.0000000000001136868377216160297393798828125000000000
2^-44 = 0.0000000000000568434188608080148696899414062500000000
2^-45 = 0.0000000000000284217094304040074348449707031250000000
2^-46 = 0.0000000000000142108547152020037174224853515625000000
2^-47 = 0.0000000000000071054273576010018587112426757812500000
2^-48 = 0.0000000000000035527136788005009293556213378906250000
2^-49 = 0.0000000000000017763568394002504646778106689453125000
2^-50 = 0.0000000000000008881784197001252323389053344726562500
2^-51 = 0.0000000000000004440892098500626161694526672363281250
2^-52 = 0.0000000000000002220446049250313080847263336181640625

1
首先,转换的数学计算不正确。例如,2^-7应该是0.0078125,而不是你发布的0.0070125。其次,即使最后一行的数字是正确的,它们也不具有显著性。它们只是基础转换的产物。请参见我上面的帖子。 - ThomasMcLeod
@ThomasMcLeod:感谢您指出错误。关于您的说法“它们并不重要”,我持有不同意见。在绝大多数情况下,它们不会很重要,但在一些情况下它们确实很重要。我的帖子试图通过展示实际涉及的数字位数来指出四舍五入和转换的复杂性。 - Olof Forshell
@Olof,你如何定义“显著”?如果我们将1除以3,我们得到0.3333333333333333……,但这并不意味着我们有无限个显著数字。基本规则:数学运算的结果不能比该运算的任何数字输入的显著数字多。 - ThomasMcLeod
@Olof,π和e是无理数,在任何有理基数下都具有无限数量的有效数字。但是,在double中对pi和e的近似表示是有理的,每个具有有限有效数字的数字也是如此。关键在于,当您从一种基数转换到另一种基数时,您不会也不能增加数字的有效位数(fp精度)。这样做将需要增加固定长度数据的信息传输能力,这是不可能的。我向您提出挑战,找到需要进行往返转换的17个以上数字的情况。 - ThomasMcLeod
@ThomasMcLeod:如果电阻测量值为3.00欧姆,则在施加1伏特电压的情况下通过它的电流量将是0.333安培(不是无限精度,因为我们不能确定该值是否大于0.3332或小于0.3334),但是三分之一的分数是0.333333...具有无限精度,因为无论写多少位数字,都可以确定使下一个数字为4会产生错误地大于1/3的值,而使其为2会产生错误地小于1/3的值。 - supercat
显示剩余2条评论

0

@ThomasMcLeod:我认为显著数字规则来自于我的领域——物理学,意味着更微妙的东西:

如果你测量得到值为1.52,但是不能从刻度上读取更多细节,并且你需要将另一个数(例如另一个测量的结果,因为这个刻度太小)加到它上面,比如2,那么结果(显然)只有2位小数,即3.52。 同样地,如果你将1.1111111111加到1.52的值上,你得到的值是2.63(没有更多了!)。

这个规则的原因是防止你欺骗自己,认为你从计算中得到的信息比你通过测量所放入的信息更多(这是不可能的,但是通过填充垃圾数据会看起来像这样)。

话虽如此,这个具体的规则仅适用于加法(对于加法而言:结果的误差是两个误差的和——因此,如果你只测量了一个错误,那就太倒霉了,你的精度就没了……)。

如何获取其他规则: 假设a是测量的数字,δa是误差。假设您的原始公式为: f:= ma 假设您还使用误差δm(让它成为正面)测量m。 然后实际极限为: f_up =(m + δm)(a + δa) 和 f_down =(m-δm)(a-δa) 所以, f_up = ma + δmδa +(δma + mδa) f_down = ma + δmδa-(δma + mδa) 因此,现在有效数字更少: f_up〜ma +(δma + mδa) f_down〜ma-(δma + mδa) 所以 δf = δma + mδa 如果您查看相对误差,则会得到: δf / f = δm / m + δa / a
对于除法,它是 δf / f = δm / m-δa / a
希望这能传达要点,也希望我没有犯太多错误,现在很晚了 :-)

简而言之:有效数字是指输出结果中实际来自于输入数据的数字位数(在现实世界中,而不是浮点数所呈现的扭曲图像中)。 如果你的测量值为1(无误差)和3(无误差),而函数应该是1/3,那么是的,所有无限数字都是实际有效数字。否则,反向操作将无法正常工作,因此它们显然必须是。

如果有效数字规则在其他领域有完全不同的含义,请继续 :-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接