在C ++中将float截断为最接近的2的幂-性能

时间:2019-02-09 22:45:03

标签: c++ performance floating-point

在C ++(至少假定C ++ 11)中,给定浮点值a,我需要找到满足以下约束的浮点值b:

  • b必须与a具有相同的符号。
  • b的大小必须小于或等于[*] a的大小。
  • b的大小必须是2的幂。
  • 在这些限制条件下,b的大小必须尽可能大。

换句话说,我需要将a的大小“截断”到最接近的2的幂,同时保持符号不变。

[*就我而言,“小于或等于”约束比较宽松,并且“小于”也可以使用。]

给出一些IEEE 754二进制表示形式,这是实现这一目标的一种方法 将只是通过位扑清除所有尾数位,而保持符号和指数位不变。

更便携的方法是:

  1. 使用例如,获得幅度的以2为底的对数,向下舍入。 logbilogb,甚至更便携的log2frexp
  2. 使用例如将2提高到第n次方整数位移位(请注意负幂和值范围问题),pow(2.0,n)exp2(n)ldexp(1.0,n)
  3. 通过copysign复制标志。

这允许使用许多可能的组合来解决任务,甚至在考虑单精度替代方案时,甚至更多。 在使用现代硬件上的性能和使用现代编译器方面,是否有人对这些方法有任何经验?

2 个答案:

答案 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为基数的表示形式,则frexpilogb的可行替代方案,还节省了1 ns。 frexplogb慢一点(与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)。其他环境的里程可能会有所不同,对结果在质量上有所不同的案例的学习对我来说最有价值