在不丢失精度但具有最少位数的情况下格式化双精度数
Format a double without losing precision but with a minimum number of digits
在我们的 C++ 代码库中,我们有一个默认的格式化方法,可以将 double
浮点数转换为字符串,特别用于 JSON 序列化和调试日志。对于默认的数字格式,我有以下相互矛盾的要求:
- 有利于人类的可读性。
1000
优于 1e3
或 0.125
优于 1.25e-1
.
- 保持精度。比
3.14
. 更喜欢 3.1415926535897931
- 避免十进制数字出现虚假数字。比
0.10000000000000001
. 更喜欢 0.1
到目前为止,我发现的最佳权衡是使用与 printf("%.15g", value)
格式等效的方法。它满足 1 和 3 的要求,但不完全 2。大约有 4 位的精度损失。
其他人使用基于 "%.17g"
的默认格式,它满足 1 和 2 的要求,但不满足 3。例如,数字 0.2 的格式为 0.20000000000000001
.
在两者之间,格式 "%.16g"
接近于满足 2 和 3 的要求,但并不总是满足两者的要求。
作为示例,我希望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++ 中实现相同的行为?
在 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);
}
Microsoft 讲座的好消息是,除了解决最短往返 问题外,MSVC 中的实现速度非常快!它基于令人难以置信的 Ryu algorithm.
坏消息是,在撰写本文时,std::to_chars
仅适用于 Microsoft 工具链中的浮点数。 Clang libc++ 和 GCC libstdc++ 中的实现目前仅限于整数。
在我们的 C++ 代码库中,我们有一个默认的格式化方法,可以将 double
浮点数转换为字符串,特别用于 JSON 序列化和调试日志。对于默认的数字格式,我有以下相互矛盾的要求:
- 有利于人类的可读性。
1000
优于1e3
或0.125
优于1.25e-1
. - 保持精度。比
3.14
. 更喜欢 - 避免十进制数字出现虚假数字。比
0.10000000000000001
. 更喜欢
3.1415926535897931
0.1
到目前为止,我发现的最佳权衡是使用与 printf("%.15g", value)
格式等效的方法。它满足 1 和 3 的要求,但不完全 2。大约有 4 位的精度损失。
其他人使用基于 "%.17g"
的默认格式,它满足 1 和 2 的要求,但不满足 3。例如,数字 0.2 的格式为 0.20000000000000001
.
在两者之间,格式 "%.16g"
接近于满足 2 和 3 的要求,但并不总是满足两者的要求。
作为示例,我希望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++ 中实现相同的行为?
在 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);
}
Microsoft 讲座的好消息是,除了解决最短往返 问题外,MSVC 中的实现速度非常快!它基于令人难以置信的 Ryu algorithm.
坏消息是,在撰写本文时,std::to_chars
仅适用于 Microsoft 工具链中的浮点数。 Clang libc++ 和 GCC libstdc++ 中的实现目前仅限于整数。