在 C 中*有效*提取 double 的小数部分

问题描述 投票:0回答:7

我希望以最有效的方式获取 IEEE double 并删除它的任何整数部分。

我想要

1035 ->0
1045.23->0.23
253e-23=253e-23

我不关心如何正确处理非正规数、无穷大或 NaN。 我不介意有点麻烦,因为我知道我正在使用 IEEE 双打,所以它应该可以跨机器工作。

无分支代码将是首选。

我的第一个想法是(伪代码)

char exp=d.exponent;
(set the last bit of the exponent to 1)
d<<=exp*(exp>0);
(& mask the last 52 bits of d)
(shift d left until the last bit of the exponent is zero, decrementing exp each time)
d.exponent=exp;

但问题是,我想不出一种有效的方法来将 d 左移,直到指数的最后一位为零,而且如果没有设置所有最后一位,似乎需要输出零。 这似乎与以 2 为底的对数问题有关。

如果能提供此算法或任何更好的算法的帮助,我们将不胜感激。

我可能应该注意到,我想要无分支代码的原因是因为我希望它能够有效地矢量化。

c floating-point double micro-optimization bit-manipulation
7个回答
42
投票

来点简单的怎么样?

double fraction = whole - ((long)whole);

这只是从值本身中减去 double 的整数部分,余数应该是小数部分。 当然,这可能会存在一些代表性问题。


15
投票
#include <math.h>
double fraction = fmod(d, 1.0);

13
投票

最佳实现完全取决于目标架构。

在最新的 Intel 处理器上,这可以通过两条指令来实现:

roundsd
subsd
,但这不能用 portable C 代码来表达。

在某些处理器上,最快的方法是对浮点表示进行整数运算。 我想到了早期的 Atom 和许多 ARM CPU。

在其他一些处理器上,最快的方法是转换为整数并返回,然后进行减法、分支以保护大值。

如果要处理大量值,可以将舍入模式设置为舍入到零,然后将截断为整数的数字加减 +/-2^52,然后从原始值中减去得到分数。 如果您没有 SSE4.1,但有一个现代的 Intel CPU 并且想要矢量化,这通常是您能做的最好的事情。 然而,只有当您有很多值需要处理时,这才有意义,因为更改舍入模式有点昂贵。

在其他架构上,其他实现是最佳的。 一般来说,谈论C程序的“效率”是没有意义的;仅特定架构上特定实现的效率。


11
投票

提案

函数

remainder
计算 x-n*y,其中 n 是值 x / y,四舍五入到最接近的整数。

#include <math.h>

double fracpart(double input)
{
    return remainder(input, 1.);
}

这是最有效(且可移植)的方法,因为它不会计算不必要的值来完成这项工作(参见

modf
(long)
fmod
等)


基准

正如 Mattew 在评论中建议的那样,我编写了一些 基准代码 来将此解决方案与本页提供的所有其他解决方案进行比较。

请在下面找到 65536 次计算的时间测量(使用 Clang 编译并关闭优化):

method 1 took 0.002389 seconds (using remainder)
method 2 took 0.000193 seconds (casting to long)
method 3 took 0.000209 seconds (using floor)
method 4 took 0.000257 seconds (using modf)
method 5 took 0.010178 seconds (using fmod)

再次使用 Clang,这次使用

-O3
标志:

method 1 took 0.002222 seconds (using remainder)
method 2 took 0.000000 seconds (casting to long)
method 3 took 0.000000 seconds (using floor)
method 4 took 0.000223 seconds (using modf)
method 5 took 0.010131 seconds (using fmod)

事实证明,最简单的解决方案似乎在大多数平台上都能给出最佳结果,而执行该任务的具体方法(

fmod
modf
remainder
)实际上非常慢!


4
投票

在 Microsoft Visual Studio 2015 中使用 C++ 进行的一些分析和实验表明,计算正数的最佳方法是:

double n;
// ...
double fractional_part = n - floor(n);

它比

modf
更快,而且,正如已经提到的,余数函数四舍五入到最接近的整数,因此没有用处。


3
投票

标准库函数 modf 相当巧妙地解决了这个问题。

#include <math.h>
/*...*/
double somenumber;
double integralPart;
double fractionalPart = modf(somenumber, &integralPart);

这应该满足您的要求,便携且相当高效。

未记录的细节是第二个参数是否可以为 NULL,然后避免整数部分临时,这在您所描述的用途中是可取的。

不幸的是,许多实现不支持第二个参数为 NULL,因此无论您是否使用此值,都必须使用临时值。


2
投票

你想要的是:

inline double min(double d1, double d2) { return d2 < d1 ? d2 : d1; }
inline double max(double d1, double d2) { return d2 > d1 ? d2 : d1; }

double fract_fast(double d) {
  // Clamp d to values that are representable 64-bit integers.
  // Any double-precision floating-point number outside of this range
  // has no fractional part, so these calls to min / max will have
  // no effect on the return value of this function.
  d = min((double)(((int64_t)1)<<62), d);
  d = max(-(double)(((int64_t)1)<<62), d);

  // C / C++ define casts to integer as always being the round-toward-zero
  // of the floating point number, regardless of rounding mode.
    int64_t integral_part = (int64_t)d;
    return d - (double)integral_part;
}

x86 具有用于浮点的最小/最大指令,以及用于截断为有符号整数以及从整数转换回浮点的单个指令。 因此,上面的例程可以用 5 条指令来实现:min、max、convert-to-int、convert-from-int 和 minus。 事实上,至少有一个编译器 (clang) 提供了 5 条指令的实现,如您在 https://godbolt.org/z/7oqbPKMhc

中看到的那样

(或者查看 C++ 版本 https://godbolt.org/z/6sqGxzYsq

© www.soinside.com 2019 - 2024. All rights reserved.