【问题标题】:How do you print the EXACT value of a floating point number?你如何打印浮点数的精确值?
【发布时间】:2011-03-14 00:21:46
【问题描述】:

首先,这不是一个浮点新手问题。我知道浮点运算的结果(更不用说超越函数)通常无法准确表示,并且大多数终止小数不能准确表示为二进制浮点数。

也就是说,每个可能的浮点值都与一个有理数完全对应(有理数p/q,其中q 是2 的幂),而后者又具有精确的十进制表示。

我的问题是:如何有效地找到这个精确的十进制表示? sprintf 和类似的函数通常只指定最多几个有效数字来唯一确定原始浮点值;他们不一定打印确切的十进制表示。我知道我使用过一种算法,但它非常慢,O(e^2) 其中e 是指数。这是一个大纲:

  1. 将尾数转换为十进制整数。您可以通过将位分开以直接读取尾数来做到这一点,或者您可以编写一个凌乱的浮点循环,首先将值乘以 2 的幂,使其在 1
  2. 通过反复乘以或除以 2 来应用指数。这是对您生成的十进制数字 字符串 的操作。每〜3次乘法将在左侧增加一个额外的数字。每一个除法都会在右边增加一个数字。

这真的是最好的吗?我对此表示怀疑,但我不是浮点专家,我找不到一种方法来对数字的浮点表示进行以 10 为底的计算,而不会遇到不精确结果的可能性(乘以或除以除了 2 的幂之外的任何东西都是浮点数的有损运算,除非你知道你有空闲位可以使用)。

【问题讨论】:

  • 最后,我只是用 base-1e9 替换了旧的 base-10 代码,并在随后的大多数迭代中重复乘法/除以 2 与 mult 乘 2^29 和 div 乘以 2^9尾部乘以 mult/div 乘以 2。生成的代码在相当短的时间内打印出最小的 80 位 long double,所以我很高兴。
  • Jon Skeet 有一个DoubleConverter class,可以打印精确的十进制表示。它是用 C# 编写的,但您可以将其转换为 C stackoverflow.com/questions/4732680/…

标签: c algorithm math floating-point


【解决方案1】:

你没有。最接近的方法是转储字节。

【讨论】:

  • 我已经考虑了更多,我认为我错了。由于以 10 为基数进入以 2 为基数,因此如果我们允许重复数字,则不应有任何只能以十进制表示的二进制值。因此,原则上您应该能够将 float/double 转换为(可能很长)十进制数字字符串。
  • 当然可以。正如我所描述的,我有一个实现,它在O(e^2) 时间(希望可以改进)和O(e) 空间(十进制表示必然需要)中完成。
  • 要完成回答,是的,您描述的算法看起来可以工作,但是任意精度库(如 Byron 推荐的)会使事情变得简单。对于一些相关但我认为不同的东西,还有:keithbriggs.info/xrc.html
  • 我怀疑将乘法转换为班次会加快速度,但这并不一定会改善大 O。
  • 我认为我刚才写的是错误的,因为我错过了十进制值发生翻倍的事实。也许处理这个问题的方法是将输出保持为像 BCD 这样的格式,直到你完成。
【解决方案2】:

我自己不是浮点专家,我会推迟使用经过良好测试的开源库。

GNU MPFR 不错。

MPFR 库是一个 C 库,用于 多精度浮点 正确舍入的计算。 MPFR 的主要目标是提供一个 多精度库 浮点计算是 既高效又具有明确的定义 语义。

【讨论】:

  • 而且它确实支持从双精度到任意十进制的转换。
【解决方案3】:

如果您想要更精确的结果,为什么不使用定点数学呢?转换很快。错误是已知的并且可以解决。不是对您问题的确切答案,而是对您的不同想法。

【讨论】:

  • 如果我在特定的应用程序中使用它不会是一个坏主意,但问题域是专门解决这个(相当痛苦的)浮点到精确十进制转换的问题。
【解决方案4】:

在我的脑海中,为什么不先把指数分解成二进制指数的总和,然后你的所有操作都是无损的。

10^2 = 2^6 + 2^5 + 2^2

然后求和:

mantissa<<6 + mantissa<<5 + mantissa<<2

我认为分解它会在 O(n) 的位数上,移位是 O(1),求和是 O(n) 位数...

当然,你必须有一个足够大的整数类来存储结果......

让我知道 - 我很好奇,这真的让我思考。 :-)

【讨论】:

  • 指数是一个二进制指数。并且绝对没有能够存储结果的整数类型(没有某种 bigint)。双精度可以超过 1000 位,长双精度可以超过 16000 位。 :-)
  • @r:我想你可以 calloc(1000) 然后在正确的位置进行位复制。但绝对凌乱。浮点数是有原因的。 :-)
  • 这仅适用于数字的整数部分,并且有更快更简单更好的方法......看看我对 log2(10) 的回答,这是相当恒定的......所以如果你想要十进制整数位数而不是 n(base10) = n(base2)/log2(10)。问题是这个问题都是关于不能分解为2的幂的小数部分......至少我不知道10^-n = 2^-a+2^-b+2^-c+.. . 唯一的方法是在给定精度内将其四舍五入到最接近的匹配
【解决方案5】:

虽然它是 C# 并且您的问题用 C 标记,但 Jon Skeet 有代码可以将 double 转换为字符串的精确表示:http://www.yoda.arachsys.com/csharp/DoubleConverter.cs

乍一看,移植到 C 似乎并不难,用 C++ 编写甚至更容易。

经过进一步思考,Jon 的算法似乎也是 O(e^2),因为它也在指数上循环。但是,这意味着算法是 O(log(n)^2) (其中 n 是浮点数),我不确定您是否可以比对数平方时间更好地从基数 2 转换为基数 10。

【讨论】:

  • 有趣。看起来他采用了 BCD 方法,或者接近它。
  • 就是他在问题中提到的方法。
  • @Kaestur:是的,但是代码显示了如何处理边缘情况,例如次正规。值得一看。
  • 如果您正在考虑理论上的 big-O(和 bignum 的东西),那么从基数 2 到基数 10 的转换可能无法在小于对数平方的时间内完成。但如果你的数字适合机器字,那就是记录时间,这要好得多。问题是您是否可以使用机器的浮点运算对浮点数做同样的事情。
  • 我的实现使用丑陋的循环(而不是位摆弄)来提取尾数,所以它并不关心浮点值是否一开始就低于正常值。 for (e=0; x&lt;1; x*=2, e--); 在几次迭代中使其进入正常范围。
【解决方案6】:

这个问题有官僚部分和算法部分。浮点数在内部存储为 (2e × m),其中 e 是指数(本身二进制)并且 m 是尾数。问题的官僚部分是如何访问这些数据,但 R. 似乎对问题的算法部分更感兴趣,即转换 (2e × m) 转换为十进制形式的分数 (a/b)。几种语言的官僚问题的答案是frexp(这是我今天之前不知道的一个有趣的细节)。

确实,乍一看,只写2就需要O(e2)的工作 e 十进制,还有更多时间用于尾数。但是,由于 Schönhage–Strassen 快速乘法算法的魔力,您可以在 Õ(e) 时间内完成,其中波浪号表示“最多对数因子” .如果您将 Schönhage-Strassen 视为魔术,那么不难想到该怎么做。如果 e 是偶数,则可以递归计算 2e/2,然后使用快速乘法对其进行平方。另一方面,如果 e 是奇数,您可以递归计算 2e−1 然后将其加倍。您必须仔细检查是否存在 Base 10 中的 Schönhage–Strassen 版本。虽然它没有被广泛记录,但可以在任何基础中完成。

将很长的尾数从二进制转换为以 10 为底的问题并不完全相同,但答案相似。你可以把尾数分成两半,m = a × 2k + b时间>。然后递归地将 ab 转换为以 10 为底,将 2k 转换为以 10 为底,再进行一次快速乘法以 10 为底计算 m

所有这一切背后的抽象结果是,您可以在 Õ(N) 时间内将整数从一个基数转换为另一个基数。

如果问题是关于标准 64 位浮点数的,那么它对于花哨的 Schönhage-Strassen 算法来说太小了。在这个范围内,您可以使用各种技巧来节省工作。一种方法是将 2e 的所有 2048 个值存储在查找表中,然后使用非对称乘法(在长乘法和短乘法之间)处理尾数。另一个技巧是以 10000 为底(或 10 的更高幂,取决于架构)而不是 10 为底。但是,正如 R. 在 cmets 中指出的那样,128 位浮点数已经允许足够大的指数调用质疑查找表和标准长乘法。实际上,长乘法最快到几位数,然后在显着的中等范围内可以使用Karatsuba multiplicationToom–Cook multiplication,然后在此之后,Schönhage–Strassen 的变体最好不仅仅是理论上但在实践中也是如此。

实际上,大整数包GMP 已经具有Õ(N) 次基数转换,以及选择乘法算法的良好启发式。他们的解决方案和我的解决方案之间的唯一区别是,他们不是以 10 为底进行任何大算术,而是以 2 为底计算 10 的大幂。在此解决方案中,他们还需要快速除法,但这可以从任何快速乘法中获得几种方法。

【讨论】:

  • 感谢您的链接和任何理论内容的第一个答案!看起来Toom-Cook 实际上可能是非天文指数的首选算法。
  • 非常有趣。你能解释一下使用 base 10000 是如何加快速度的吗?
  • Steven:使用 base 10000 可以加快速度,因为它比 base 10 快 4 倍,因为它们都适合机器字。
  • @Gabe,你确定吗? “64 位”浮点数涉及 ~1076 位(十进制)算术。 “80 位”浮点数涉及 ~16448 位算术。
  • 您正在考虑指数为正的情况。如果它是负数,每次你进一步递减指数时,你会在右边得到一个额外的小数位(保持'5'),但是需要几个指数递减才能清除左边的小数位(例如 5->2->1 -> 0)。我高估了,但您似乎需要大约 binary_exp*2/3 十进制数字,所以 IEEE 754 需要大约 700 位。
【解决方案7】:

在打印浮点数方面做了大量工作。黄金标准是打印出最小长度的十进制等值,这样当回读十进制等值时,无论回读期间的舍入模式是什么,您都会得到与开始时相同的浮点数。您可以在优秀的paper by Burger and Dybvig 中阅读有关该算法的信息。

【讨论】:

  • 这是一个经过充分研究的问题,在某些方面更简单,在某些方面更困难,但不管它是一个不同的问题。不过感谢您的链接。
  • @R:哎呀。我没能理解这个问题。也许举个例子会有所帮助。
【解决方案8】:

我看到你已经接受了一个答案,但这里有几个你可能想要查看的这种转换的开源实现:

  1. David Gay 在dtoa.c 中的dtoa() 函数:https://www.netlib.org/fp/dtoa.c

  2. Glibc 中/stdio-common/printf_fp.c 文件中的函数___printf_fp()(例如https://ftp.gnu.org/gnu/glibc/glibc-2.11.2.tar.gz)。

两者都会在%f-type printf 中打印您要求的尽可能多的数字,正如我在以下位置所写的那样:

【讨论】:

  • 很好的答案!这是我一直在寻找的类型。我会检查这些来源。
  • 你的博客很棒。我之前看过一些关于它的帖子,但不知道作者也在这里:)
  • ISTM 认为 David M. gay 的实现是事实上的(但不是官方的)标准实现。像这样的几种语言也根据他们的需要采用了它。我实际上是想让 Embarcadero 的 Delphi 和 C++Builder 人员也采用它。 ——哦等等,你是Exploring Binary的那个人?好工作!喜欢你的网站。
【解决方案9】:

sprintf 和类似的功能是 通常只指定一个数字 有效数字唯一 确定原始浮点数 价值;他们不一定打印 精确的十进制表示。

您可以要求比默认更多的有效数字:

printf("%.100g\n", 0.1);

打印0.1000000000000000055511151231257827021181583404541015625

【讨论】:

  • 您系统的 printf 碰巧做了礼貌(但没有任何标准规定)的事情,并根据要求计算尽可能多的数字。大多数只是在计算足够多的数字以唯一确定浮点数后将所有内容切掉。请参阅 Rick Regan 的答案中的链接。
  • 这适用于 gcc(gnu 编译器集合)和 tcc(tiny c 编译器)
  • @barlop 这是否有效取决于标准库(例如 glibc)的实现,而不是编译器。
  • @kikones34 虽然我认为特定的编译器使用标准库的特定实现。所以它确实依赖于编译器,因为编译器依赖于它使用的标准库的任何实现。
【解决方案10】:

有3种方式

  1. 打印binhex中的数字

    这是最精确的方法。我更喜欢hex,因为它更像是基础10,用于阅读/感觉就像F.8h = 15.5,这里没有精度损失。

  2. 打印dec,但只打印相关数字

    我的意思是只有数字中可以包含1 的数字尽可能接近。

    num of 整数简单而精确(没有精度损失):

    // n10 - base 10 integer digits
    // n2  - base 2 integer digits
    n10=log10(2^n2)
    n10=log2(2^n2)/log2(10)
    n10=n2/log2(10)
    n10=ceil(n2*0.30102999566398119521373889472449)
    // if fist digit is 0 and n10 > 1 then n10--
    

    num of 小数位数更棘手,根据经验我发现了这一点:

    // n10 - base 10 fract. digits
    // n2  - base 2 fract. digits >= 8
    n10=0; if (n02==8) n10=1;
    else if (n02==9) n10=2;
    else if (n02> 9)
        {
        n10=((n02-9)%10);
             if (n10>=6) n10=2;
        else if (n10>=1) n10=1;
        n10+=2+(((n02-9)/10)*3);
        }
    

    如果您创建一个依赖表n02 &lt;-&gt; n10,那么您会看到常量0.30102999566398119521373889472449 仍然存在,但从8 位开始,因为less 不能以令人满意的精度表示0.1 (0.85 - 1.15)。由于基数 2 的负指数,依赖性不是线性的,而是模式化的。此代码适用于少量位数 (&lt;=52),但在位数较大时可能会出现错误,因为使用的模式不完全适合 log10(2)1/log2(10)

    对于更大的位数,我使用这个:

    n10=7.810+(9.6366363636363636363636*((n02>>5)-1.0));
    

    但该公式是 32 位对齐的!!!还有更大的位计数广告错误

    P.S.进一步分析十进制数的二进制表示

    0.1
    0.01
    0.001
    0.0001
    ...
    

    可能会揭示确切的模式重复,这将导致任何位计数的相关数字的确切数量。

    为了清楚起见:

    8 bin digits -> 1 dec digits
    9 bin digits -> 2 dec digits
    10 bin digits -> 3 dec digits
    11 bin digits -> 3 dec digits
    12 bin digits -> 3 dec digits
    13 bin digits -> 3 dec digits
    14 bin digits -> 3 dec digits
    15 bin digits -> 4 dec digits
    16 bin digits -> 4 dec digits
    17 bin digits -> 4 dec digits
    18 bin digits -> 4 dec digits
    19 bin digits -> 5 dec digits
    20 bin digits -> 6 dec digits
    21 bin digits -> 6 dec digits
    22 bin digits -> 6 dec digits
    23 bin digits -> 6 dec digits
    24 bin digits -> 6 dec digits
    25 bin digits -> 7 dec digits
    26 bin digits -> 7 dec digits
    27 bin digits -> 7 dec digits
    28 bin digits -> 7 dec digits
    29 bin digits -> 8 dec digits
    30 bin digits -> 9 dec digits
    31 bin digits -> 9 dec digits
    32 bin digits -> 9 dec digits
    33 bin digits -> 9 dec digits
    34 bin digits -> 9 dec digits
    35 bin digits -> 10 dec digits
    36 bin digits -> 10 dec digits
    37 bin digits -> 10 dec digits
    38 bin digits -> 10 dec digits
    39 bin digits -> 11 dec digits
    40 bin digits -> 12 dec digits
    41 bin digits -> 12 dec digits
    42 bin digits -> 12 dec digits
    43 bin digits -> 12 dec digits
    44 bin digits -> 12 dec digits
    45 bin digits -> 13 dec digits
    46 bin digits -> 13 dec digits
    47 bin digits -> 13 dec digits
    48 bin digits -> 13 dec digits
    49 bin digits -> 14 dec digits
    50 bin digits -> 15 dec digits
    51 bin digits -> 15 dec digits
    52 bin digits -> 15 dec digits
    53 bin digits -> 15 dec digits
    54 bin digits -> 15 dec digits
    55 bin digits -> 16 dec digits
    56 bin digits -> 16 dec digits
    57 bin digits -> 16 dec digits
    58 bin digits -> 16 dec digits
    59 bin digits -> 17 dec digits
    60 bin digits -> 18 dec digits
    61 bin digits -> 18 dec digits
    62 bin digits -> 18 dec digits
    63 bin digits -> 18 dec digits
    64 bin digits -> 18 dec digits
    

    最后不要忘记将截断的数字四舍五入!!!这意味着如果最后一个相关数字之后的数字是&gt;=5 in dec,那么最后一个相关数字应该是+1 ...如果它已经是9,那么你必须转到上一个数字等等...

  3. 打印准确值

    要打印 小数二进制数 的精确值,只需打印小数 n 数字,其中 n 是小数位数,因为表示的值是这些值的总和,所以 小数点不能大于LSB的小数位num。上面的内容(项目符号 #2)与将十进制数存储到 float(或仅打印相关的小数)有关。

    两个精确值的负幂...

    2^- 1 = 0.5
    2^- 2 = 0.25
    2^- 3 = 0.125
    2^- 4 = 0.0625
    2^- 5 = 0.03125
    2^- 6 = 0.015625
    2^- 7 = 0.0078125
    2^- 8 = 0.00390625
    2^- 9 = 0.001953125
    2^-10 = 0.0009765625
    2^-11 = 0.00048828125
    2^-12 = 0.000244140625
    2^-13 = 0.0001220703125
    2^-14 = 0.00006103515625
    2^-15 = 0.000030517578125
    2^-16 = 0.0000152587890625
    2^-17 = 0.00000762939453125
    2^-18 = 0.000003814697265625
    2^-19 = 0.0000019073486328125
    2^-20 = 0.00000095367431640625
    

    现在10 的负幂以 64 位 doubles 的精确值样式打印:

    10^+ -1 = 0.1000000000000000055511151231257827021181583404541015625
            = 0.0001100110011001100110011001100110011001100110011001101b
    10^+ -2 = 0.01000000000000000020816681711721685132943093776702880859375
            = 0.00000010100011110101110000101000111101011100001010001111011b
    10^+ -3 = 0.001000000000000000020816681711721685132943093776702880859375
            = 0.000000000100000110001001001101110100101111000110101001111111b
    10^+ -4 = 0.000100000000000000004792173602385929598312941379845142364501953125
            = 0.000000000000011010001101101110001011101011000111000100001100101101b
    10^+ -5 = 0.000010000000000000000818030539140313095458623138256371021270751953125
            = 0.000000000000000010100111110001011010110001000111000110110100011110001b
    10^+ -6 = 0.000000999999999999999954748111825886258685613938723690807819366455078125
            = 0.000000000000000000010000110001101111011110100000101101011110110110001101b
    10^+ -7 = 0.0000000999999999999999954748111825886258685613938723690807819366455078125
            = 0.0000000000000000000000011010110101111111001010011010101111001010111101001b
    10^+ -8 = 0.000000010000000000000000209225608301284726753266340892878361046314239501953125
            = 0.000000000000000000000000001010101111001100011101110001000110000100011000011101b
    10^+ -9 = 0.0000000010000000000000000622815914577798564188970686927859787829220294952392578125
            = 0.0000000000000000000000000000010001001011100000101111101000001001101101011010010101b
    10^+-10 = 0.00000000010000000000000000364321973154977415791655470655996396089904010295867919921875
            = 0.00000000000000000000000000000000011011011111001101111111011001110101111011110110111011b
    10^+-11 = 0.00000000000999999999999999939496969281939810930172340963650867706746794283390045166015625
            = 0.00000000000000000000000000000000000010101111111010111111111100001011110010110010010010101b
    10^+-12 = 0.00000000000099999999999999997988664762925561536725284350612952266601496376097202301025390625
            = 0.00000000000000000000000000000000000000010001100101111001100110000001001011011110101000010001b
    10^+-13 = 0.00000000000010000000000000000303737455634003709136034716842278413651001756079494953155517578125
            = 0.00000000000000000000000000000000000000000001110000100101110000100110100001001001011101101000001b
    10^+-14 = 0.000000000000009999999999999999988193093545598986971343290729163921781719182035885751247406005859375
            = 0.000000000000000000000000000000000000000000000010110100001001001101110000110101000010010101110011011b
    10^+-15 = 0.00000000000000100000000000000007770539987666107923830718560119501514549256171449087560176849365234375
            = 0.00000000000000000000000000000000000000000000000001001000000011101011111001111011100111010101100001011b
    10^+-16 = 0.00000000000000009999999999999999790977867240346035618411149408467364363417573258630000054836273193359375
            = 0.00000000000000000000000000000000000000000000000000000111001101001010110010100101111101100010001001101111b
    10^+-17 = 0.0000000000000000100000000000000007154242405462192450852805618492324772617063644020163337700068950653076171875
            = 0.0000000000000000000000000000000000000000000000000000000010111000011101111010101000110010001101101010010010111b
    10^+-18 = 0.00000000000000000100000000000000007154242405462192450852805618492324772617063644020163337700068950653076171875
            = 0.00000000000000000000000000000000000000000000000000000000000100100111001001011101110100011101001001000011101011b
    10^+-19 = 0.000000000000000000099999999999999997524592683526013185572915905567688179926555402943222361500374972820281982421875
            = 0.000000000000000000000000000000000000000000000000000000000000000111011000001111001001010011111011011011010010101011b
    10^+-20 = 0.00000000000000000000999999999999999945153271454209571651729503702787392447107715776066783064379706047475337982177734375
            = 0.00000000000000000000000000000000000000000000000000000000000000000010111100111001010000100001100100100100100001000100011b
    

    现在 64 位 doubles 的相关十进制数字仅以样式打印 10 的负幂(我更习惯于此):

    10^+ -1 = 0.1
    10^+ -2 = 0.01
    10^+ -3 = 0.001
    10^+ -4 = 0.0001
    10^+ -5 = 0.00001
    10^+ -6 = 0.000001
    10^+ -7 = 0.0000001
    10^+ -8 = 0.00000001
    10^+ -9 = 0.000000001
    10^+-10 = 0.0000000001
    10^+-11 = 0.00000000001
    10^+-12 = 0.000000000001
    10^+-13 = 0.0000000000001
    10^+-14 = 0.00000000000001
    10^+-15 = 0.000000000000001
    10^+-16 = 0.0000000000000001
    10^+-17 = 0.00000000000000001
    10^+-18 = 0.000000000000000001
    10^+-19 = 0.0000000000000000001
    10^+-20 = 0.00000000000000000001
    

    希望对你有帮助:)

【讨论】:

  • 这个答案很有趣(所以请不要删除它,它可能对遇到稍微不同问题的人有帮助)但它没有回答这个问题。这个问题是关于打印确切的值,而不是打印足够多的数字来通过四舍五入恢复原始值。
  • 二进制小数不能转换为小数而没有精度损失(以有限位数计数),因此如果您想打印比点 1. 精确的值,仅相关(以 hex/bin 或任何形式打印数字碱基可按 2) 的幂分解。我在想你想打印以浮点形式存储的精确十进制值(以给定的尾数精度),而不是以十进制数形式存储在浮点中的精确浮点值。抱歉...第 1 点仍然回答了您的问题(您没有指定十进制系统),例如 1.6A09E667F3BCC908B2FB1366h 是十六进制的 sqrt(2)
  • 是的,他们可以。例如,0.01 的二进制小数是十进制的 0.25,而 0.001 的二进制小数是十进制的 0.125。一般来说,小数点右边的小数位数等于小数点右边的二进制位数。
  • 愚蠢的我......我又在想它了作为小数位数,请参阅我的答案...进行编辑
  • 顺便说一句,我忘了告诉:为了消除转换 bin 期间的精度损失 -> dec 我创建了十六进制字符串(简单的 shift+and 循环中的尾数),然后我将此十六进制字符串转换为 dec 字符串(然后重新格式化并打印)。我的转换代码在这里(不使用 bignums 或 FPU)stackoverflow.com/a/18231860/2521214
猜你喜欢
  • 2021-05-15
  • 2012-07-14
  • 2018-05-09
  • 2020-08-04
  • 1970-01-01
  • 1970-01-01
  • 2012-06-09
  • 1970-01-01
  • 2020-02-28
相关资源
最近更新 更多