【问题标题】:Truncate a floating-point number to the leading N decimal digits将浮点数截断为前 N 个十进制数字
【发布时间】:2016-11-15 20:20:13
【问题描述】:

这是获取浮点数(数字 >= 0.0)最左边的 n 个非零数字的最佳方法。

例如,

如果 n = 1:

  • 0.014568 -> 0.01
  • 0.246456 -> 0.2

如果 n = 2:

  • 0.014568 -> 0.014
  • 0.246456 -> 0.24

@schil227 评论后: 目前,我正在根据需要进行乘法和除法(除以 10),以便在十进制数字字段中有 n 个数字。

【问题讨论】:

  • “Optimal”已经是最高级的了。 “最佳”没有意义。这就像“C++++”。
  • 抛出一个基本的想法:将浮点数乘以 10,直到你在那个位置找到一个非零数字(比如 x 次),此时你知道你需要 x+n小数点,然后您可以将浮点数截断(注意不要超出范围)。
  • 我想你可以检查你的数字是否大于其他小数,而不是乘法;例如if(f > 0.1){ return 0;}else if (f > 0.01){ return 1;} ... 不确定效率会提高多少,或多或少会遍历小数点。
  • @Bruno Guardia 我更喜欢*基于 IEEE 754-2008 的答案
  • “最佳方式”和“正确”在这里可能会发生冲突。非常准确的答案很难,因为代码会有效地重复很多 sprintf() 代码。更快,不太准确的代码很容易或超过限制范围。我假设代码首先需要在所有 FP 范围内准确/正确,然后快速。因此,获得更快代码的唯一真正方法是指定您可以忍受的限制:number 的范围、n 的范围、结果的精度。到目前为止,发布的唯一限制是 number >= 0.0 和 IEEE 754-2008,但甚至没有指定 binary FP。

标签: c++ c performance floating-point ieee-754


【解决方案1】:

代码可以使用sprintf(buf, "%e",...) 来完成大部分繁重的工作。

有很多极端情况,其他直接代码可能会失败,sprintf() 可能至少是一个很好的可靠参考解决方案。

此代码打印doubleDBL_DECIMAL_DIG 的位置,以确保数字中没有四舍五入会产生影响。 然后它将根据n清零各种数字。

请参阅@Mark Dickinson comment,了解使用大于DBL_DECIMAL_DIG 的值的原因。也许在DBL_DECIMAL_DIG*2 的顺序上。如上所述,有很多极端情况。

#include <float.h>
#include <math.h>
#include <stdio.h>

double foo(double x, int n) {
  if (!isfinite(x)) {
    return x;
  }
  printf("%g\n", x);
  char buf[DBL_DECIMAL_DIG + 11];
  sprintf(buf, "%+.*e", DBL_DECIMAL_DIG, x);
  //puts(buf);
  assert(n >= 1 && n <= DBL_DECIMAL_DIG + 1);
  memset(buf + 2 + n, '0', DBL_DECIMAL_DIG - n + 1);
  //puts(buf);
  char *endptr;
  x = strtod(buf, &endptr);
  printf("%g\n", x);
  return x;
}

int main() {
 foo(0.014568, 1);
 foo(0.246456, 1);
 foo(0.014568, 2);
 foo(0.246456, 2);
 return 0;
}

输出

0.014568
0.01
0.246456
0.2
0.014568
0.014
0.246456
0.24

这个答案假设 OP 不想要一个四舍五入的答案。回复:0.246456 -&gt; 0.24

【讨论】:

  • 所以即使这样也不会涵盖所有极端情况,对吧?例如,如果x = 0x1.1079848152a7ap-1(精确的十进制值0.5321771056999999860437355891917832195758819580078125),则该点后的前十位数字为0.5321771056,但格式化为该点后的15位然后截断将给出0.5321771057
  • @Mark Dickinson 在您的示例中,为什么您“格式化为 15”会导致其自身的舍入问题。此代码不使用15 来形成结果。最好至少使用DBL_DECIMAL_DIG 总有效数字。无法复制您的发现。请使用"%a" 报告返回值。当我运行foo(0x1.1079848152a7ap-1, 10) 时,返回值为0x1.1079848076c0ap-10.5321_7710_5599_9999_7777..。或 0.5321_7710_56 格式化为 10 位有效数字。嗯,也许您的代码使用了DBL_DIG 而不是DBL_DECIMAL_DIG
  • 啊,对不起,是的。但是DBL_DECIMAL_DIG 也存在问题,只是没有那个特定的例子。 sprintf 呼叫轮次,并且该轮次可能最终将一串 9 向上舍入。我很快就会寻找例子。
  • 更新示例:使用x = 0x1.e529dcaae1d0ap-1(十进制值0.9475850065799999999427427610498853027820587158203125),假设DBL_DECIMAL_DIG 为17,这对于IEEE 754 系统来说是相当典型的,sprintf 结果为"'9.47585006580000000e-01'" ,因此对该点之后的前 11 位数字的请求将得到 0.94758500658 而不是正确的 0.94758500657。不过,我不确定 OP 是否真的关心这种极端情况。
  • @MarkDickinson 同意,DBL_DECIMAL_DIG 的精度在您的comment's case 中不足。然后这个问题变成了在二进制数的十进制化中可能存在的9s 的最大数量。我怀疑它是DBL_DIG,所以sprintf("%.*e") 方法可能需要DBL_DECIMAL_DIG + DBL_DIG 有效数字。然而,一旦代码尝试超过DBL_DECIMAL_DIG 位数,sprintf() 的质量就值得怀疑。 C 没有规范通过DBL_DECIMAL_DIG。你的想法?
【解决方案2】:

如果您希望将结果作为字符串,您可能应该以更高的精度打印到字符串,然后自己将其切掉。 (有关 IEEE 64 位 double 需要多少额外精度的详细信息,请参阅 @chux 的答案以避免从 9 的字符串向上取整,因为您想要截断,但所有常用的字符串函数都四舍五入到最近。)

如果你想要double 结果,那么你确定你真的想要这个吗?在计算中间的早期舍入/截断通常只会降低最终结果的准确性。当然,floor/ceil、trunc 和 nearint 在实际算法中也有使用,这只是 trunc 的缩放版本。


如果你只想要一个double,你可以得到相当好的结果,而无需使用字符串。 使用ndigitsfloor(log10(fabs(x))) 计算比例因子,然后将缩放后的值截断为整数,然后再按比例缩小

经过测试和工作(有和没有-ffast-math)。请参阅Godbolt compiler explorer 上的 asm。这可能会相当有效地运行,尤其是使用-ffast-math -msse4.1(因此 floor 和 trunc 可以内联到 roundsd)。

如果您关心速度,请考虑将pow() 替换为利用指数是一个小整数这一事实的东西。我不确定在这种情况下库 pow() 实现的速度有多快。 GNU C __builtin_powi(x, n) trades accuracy for speed, for integer exponents, doing a multiplication tree, which is less accurate than what pow() does.

#include <float.h>
#include <math.h>
#include <stdio.h>

double truncate_n_digits(double x, int digits)
{
    if (x==0 || !isfinite(x))
        return x;   // good idea stolen from Chux's answer :)

    double l10 = log10(fabs(x));
    double scale = pow(10.,  floor(l10) + (1 - digits));  // floor rounds towards -Inf
    double scaled = x / scale;
    double scaletrunc = trunc(scaled);  // trunc rounds towards zero
    double truncated = scaletrunc * scale;

#if 1    // debugging code
    printf("%2d %24.14g =>\t%24.14g\t scale=%g, scaled=%.30g\n", digits, x, truncated, scale, scaled);
    // print with more accuracy to reveal the real behaviour
    printf("   %24.20g =>\t%24.20g\n", x, truncated);
#endif

    return truncated;
}

测试用例:

int main() {
 truncate_n_digits(0.014568, 1);
 truncate_n_digits(0.246456, 1);
 truncate_n_digits(0.014568, 2);
 truncate_n_digits(-0.246456, 2);
 truncate_n_digits(1234567, 2);
 truncate_n_digits(99999999999, 6);
 truncate_n_digits(-99999999999, 6);
 truncate_n_digits(99999, 10);
 truncate_n_digits(-0.0000000001234567, 3);
 truncate_n_digits(1000, 6);
 truncate_n_digits(0.001, 6);
 truncate_n_digits(1e-312, 2);  // denormal, and not exactly representable: 9.999...e-313
 truncate_n_digits(nextafter(1e-312, INFINITY), 2);  // denormal, just above 1.00000e-312
 return 0;
}

每个结果显示两次:首先只有%.14g,所以四舍五入给出了我们想要的字符串,然后再次使用%.20g 显示足够多的地方来揭示浮点数学的现实。大多数数字都不能精确表示,因此即使完美舍入也不可能返回 double 完全 表示截断的十进制字符串。 (大约尾数大小的整数可以精确表示,分母是 2 的幂的分数也是如此。)

 1                 0.014568 =>                      0.01         scale=0.01, scaled=1.45679999999999987281285029894
    0.014567999999999999353 =>   0.010000000000000000208
 1                 0.246456 =>                       0.2         scale=0.1, scaled=2.46456000000000008398615136684
      0.2464560000000000084 =>     0.2000000000000000111
 2                 0.014568 =>                     0.014         scale=0.001, scaled=14.5679999999999996163069226895
    0.014567999999999999353 =>   0.014000000000000000291
 2                -0.246456 =>                     -0.24         scale=0.01, scaled=-24.6456000000000017280399333686
     -0.2464560000000000084 =>   -0.23999999999999999112
 3               1234.56789 =>                      1230         scale=10, scaled=123.456789000000000555701262783
       1234.567890000000034 =>                      1230
 6               1234.56789 =>                   1234.56         scale=0.01, scaled=123456.789000000004307366907597
       1234.567890000000034 =>     1234.5599999999999454
 6              99999999999 =>               99999900000         scale=100000, scaled=999999.999990000040270388126373
                99999999999 =>               99999900000
 6             -99999999999 =>              -99999900000         scale=100000, scaled=-999999.999990000040270388126373
               -99999999999 =>              -99999900000
10                    99999 =>                     99999         scale=1e-05, scaled=9999900000
                      99999 =>     99999.000000000014552
 3            -1.234567e-10 =>                 -1.23e-10         scale=1e-12, scaled=-123.456699999999983674570103176
   -1.234566999999999879e-10 => -1.2299999999999998884e-10
 6                     1000 =>                      1000         scale=0.01, scaled=100000
                       1000 =>                      1000
 6                    0.001 =>                     0.001         scale=1e-08, scaled=100000
   0.0010000000000000000208 =>  0.0010000000000000000208
 2     9.9999999999847e-313 =>      9.9999999996388e-313         scale=1e-314, scaled=100.000000003458453079474566039
   9.9999999999846534143e-313 =>        9.9999999996388074622e-313
 2     1.0000000000034e-312 =>      9.0000000001196e-313         scale=1e-313, scaled=9.9999999999011865980946822674
   1.0000000000034059979e-312 =>        9.0000000001195857973e-31

由于您想要的结果通常无法精确表示,(并且由于其他舍入误差)生成的 double 有时会低于您想要的结果,因此以全精度打印它可能会得到 1.19999999 而不是 1.20000011。您可能希望使用 nextafter(result, copysign(INFINITY, original)) 来获得比您想要的更可能具有更高量级的结果。

当然,在某些情况下,这只会让事情变得更糟。但由于我们向零截断,大多数情况下我们得到的结果刚好低于(在数量上)无法表示的精确值。

【讨论】:

  • 很好,也许有一天我会针对这两种方法运行一个随机的double 生成器,以找出解决方案中的差异和优势/劣势。真正漂亮的是有> 1种独立的方式来互相检查。 BTW:建议添加零测试if (x == 0 || !isfinite(x)) return x;
猜你喜欢
  • 1970-01-01
  • 2017-06-20
  • 1970-01-01
  • 2014-03-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-07
  • 2013-01-02
  • 2014-11-24
相关资源
最近更新 更多