【问题标题】:dtoa vs sprintf vs Grisu3 algorithmdtoa vs sprintf vs Grisu3 算法
【发布时间】:2011-07-06 17:46:54
【问题描述】:

在 C++ 中将双精度数字呈现为字符串的最佳方法是什么?

我看到了文章 Here be dragons: advances in problems you didn’t even know you had,它讨论了打印浮点数。

我一直在使用sprintf。我不明白为什么我需要修改代码?

【问题讨论】:

  • 您可能不需要修改您的代码。去想想更紧迫的事情吧。
  • 您所说的“最佳”究竟是什么意思?
  • 作为最近使用 Grisu2 的人:如果您需要修改代码,您会从 profiling 中确切知道原因。即使这样,编写一个简单的定点双输出例程也会比使用 Grisu{2,3} 产生更好的性能,尽管它并不总是适用

标签: c++ floating-point rendering double


【解决方案1】:

如果您对 sprintf_s 感到满意,则不应更改。但是,如果您需要以库不支持的方式格式化输出,则可能需要重新实现 sprintf 的专用版本(使用任何已知算法)。

例如,JavaScript 对如何打印其数字有非常精确的要求(请参阅specification 的第 9.8.1 节)。简单地调用 sprintf 不能完成正确的输出。事实上,Grisu 的开发目的是为 JavaScript 编译器实现正确的数字打印。

Grisu 也比 sprintf 快,但除非浮点打印是您应用程序的瓶颈,否则这不应成为切换到其他库的理由。

【讨论】:

  • 按照今天的规范链接,我想部分已经重新编号。可能是 ECMA-262 第 9 版/2018 年 6 月标准第 103 页上的第 7.1.12.1 节“NumberToString (m)”是 Florian 所指的当前版本。
【解决方案2】:

啊哈!

您给出的文章中概述的问题是,对于某些数字,计算机显示的东西在理论上是正确的,但不是我们人类会使用的东西。

例如,如文章所说,1.2999999... = 1.3,因此如果您的结果是 1.3,那么计算机将其显示为 1.299999999...(非常)正确...但这不是您所看到的。 ..

现在的问题是计算机为什么要这样做?原因是计算机以 2 为底(二进制)计算,而我们通常以 10 为底(十进制)计算。结果是一样的(感谢上帝!),但内部存储和表示不一样。

有些数字在以 10 为底显示时看起来不错,例如 1.3,但其他数字则不然,例如 1/3 = 0.333333333....在以 2 为底时也是如此,有些数字在底数中“看起来”不错2(通常由 2 的分数组成)而其他不是。当计算机在内部存储数字时,它可能无法“准确地”存储它并存储最接近的可能表示形式,即使该数字在十进制中看起来“有限”。所以是的,在这种情况下,它有点“漂移”。如果你一次又一次地这样做,你可能会失去精确度。但是没有其他方法(除非使用能够存储分数的特殊数学库)

当计算机试图以 10 为基数将你给它的数字还给你时,问题就出现了。那么计算机可能会给你 1.299999 而不是你预期的 1.3。

这也是为什么您应该从不将浮点数与 ==、 进行比较,而是使用特殊函数 islessgreater(a, b) isgreater(a, b) 等等。

因此,您使用的实际函数 (sprintf) 很好,并且尽可能精确,它为您提供正确的值,您只需要知道在处理浮点数时,如果您期望 1.3

现在,如果您想“漂亮地打印”这些数字以获得最佳的“人类”表示(以 10 为基数),您可能需要使用一个特殊的库,例如您的 grisu3,它会尝试消除可能发生的漂移并将数字与最接近的以 10 为底的表示对齐。

现在库无法使用水晶球来查找是否漂移了哪些数字,因此您真的可能会以存储在计算机中的最大精度表示 1.2999999,并且库将“转换” " 到 1.3...但它并不比显示 1.29999 而不是 1.3 更差,也没有更精确。

如果您需要良好的可读性,这样的库会很有用。如果没有,那只是浪费时间。

希望有帮助!

【讨论】:

  • 大声笑我看了一会儿自己的答案,很难理解^^必须提高我的写作水平^^
【解决方案3】:

任何合理的语言做到这一点的最佳方法是:

  1. 使用您的语言的运行时库。永远不要自己滚动。即使您有编写它的知识和好奇心,您也不想测试它,也不想维护它。
  2. 如果您发现运行时库转换有任何不当行为,请提交错误
  3. 如果这些转换对您的程序来说是一个可衡量的瓶颈,请不要试图让它们更快。相反,找到一种方法来完全避免这样做。与其将数字存储为字符串,不如存储浮点数据(在可能控制字节顺序之后)。如果您需要字符串表示,请改用十六进制浮点格式。

我并不是要劝阻你或任何人。这些实际上是令人着迷的功能,但它们也非常复杂,并且尝试为任何非天真的实现设计良好的测试覆盖率甚至更加复杂。除非您准备好花几个月的时间思考这个问题,否则不要开始。

【讨论】:

  • [opinion]事物复杂的事实让我想更多地而不是更少地理解它。盲目地使用运行时库——或任何与此相关的库——会导致许多关于行为的假设,在我看来,这是一个等待发生的错误。 IMO 您真正想问自己的问题是:您想成为做出这些假设的普通程序员,还是想成为理解您的大多数假设的优秀程序员'正在制作。在这种情况下,几个月的工作真的无关紧要。正确方向的指针会有所帮助。
【解决方案4】:

您可能想要使用 Grisu(或 faster method)之类的东西,因为它为您提供最短的十进制表示,并保证往返行程,而 sprintf 只采用固定精度。好消息是 C++20 包含std::format,默认情况下会为您提供此功能。例如:

printf("%.*g", std::numeric_limits<double>::max_digits10, 0.3);

同时打印0.29999999999999999

puts(fmt::format("{}", 0.3).c_str());

打印0.3 (godbolt)。

同时可以使用the {fmt} librarystd::format是基于。 {fmt} 还提供了print 函数,使这更容易和更高效(godbolt):

fmt::print("{}", 0.3);

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

【讨论】:

    【解决方案5】:

    在 C++ 中,您为什么不使用 iostreams?您可能应该将cout 用于控制台,将ostringstream 用于面向字符串的输出(除非您非常需要使用printf 系列方法)。

    除非实际分析表明 CPU 是瓶颈(与 I/O 相比),否则您不必担心格式化性能。

    【讨论】:

    • iostream 比糖蜜慢。
    【解决方案6】:
    void outputdouble( ostringstream & oss, double d )
    {
        oss.precision( 5 );
        oss << d;
    }
    

    http://www.cplusplus.com/reference/iostream/ostringstream/

    【讨论】:

    • 很好:您不仅完全忽略了这个问题,而且还设法编写了一个带有意外副作用的 2 行函数!
    • @AlexeiAverchenko 我并没有完全忽略这个问题,笨蛋,我给出了一个即兴的简单实现,它通常回答了这个问题“将双精度数字呈现为字符串的最佳方法是什么? C++?”以及指向参考信息的链接,以便他获得更多信息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-09
    • 1970-01-01
    • 1970-01-01
    • 2021-07-06
    相关资源
    最近更新 更多