双面打印而不失精度

Mar*_*ork 32 c++ floating-point iostream

如何在流中打印双精度数,以便在读入时不会丢失精度?

我试过了:

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;
Run Code Online (Sandbox Code Playgroud)

这不像我预期的那样有效.

但我可以提高精度(这让我感到惊讶,因为我认为数字10是所需的最大值).

ss << std::setprecision(std::numeric_limits<T>::digits10 + 2) << v << " ";
                                                 //    ^^^^^^ +2
Run Code Online (Sandbox Code Playgroud)

它与有效位数有关,前两位不计入(0.01).

那么有人看过准确表示浮点数吗?我需要做的流上究竟是什么神奇的咒语?

经过一些实验:

问题在于我的原始版本.小数点后面的字符串中有无效数字会影响精度.

因此,为了弥补这一点,我们可以使用科学记数法来补偿:

ss << std::scientific
   << std::setprecision(std::numeric_limits<double>::digits10 + 1)
   << v;
Run Code Online (Sandbox Code Playgroud)

这仍然不能解释+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";
Run Code Online (Sandbox Code Playgroud)

它导致:

1.000000000000000e-02
1.0000000000000002e-02
1.00000000000000019428902930940239457413554200000000000e-02
Run Code Online (Sandbox Code Playgroud)

基于@Stephen佳能的答案如下:

我们可以使用printf()格式化程序"%a"或"%A"完全打印出来.要在C++中实现这一点,我们需要使用固定和科学的操纵器(参见n3225:22.4.2.2.2p5表88)

std::cout.flags(std::ios_base::fixed | std::ios_base::scientific);
std::cout << v;
Run Code Online (Sandbox Code Playgroud)

现在我已定义:

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);}
Run Code Online (Sandbox Code Playgroud)

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

Dig*_*oss 17

说"浮点不准确"是不正确的,尽管我承认这是一个有用的简化.如果我们在现实生活中使用8或16基数,那么这里的人会说"基数10小数部分包是不准确的,为什么有人做过那些?".

问题是积分值恰好从一个基数转换为另一个基数,但是小数值不能,因为它们代表了积分步骤的一部分,并且只使用了少数几个.

浮点运算在技术上非常准确.每个计算都有一个且只有一个可能的结果.这里一个问题,那就是大多数小数有基2表示的是重复.实际上,在序列0.01,0.02,... 0.99中,仅有3个值具有精确的二进制表示.(0.25,0.50和0.75.)有96个值重复,因此显然没有准确表示.

现在,有许多方法可以写入和读回浮点数而不会丢失一个位.我们的想法是避免尝试用基数10分数表示二进制数.

  • 把它们写成二进制.目前,每个人都实现了IEEE-754格式,因此只要您选择字节顺序并且只读取或读取该字节顺序,那么这些数字将是可移植的.
  • 将它们写为64位整数值.在这里你可以使用通常的基数10.(因为你代表的是64位别名整数,而不是52位分数.)

您也可以只写更多的小数位数.这是否逐位精确将取决于转换库的质量,我不确定在这里我会依靠完美的准确度(来自软件).但是任何错误都会非常小,而原始数据肯定没有低位信息.(52位的物理和化学常数都不知道,地球上的任何距离都没有被测量到52位精度.)但对于备份或恢复,可以自动比较逐位精度,显然不太理想.

  • @MSN:不,"正确"是正确的.浮点*经常*不准确,但许多整数算法也是如此.完全可以用浮点编写精确的算法(事实上,这是我得到报酬的很大一部分). (2认同)
  • 不认为FPU的+1是随机数生成器. (2认同)

Ste*_*non 14

如果您不想丢失精度,请不要以十进制打印浮点值.即使您打印足够的数字来准确表示数字,并非所有实现都在整个浮点范围内对十进制字符串进行了正确的舍入转换,因此您可能仍会失去精度.

请改用十六进制浮点.在C:

printf("%a\n", yourNumber);
Run Code Online (Sandbox Code Playgroud)

C++ 0x hexfloat为iostream 提供了相同的操作器(在某些平台上,使用std::hex修饰符具有相同的结果,但这不是一个可移植的假设).

出于若干原因,优选使用十六进制浮点.

首先,打印值始终是准确的.在写入或读取以这种方式格式化的值时不会发生舍入.除了准确性优势之外,这意味着使用经过良好调整的I/O库可以更快地读取和写入这些值.它们还需要更少的数字来准确表示值.

  • `%a` 说明符在 C 标准中已经存在 11 年了;任何仍然不支持它的平台都不能真正声称是“C”。`hexfloat` 是在 C++0x 中添加的(我相信——我不是 C++ 人),所以它的使用可能不太便携。 (2认同)

Yal*_*ang 9

我对这个问题很感兴趣,因为我正试图将数据序列化为JSON.

我想我有一个更明确的解释(少用手放弃)为什么17个十进制数字足以无损地重建原始数字:

在此输入图像描述

想象一下3条数字线:
1.对于原始基数2数字
2.对于圆形基数10表示
3.对于重建数字(与#1相同,因为在基数2中)

当您以图形方式转换为基数10时,在第1个最接近tic的第2个数字行上选择tic.同样,当您从圆角基数10值重建原始时.

我的关键观察是,为了允许精确重建,基本10步长(量子)必须是<基2量子.否则,您不可避免地会以红色显示错误的重建.

对于base2表示,当指数为0时的具体情况.然后base2量子将是2 ^ -52~ = 2.22*10 ^ -16.最接近的基数10量子小于10 ^ -16.既然我们知道了所需的10量子,那么编码所有可能值需要多少位数?鉴于我们只考虑exponent = 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位数就足够的趋势.

希望你喜欢我的解释.


vit*_*aut 7

在 C++20 中,您将能够使用它std::format来执行此操作:

std::stringstream ss;
double v = 0.1 * 0.1;
ss << std::format("{}", v);
double u;
ss >> u;
assert(v == u);
Run Code Online (Sandbox Code Playgroud)

默认浮点格式是具有往返保证的最短十进制表示。与使用from的精度相比max_digits10(not digits10which is not std::numeric_limitsthat is not适合通过十进制往返)相比,此方法的优点是它不会打印不必要的数字。

在此期间,您可以使用的{} FMT库std::format是基于。例如(godbolt):

fmt::print("{}", 0.1 * 0.1);
Run Code Online (Sandbox Code Playgroud)

输出(假设 IEEE754 double):

0.010000000000000002
Run Code Online (Sandbox Code Playgroud)

{fmt} 使用Dragonbox 算法进行快速二进制浮点到十进制转换。除了给出最短的表示之外,它还printfiostream 和 iostream 的常见标准库实现快 20-30 倍

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


Tho*_*eod 5

double的精度为52位二进制数或15.95位十进制数.见http://en.wikipedia.org/wiki/IEEE_754-2008.在所有情况下,您需要至少16个十进制数字来记录双精度的完整精度.[但请参见下面的第四个编辑].

顺便说一句,这意味着有效数字.

回答OP编辑:

您的浮点数到十进制字符串运行时输出的数字比重要数字更多.双精度数只能容纳52位有效数字(实际上,53,如果计算未存储的"隐藏"1).这意味着分辨率不超过2 ^ -53 = 1.11e-16.

例如:1 + 2 ^ -52 = 1.0000000000000002220446049250313....

那些十进制数字,.0000000000000002220446049250313....是转换为十进制时 double中最小的二进制"step" .

双重内部的"步骤"是:

二进制中的 .00000000000000000000000000000000000000000000000000000001 .

请注意,二进制步骤是精确的,而十进制步骤是不精确的.

因此上面的十进制表示,

1.0000000000000002220446049250313...

是精确二进制数的不精确表示:

1.0000000000000000000000000000000000000000000000000001.

第三编辑:

double的下一个可能值,精确二进制是:

1.0000000000000000000000000000000000000000000000000010

十进制不精确地转换为

1.0000000000000004440892098500626....

所以十进制中的所有额外数字都不是很重要,它们只是基本的转换工件.

第四编辑:

虽然双重存储最多16个有效十进制数字,但有时需要17个十进制数字来表示数字.原因与数字切片有关.

如上所述,double中有52 + 1个二进制数字."+ 1"是假设的前导1,既不存储也不重要.在整数的情况下,那52个二进制数字形成0到2 ^ 53-1之间的数字.1.存储这样一个数字需要多少个十进制数字?那么,log_10(2 ^ 53-1)大约是15.95.因此最多需要16个十进制数字.让我们将这些d_0标记为d_15.

现在考虑IEEE浮点数也有二进制指数.当我们用2增加exponet时会发生什么?我们将52位数字乘以4乘以现在,而不是我们的52位二进制数字与我们的十进制数字d_0到d_15完美对齐,我们在d_16中有一些重要的二进制数字.但是,由于我们乘以小于10的值,我们仍然在d_0中表示有效的二进制数字.所以我们的15.95十进制数字现在占据d_1到d_15,再加上d_0的一些高位和d_16的一些低位.这就是为什么有时需要17个十进制数来表示IEEE双精度数.

第五编辑

修正了数字错误