双面打印而不失精度

时间:2011-01-19 17:49:40

标签: 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;

这没有按照我的预期发挥作用。

但我可以提高精确度(这让我感到惊讶,因为我认为数字10是最高要求)。

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佳能的答案如下:

我们可以使用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;

目前我已定义:

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?

8 个答案:

答案 0 :(得分:17)

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

问题在于积分值恰好从一个基数转换为另一个基数,但小数值不能,因为它们代表整数步骤的一部分,只使用其中的一部分。

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

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

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

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

答案 1 :(得分:14)

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

使用十六进制浮点代替。在C:

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

C ++ 0x为iostreams提供了hexfloat操纵器,它做同样的事情(在某些平台上,使用std::hex修饰符具有相同的结果,但这不是一个可移植的假设)。 / p>

出于多种原因,首选使用十六进制浮点数。

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

答案 2 :(得分:9)

我对这个问题很感兴趣,因为我正试图将我的数据序列化为&amp;来自JSON。

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

enter image description here

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

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

我的关键观察是,为了允许精确重建,基本10步长(量子)必须<1。基数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位数的趋势。

希望你喜欢我的解释。

答案 3 :(得分: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。 。 。 。是转换为十进制时双精度中最小的二进制“步”。

双人内部的“步骤”是:

二进制

.0000000000000000000000000000000000000000000000000001

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

因此上面的十进制表示,

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双倍。

第五次修改

修正数字错误

答案 4 :(得分:5)

在C ++ 20中,您将可以使用std::format来做到这一点:

public class SerialDate {

    //days from 1899-12-31 to Instant.EPOCH (1970-01-01T00:00:00Z)
    public static final long EPOCH = -25568L;
    
    private long serialDays;
    private long epochDays;
    
    /**
     * @param date number of days since <i>January 0, 1899</i>
     */
    public SerialDate(long date) {
        serialDays = date;
        if (date > 59)//Lotus123 bug
            --date;
        epochDays = EPOCH + date;
    }
    
    /**
     * @return days since 1970-01-01
     */
    public long toEpochDays() {
        return epochDays;
    }
    
    /**
     * @return a value suitable for an Excel date
     */
    public double getSerialDate() {
        return serialDays;
    }
    
    public static void main(String[] args) {
        LocalDate dt = LocalDate.ofEpochDay(new SerialDate(39448).toEpochDays());
        System.out.println(dt);//prints 2008-01-01
    }
}

默认浮点格式是带有往返保证的最短十进制表示形式。与使用std::stringstream ss; double v = 0.1 * 0.1; ss << std::format("{}", v); double u; ss >> u; assert(v == u); 中的max_digits10的精度(不适合digits10,不适用于十进制的往返)相比,此方法的优点是它不会打印不必要的数字。 / p>

在此期间,您可以使用the {fmt} librarystd::numeric_limits是基于。例如(godbolt):

std::format

输出(假设IEEE754 fmt::print("{}", 0.1 * 0.1); ):

double

{fmt}使用the Dragonbox algorithm进行快速的二进制浮点数到十进制转换。除了提供最短的表示方式,它还比0.010000000000000002 和iostream的通用标准库实现20-30x faster

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

答案 5 :(得分:3)

保证往返转换的最简单方法(IEEE 754双倍)始终使用17位有效数字。但这有一个缺点,有时包括不必要的噪音数字(0.1→“0.10000000000000001”)。

对我有效的方法是sprintf具有15位精度的数字,然后检查atof是否返回原始值。如果没有,请尝试16位数。如果 不起作用,请使用17。

您可能想尝试David Gay's algorithm(在Python 3.1中用于实现float.__repr__)。

答案 6 :(得分:1)

感谢ThomasMcLeod指出我的表格计算中的错误

为了保证使用15或16或17位的往返转换仅适用于相对较少的情况。数字15.95来自于2 ^ 53(1个隐含位+有效数字中的52位/“尾数”),它出现在10 ^ 15到10 ^ 16(接近10 ^ 16)范围内的整数。

考虑指数为0的双精度值x,即它落入浮点范围范围1.0&lt; = x&lt; 2.0。隐含位将标记x的2 ^ 0分量(部分)。有效数的最高显式位将表示下一个较低指数(从0开始)&lt; =&gt; -1 =&gt; 2 ^ -1或0.5分量。

下一位0.25,0.125,0.0625,0.03125,0.015625之后的那些(见下表)。因此,值1.5将由两个加在一起的组件表示:隐含位表示1.0,最高显式有效位表示0.5。

这说明从隐式位向下,您有52个额外的显式位来表示可能的组件,其中最小值为0(指数) - 52(有效位中的显式位)= -52 =&gt;根据下表显示的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

答案 7 :(得分:0)

@ThomasMcLeod:我认为重要的数字规则来自我的领域,物理,并且意味着更微妙的东西:

如果您的测量值达到1.52并且您无法从比例尺中读取任何更多细节,并且说您应该添加另一个数字(例如另一个测量值,因为这个尺度太小)到它,比方说2,那么结果(显然)只有2位小数,即3.52。 但同样,如果将1.1111111111添加到值1.52,则得到值2.63(仅此而已!)。

规则的原因是为了防止你开玩笑地认为你从计算中获得的信息比你通过测量得到的更多(这是不可能的,但是看起来就像填充垃圾一样,见上文)。

那就是说,这个特定的规则仅用于添加(对于加法:结果的错误是两个错误的总和 - 所以如果你只测量一个错误,虽然运气,你的精确度......)。

如何获得其他规则: 假设a是测量数,δ是误差。假设你原来的公式是: f:= m a 假设你也用误差δm测量m(让它成为正面)。 然后实际限制是: f_up =(m +δm)(a +δa) 和 f_down =(m-δm)(a-δa) 所以, f_up = m a +δmδa+(δma +mδa) f_down = m a +δmδa - (δma +mδa) 因此,现在有效数字甚至更少: f_up~m a +(δma +mδa) f_down~m a-(δma +mδa) 所以 δf=δma+mδa 如果你看相对错误,你会得到: ΔF/ F =ΔM/米+ΔA/ A

对于分裂而言 ΔF/ F =ΔM/ M-ΔA/ A

希望获得要点并希望我没有犯太多错误,这里已经很晚了: - )

tl,dr:有效数字表示输出中有多少数字实际来自输入中的数字(在现实世界中,而不是浮点数具有的失真图像)。 如果您的测量值为1,“no”错误,3为“no”错误且函数应为1/3,则是,所有无限数字均为实际有效数字。否则,逆操作将不起作用,所以很明显它们必须是。

如果有效数字规则意味着在另一个字段中完全不同,请继续: - )