在C ++(至少假定C ++ 11)中,给定浮点值a,我需要找到满足以下约束的浮点值b:
换句话说,我需要将a的大小“截断”到最接近的2的幂,同时保持符号不变。
[*就我而言,“小于或等于”约束比较宽松,并且“小于”也可以使用。]
给出一些IEEE 754二进制表示形式,这是实现这一目标的一种方法 将只是通过位扑清除所有尾数位,而保持符号和指数位不变。
更便携的方法是:
logb
,ilogb
,甚至更便携的log2
或frexp
。pow(2.0,n)
,exp2(n)
或ldexp(1.0,n)
。copysign
复制标志。这允许使用许多可能的组合来解决任务,甚至在考虑单精度替代方案时,甚至更多。 在使用现代硬件上的性能和使用现代编译器方面,是否有人对这些方法有任何经验?
答案 0 :(得分:5)
使用frexp()
1 ,ldexp()
2 渲染a
并形成答案。
这两个功能几乎完全是所需的。
frexp
函数将浮点数分为归一化分数和2的整数次幂。...frexp
函数返回值x
, 使得x
的幅度介于[1/2,1)或零之间。
ldexp
函数将浮点数乘以2的整数次幂。
#include <math.h>
#include <stdio.h>
double round_pow2(double a) {
int exp;
double frac = frexp(a, &exp);
if (frac > 0.0) frac = 0.5;
else if (frac < 0.0) frac = -0.5;
double b = ldexp(frac, exp);
printf("% 20g % 25a % 25a", a, a, b);
printf(" %d", !!signbit(a) == !!signbit(b)); // b must have the same sign as a.
printf(" %d\n", !(fabs(b) > fabs(a))); // magnitude `b` must be <= magnitude `a`.
return b;
}
测试代码
void round_pow2_test(double x) {
round_pow2(nextafter(-x, -INFINITY));
round_pow2(-x);
round_pow2(nextafter(-x, INFINITY));
round_pow2(nextafter(x, -INFINITY));
round_pow2(x);
round_pow2(nextafter(x, INFINITY));
}
int main(void) {
round_pow2_test(0);
round_pow2_test(DBL_MIN);
round_pow2_test(1.0);
round_pow2_test(42.0);
round_pow2_test(DBL_MAX);
round_pow2(NAN);
return 0;
}
输出
-4.94066e-324 -0x1p-1074 -0x1p-1074 1 1
-0 -0x0p+0 -0x0p+0 1 1
4.94066e-324 0x1p-1074 0x1p-1074 1 1
-4.94066e-324 -0x1p-1074 -0x1p-1074 1 1
0 0x0p+0 0x0p+0 1 1
4.94066e-324 0x1p-1074 0x1p-1074 1 1
-2.22507e-308 -0x1.0000000000001p-1022 -0x1p-1022 1 1
-2.22507e-308 -0x1p-1022 -0x1p-1022 1 1
-2.22507e-308 -0x1.ffffffffffffep-1023 -0x1p-1023 1 1
2.22507e-308 0x1.ffffffffffffep-1023 0x1p-1023 1 1
2.22507e-308 0x1p-1022 0x1p-1022 1 1
2.22507e-308 0x1.0000000000001p-1022 0x1p-1022 1 1
-1 -0x1.0000000000001p+0 -0x1p+0 1 1
-1 -0x1p+0 -0x1p+0 1 1
-1 -0x1.fffffffffffffp-1 -0x1p-1 1 1
1 0x1.fffffffffffffp-1 0x1p-1 1 1
1 0x1p+0 0x1p+0 1 1
1 0x1.0000000000001p+0 0x1p+0 1 1
-42 -0x1.5000000000001p+5 -0x1p+5 1 1
-42 -0x1.5p+5 -0x1p+5 1 1
-42 -0x1.4ffffffffffffp+5 -0x1p+5 1 1
42 0x1.4ffffffffffffp+5 0x1p+5 1 1
42 0x1.5p+5 0x1p+5 1 1
42 0x1.5000000000001p+5 0x1p+5 1 1
-inf -inf -0x1p-1 1 1
-1.79769e+308 -0x1.fffffffffffffp+1023 -0x1p+1023 1 1
-1.79769e+308 -0x1.ffffffffffffep+1023 -0x1p+1023 1 1
1.79769e+308 0x1.ffffffffffffep+1023 0x1p+1023 1 1
1.79769e+308 0x1.fffffffffffffp+1023 0x1p+1023 1 1
inf inf 0x1p-1 1 1
nan nan nan 1 1
1 从OP的“获取幅度的以2为底的对数,使用例如... frexp取整。”
2 从OP的“使用例如... ldexp(1.0,n)将2提高到第n次幂。”
答案 1 :(得分:0)
到目前为止,根据我自己的测试,我得出以下结论(但由于我没有可利用的测试实验室,因此我的观察证据有限,并且陪审团仍在进行中):
是在单精度还是双精度域中执行操作都没有关系。实际上,即使需要额外的转换,大多数涉及的函数在其双精度化身中的执行速度似乎也会稍快。
应避免使用不带后缀f
(例如ilogb
)的单精度函数,因为它们的性能通常比后缀f
(例如ilogbf
)低)。
“位扑打”在性能方面无与伦比。令人惊讶的是,这在64位域中也表现更好(然后,我再次在64位计算机上进行测试)。我看到每次执行不到1 ns。相比之下,我的“测试平台”本身每次迭代的权重约为15 ns。
至于“ pow2(floor(log2))”方法的实现,这就是我到目前为止的结论:
我看不到基本构建基块的任何特殊组合,它们会因意外的协同效应而提高性能,因此考虑构建基块的类型似乎是合理的(“ pow2”,“ floor(log2)”和符号修正)。
假定0.0的大小写是无关紧要的,处理符号的最快方法是本质上执行“ pow2(floor(log2(abs)))”操作,然后使用简单的{{1 }},比if(a<0) b=-b;
快5 ns。如果“ pow2”构建块具有类似于尾数的因子(例如copysign
),则使用比较在正因子或负因子之间进行选择也是可行的选择,仅比操作后的条件条件稍慢修复。
到目前为止,“ pow2”操作(我正在使用的软件已经在两种实现中使用了很长时间了)最糟糕的选择是天真地使用ldexp
。可以想象,编译器可以更快地对其进行优化,而我的却不是。 pow(2.0,x)
快60 ns。 exp2
还要再快15 ns,使其成为最佳选择,估计为8-10 ns。
还有一个甚至更快的选项(我正在使用的软件中也使用过),即在整数域中使用位移,但这样做的代价是严格限制了功能起作用。如果要冒险走这条路,则应在ldexp
域中执行该操作,因为它仅比long long
域中要慢一些。这种方法还可以节省4-5 ns。
我能找到的最慢的“ floor(log2)”构建块(除了int
,我什至没有去测试)是(int)(log(x)/log(2))
及其亲属。 (int)log2(fabs(x))
大约快30 ns,估计为8到10 ns。
如果浮点类型使用以2为基数的表示形式,则frexp
是ilogb
的可行替代方案,还节省了1 ns。 frexp
比logb
慢一点(与ilogb
相当),我猜这是有道理的。
总而言之,到目前为止,以下实现似乎值得考虑:
frexp
是最快的实现方式(约1 ns),只要不考虑特殊值,已知浮点二进制格式(在这种情况下为IEEE binary64),并且可以使用相同大小和字节顺序的int类型; < / p>
double Pow2Trunc(double a)
{
union { double f; uint64_t i; } hack;
hack.f = a;
hack.i &= 0xFFF0000000000000u;
return hack.f;
}
是最快的完全可移植的实现方式(约16 ns);也许
double Pow2Trunc(double a)
{
int exp;
(void)frexp(a,&exp);
double b = ldexp(0.5, exp);
if (a < 0) b = -b;
return b;
}
便携式性稍差,但替代性也稍快(约15 ns)。
(特殊值的处理可能会得到改善;但是对于我的用例而言,它们不足以值得进一步检查。)
基于double Pow2Trunc(double a)
{
double b = ldexp(1.0, ilogb(a));
if (a < 0) b = -b;
return b;
}
提供替代方案似乎并不值得。如果提供了这些功能,则使用带有float
后缀的功能变体很重要。
显然,这些结果取决于硬件平台,编译器和设置(i7-5820K,适用于Linux的Windows 10子系统,g ++ 5.4.0,f
)。其他环境的里程可能会有所不同,对结果在质量上有所不同的案例的学习对我来说最有价值。