【问题标题】:Format a double without losing precision but with a minimum number of digits格式化双精度而不损失精度,但位数最少
【发布时间】:2021-04-24 21:14:00
【问题描述】:

在我们的 C++ 代码库中,我们有一个默认的格式化方法来将 double 浮点数转换为字符串,这主要用于 JSON 序列化和调试日志。对于该默认数字格式,我有以下矛盾的要求:

  1. 有利于人类可读性。首选10001e30.1251.25e-1
  2. 保持精度。首选3.1415926535897931 而非3.14
  3. 避免十进制数的虚假数字。首选0.1 而不是0.10000000000000001

到目前为止,我发现的最佳权衡是使用等效于 printf("%.15g", value) 的格式。它满足要求13,但不完全满足2。有大约 4 位的精度损失。

其他人使用基于"%.17g" 的默认格式,它满足12 要求,但不满足3 要求。例如,数字 0.2 的格式为 0.20000000000000001

在两者之间,"%.16g" 格式接近满足要求 23,但并非总是同时满足这两个要求。

作为说明,我希望 0.3 被格式化为0.3,但是 0.1+0.2 由于舍入误差而稍大,被格式化为0.30000000000000004 看看有什么区别。

我编写了以下函数,以我希望的方式格式化浮点数,作为概念证明。然而,从性能的角度来看,这是不可接受的,因为它可以在 double 和字符串之间进行多达 34 次转换,与当前使用 "%.15g" 的实现相比,精度增益有限。

std::string doubleToString(double number)
{
    char buffer[32];
    long long intVal = static_cast<long long>(number);
    if(intVal == number)
    {
        sprintf(buffer, "%lld", intVal);
    }
    else
    {
        for(int i=1; i<=17; i++)
        {
            sprintf(buffer, "%.*g", i, number);
            double readBack = atof(buffer);
            if(readBack == number)
                break;
        }
    }
    return buffer;
}

我刚刚意识到 Python 已经按照我想要的方式格式化数字:

$ python3
Python 3.8.6 (default, Oct  8 2020, 14:06:32) 
[Clang 12.0.0 (clang-1200.0.32.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0.3
0.3
>>> 0.1+0.2
0.30000000000000004
>>> 

有没有办法在不牺牲太多性能的情况下在 C++ 中实现相同的行为?

【问题讨论】:

  • 一些线索 0.10000000000000001 == 0.1 是true。所以你可以放下长字符串。 0.20000000000000001 == 0.2 是true。再次放下长绳。但是0.30000000000000004 == 0.3false。所以你不能丢弃长字符串。
  • 请定义(或让您的需求作者定义)“虚假数字”。这应该是一个规则,而不是一个例子。 “0 的 16 位小数,后跟最不重要的 1”是一个定义,但可能不是他们想要的。
  • “保持精度。” 对浮点十进制数的实际含义并不明确。二进制分数可以产生非常多的小数位,但这些数字不一定是有效的。计算出唯一标识一个double 需要多少位数字并非易事。
  • 这听起来很像 Stephan 在他的 演示文稿(来自 cppcon 2019)中谈到的内容。试试看这里:youtube.com/watch?v=4P_kbF0EbZM这可能正是你要找的。​​span>
  • 不是一个简单的问题。例如,0.3 不能完全表示为二进制浮点值,因此任何将该值显示为“0.3”的代码都在隐藏信息。有时这就是你想要的,有时不是。

标签: c++ number-formatting


【解决方案1】:

在 Frodyne 发表评论后,我想出了一个非常简单快速的解决方案。 默认情况下,C++17 std::to_chars 函数格式化浮点数以满足最短往返要求。这意味着所有不同的浮点数在序列化后保持不同,并且要格式化的字符数被最小化。 所以转换在标准C++17中可以这样写。

#include <charconv>
#include <string>

std::string doubleToString(double number)
{
    char buffer[24];
    std::to_chars_result err = std::to_chars(buffer, buffer+sizeof(buffer), value);
    return std::string(buffer, err.ptr);
}

微软讲座的好消息是,除了解决最短往返问题外,MSVC 中的实现速度非常快!它基于令人难以置信的Ryu algorithm

坏消息是,在撰写本文时,std::to_chars 仅适用于 Microsoft 工具链中的浮点数。 Clang libc++GCC libstdc++ 中的实现目前仅限于整数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-19
    • 1970-01-01
    • 2010-10-16
    相关资源
    最近更新 更多