【问题标题】:How does printf extract digits from a floating point number?printf 如何从浮点数中提取数字?
【发布时间】:2018-12-05 17:42:15
【问题描述】:

printf 等函数如何从浮点数中提取数字?我理解原则上如何做到这一点。给定一个数字x,您想要其中的第一个n 数字,将x 缩放10 的幂,使x 介于pow(10, n)pow(10, n-1) 之间。然后将x转换成整数,取整数的位数。

我试过这个,它奏效了。有点。我的答案与printf 给出的前 16 位十进制数字的答案相同,但之后的答案往往不同。 printf是怎么做到的?

【问题讨论】:

  • a double(64 位 IEEE-754 浮点数)无论如何只有大约 15 位的精度
  • @vu1p3n0x:一个 IEEE-754 浮点数精确地表示一个数字。根据 IEEE-754 标准中的规范,它是无限精确的。各种应用程序有时希望将这些数字精确地转换为十进制。
  • @EricPostpischil:您写的内容在技术上是正确的,但 vu1pn3n0x 所说的是数据类型的精度,在其大部分范围内约为 15-16 位。
  • @DietrichEpp:数据类型在其整个范围内都是准确的。 IEEE-754 标准对此很清楚:每个浮点数精确地表示一个数字。 操作 执行舍入。数字没有近似值。 vu1p3n0x 所说的是一个关于使用二进制浮点作为十进制的粗略概念。但是,使用浮点不仅是一个糟糕的概念,而且与这个问题无关。 OP 不会询问任何涉及浮点数的问题,这些浮点数已被用于尝试十进制算术。他们询问正确打印浮点数。
  • @EricPostpischil:您对“精度”一词的使用不符合 IEEE 754 标准。 precision:一种格式中可以表示的最大有效位数p,或四舍五入的位数。请不要只假装第二个意义的存在只是为了纠正使用第一个意义的人。

标签: c++ c floating-point printf


【解决方案1】:

经典的实现是 David Gay 的 dtoa。确切的细节有些晦涩难懂(请参阅Why does "dtoa.c" contain so much code?),但总的来说,它通过使用比 32 位、64 位甚至 80 位浮点数所能获得的更高精度进行基本转换来工作。为此,它使用所谓的“bigints”或任意精度数字,它可以容纳尽可能多的数字,你可以在内存中。 Gay 的代码经过修改已被复制到无数其他库中,包括 C 标准库的常见实现(因此它可能会为您的 printf 提供支持)、Java、Python、PHP、JavaScript 等。

(附带说明...并非所有这些 Gay dtoa 代码的副本都保持最新,因此因为 PHP 使用了 old 版本的 strtod,它在解析 2.2250738585072011e-308 时会挂起.)

一般来说,如果您以“明显”且简单的方式(例如乘以 10 的幂然后转换整数)进行操作,您将损失少量精度,并且某些结果将不准确...但是也许你会得到正确的前 14 或 15 位数字。 Gay 的 dtoa() 实现声称所有数字都是正确的……但结果是,代码很难理解。跳到底部查看 strtod 本身,您可以看到它以“快速路径”开始,该路径仅使用普通的浮点运算,但随后它会检测该结果是否不正确并使用使用 bigints 的更可靠的算法,该算法适用于所有情况(但速度较慢)。

该实现有以下引用,您可能会觉得有趣:

* 灵感来自“如何准确地打印浮点数” * Guy L. Steele, Jr. 和 Jon L. White [Proc。 ACM SIGPLAN '90,第 112-126 页]。

该算法通过计算产生给定二进制数的十进制数字范围来工作,并且通过使用更多数字,范围变得越来越小,直到您得到准确的结果或者您可以正确地四舍五入到所请求的位数.

特别是从 sec 2.2 算法,

该算法使用精确的有理算术来执行其计算,因此不会损失准确性。为了生成数字,该算法对数字进行缩放,使其形式为 0.d1d2...,其中 d1 sub>, d2, ..., 是 base-B 数字。第一个数字是通过将缩放后的数字乘以输出基数 B 并取整数部分来计算的。余数用于使用相同的方法计算其余数字。

然后算法可以继续,直到它得到准确的结果(这总是可能的,因为浮点数是以 2 为底,而 2 是 10 的因数)或直到它具有所需的位数。论文进一步证明了算法的正确性。


另请注意,并非所有printf 的实现都基于 Gay 的 dtoa,这只是一个被大量复制的特别常见的实现。

【讨论】:

    【解决方案2】:

    有多种方法可以将浮点数无错误地转换为十进制数(精确或四舍五入到所需精度)。

    一种方法是使用小学教的算术。 C 提供了处理浮点数的函数,例如 frexp,它将分数(也称为有效数,通常被错误地称为尾数)和指数分开。给定一个浮点数,您可以创建一个大数组来存储十进制数字,然后计算这些数字。浮点数的小数部分中的每个位代表 2 的某个幂,由浮点数中的指数确定。因此,您可以简单地将“1”放入数字数组中,然后使用小学算术将其乘以或除以所需的次数。您可以对每个位进行此操作,然后将所有结果相加,总和是等于浮点数的十进制数字。

    商业printf 实现将使用更复杂的算法。讨论它们超出了 Stack Overflow 问答的范围。这方面的开创性论文是Correctly Rounded Binary-Decimal and Decimal-Binary Conversions by David M. Gay。 (似乎有一个副本here,但这似乎是由第三方托管的;我不确定它的官方性或持久性。网络搜索可能会出现其他来源。)最近的一篇带有算法的论文用于将二进制浮点数转换为具有唯一区分值所需的最短位数的十进制数是Printing Floating-Point Numbers: An Always Correct Method by Marc Andrysco, Ranjit Jhala, and Sorin Lerner

    如何完成的一个关键是printf 不会只使用浮点格式及其操作来完成工作。它将使用某种形式的扩展精度算术,通过使用具有更多位的整数格式处理浮点数的一部分,通过将浮点数分成几部分并使用多个浮点数来处理它,或者使用精度更高的浮点格式。

    请注意,您问题的第一步,将 x 乘以 10 的幂,已经有两个舍入误差。首先,并非所有的 10 次方都可以用二进制浮点数精确表示,因此仅产生这样的 10 次方必然会有一些表示错误。然后,将x 乘以另一个数字通常会产生无法精确表示的数学结果,因此必须将其四舍五入为浮点格式。

    【讨论】:

      【解决方案3】:

      C 或 C++ 标准都没有为此类事情规定某种算法。因此无法回答printf 是如何做到这一点的。

      如果您想了解printf 实现的示例,可以查看此处:http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/vfprintf.c 和此处:http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/printf_fp.c

      【讨论】:

      • 浮点数的数学特性可用于将其转换为十进制,而无需了解其表示形式,因此一种算法可用于一般浮点格式。
      • 另外,现在很少有平台不使用IEEE754。
      猜你喜欢
      • 2021-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-04
      • 2011-06-09
      相关资源
      最近更新 更多