我希望以最有效的方式获取 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 为底的对数问题有关。
如果能提供此算法或任何更好的算法的帮助,我们将不胜感激。
我可能应该注意到,我想要无分支代码的原因是因为我希望它能够有效地矢量化。
来点简单的怎么样?
double fraction = whole - ((long)whole);
这只是从值本身中减去 double 的整数部分,余数应该是小数部分。 当然,这可能会存在一些代表性问题。
#include <math.h>
double fraction = fmod(d, 1.0);
最佳实现完全取决于目标架构。
在最新的 Intel 处理器上,这可以通过两条指令来实现:
roundsd
和 subsd
,但这不能用 portable C 代码来表达。
在某些处理器上,最快的方法是对浮点表示进行整数运算。 我想到了早期的 Atom 和许多 ARM CPU。
在其他一些处理器上,最快的方法是转换为整数并返回,然后进行减法、分支以保护大值。
如果要处理大量值,可以将舍入模式设置为舍入到零,然后将截断为整数的数字加减 +/-2^52,然后从原始值中减去得到分数。 如果您没有 SSE4.1,但有一个现代的 Intel CPU 并且想要矢量化,这通常是您能做的最好的事情。 然而,只有当您有很多值需要处理时,这才有意义,因为更改舍入模式有点昂贵。
在其他架构上,其他实现是最佳的。 一般来说,谈论C程序的“效率”是没有意义的;仅特定架构上特定实现的效率。
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
)实际上非常慢!
在 Microsoft Visual Studio 2015 中使用 C++ 进行的一些分析和实验表明,计算正数的最佳方法是:
double n;
// ...
double fractional_part = n - floor(n);
它比
modf
更快,而且,正如已经提到的,余数函数四舍五入到最接近的整数,因此没有用处。
标准库函数 modf 相当巧妙地解决了这个问题。
#include <math.h>
/*...*/
double somenumber;
double integralPart;
double fractionalPart = modf(somenumber, &integralPart);
这应该满足您的要求,便携且相当高效。
未记录的细节是第二个参数是否可以为 NULL,然后避免整数部分临时,这在您所描述的用途中是可取的。
不幸的是,许多实现不支持第二个参数为 NULL,因此无论您是否使用此值,都必须使用临时值。
你想要的是:
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 )