钳制实际(固定/浮点)值的最快方法?

时间:2009-01-09 09:13:12

标签: c numbers bit-manipulation clamp

与使用if语句或三元运算符相比,是否有更有效的方法来钳制实数? 我想为双打和32位修复点实现(16.16)做到这一点。我要求代码可以处理这两种情况;它们将在不同的功能中处理。

显然,我可以这样做:

double clampedA;
double a = calculate();
clampedA = a > MY_MAX ? MY_MAX : a;
clampedA = a < MY_MIN ? MY_MIN : a;

double a = calculate();
double clampedA = a;
if(clampedA > MY_MAX)
    clampedA = MY_MAX;
else if(clampedA < MY_MIN)
    clampedA = MY_MIN;

fixpoint版本将使用函数/宏进行比较。

这是在代码的性能关键部分完成的,所以我正在寻找一种尽可能有效的方法(我怀疑它会涉及位操作)

编辑:它必须是标准/便携式C,特定于平台的功能在这里没有任何兴趣。此外,MY_MINMY_MAX与我想要限制的值相同(在上面的示例中加倍)。

15 个答案:

答案 0 :(得分:37)

老问题,但我今天正在研究这个问题(使用双打/浮点数)。

最好的方法是使用SSE MINSS / MAXSS作为浮点数,使用SSE2 MINSD / MAXSD作为双打。它们是无分支的,每个都需要一个时钟周期,并且由于编译器内在函数而易于使用。与使用std :: min / max进行夹紧相比,它们的性能提高了一个数量级以上。

你可能会发现这令人惊讶。我当然做到了!不幸的是,即使启用了/ arch:SSE2和/ FP:fast,VC ++ 2010也会对std :: min / max使用简单的比较。我不能代表其他编译器。

这是在VC ++中执行此操作的必要代码:

#include <mmintrin.h>

float minss ( float a, float b )
{
    // Branchless SSE min.
    _mm_store_ss( &a, _mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float maxss ( float a, float b )
{
    // Branchless SSE max.
    _mm_store_ss( &a, _mm_max_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float clamp ( float val, float minval, float maxval )
{
    // Branchless SSE clamp.
    // return minss( maxss(val,minval), maxval );

    _mm_store_ss( &val, _mm_min_ss( _mm_max_ss(_mm_set_ss(val),_mm_set_ss(minval)), _mm_set_ss(maxval) ) );
    return val;
}

除了xxx_sd之外,双精度代码是相同的。

编辑:最初我将钳位功能写为注释。但是看看汇编程序输出,我注意到VC ++编译器不够聪明,无法剔除冗余移动。少说一指。 :)

答案 1 :(得分:36)

GCC和clang都可以为以下简单,直观,可移植的代码生成漂亮的程序集:

double clamp(double d, double min, double max) {
  const double t = d < min ? min : d;
  return t > max ? max : t;
}

> gcc -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

GCC生成的程序集:

maxsd   %xmm0, %xmm1    # d, min
movapd  %xmm2, %xmm0    # max, max
minsd   %xmm1, %xmm0    # min, max
ret

> clang -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

Clang生成的程序集:

maxsd   %xmm0, %xmm1
minsd   %xmm1, %xmm2
movaps  %xmm2, %xmm0
ret

三条指令(不包括ret),没有分支。优异。

在Ubuntu 13.04上使用Core i3 M 350测试了GCC 4.7和clang 3.2。 另外,调用std :: min和std :: max的简单C ++代码生成了相同的程序集。

这是双打。对于int,GCC和clang都会生成具有五个指令(不计算ret)和没有分支的汇编。也很棒。

我目前不使用定点,所以我不会就定点发表意见。

答案 2 :(得分:15)

如果你的处理器有一个绝对值的快速指令(就像x86那样),你可以做一个无分支的最小值和最大值,它比if语句或三元运算更快。

min(a,b) = (a + b - abs(a-b)) / 2
max(a,b) = (a + b + abs(a-b)) / 2

如果其中一个项为零(通常就是钳位时),代码会进一步简化:

max(a,0) = (a + abs(a)) / 2

当您将两个操作组合在一起时,您可以将两个/2替换为单个/4*0.25以保存步骤。

当使用FMIN = 0的优化时,以下代码比我的Athlon II X2上的三元快3倍。

double clamp(double value)
{
    double temp = value + FMAX - abs(value-FMAX);
#if FMIN == 0
    return (temp + abs(temp)) * 0.25;
#else
    return (temp + (2.0*FMIN) + abs(temp-(2.0*FMIN))) * 0.25;
#endif
}

答案 3 :(得分:14)

三元运算符真的是要走的路,因为大多数编译器都能够将它们编译成使用条件移动而不是分支的本机硬件操作(因此避免了错误预测惩罚和管道气泡等)。 比特操纵可能会导致负载点击商店

特别是,带有SSE2的PPC和x86有一个硬件操作,可以用这样的内在表示:

double fsel( double a, double b, double c ) {
  return a >= 0 ? b : c; 
}

优点是它在管道内执行此操作,而不会导致分支。实际上,如果您的编译器使用内在函数,您可以使用它直接实现您的钳位:

inline double clamp ( double a, double min, double max ) 
{
   a = fsel( a - min , a, min );
   return fsel( a - max, max, a );
}

我强烈建议你避免使用整数运算对双精度进行位操作。在大多数现代CPU上,没有直接的方法可以在double和int寄存器之间移动数据,而不是通过往返dcache。这将导致称为加载命中存储的数据危险,它基本上清空CPU管道,直到内存写入完成(通常大约40个周期左右)。

例外情况是,如果double值已经在内存中而不在寄存器中:在这种情况下,不存在load-hit-store的危险。但是,您的示例表明您刚刚计算了double并从函数返回它,这意味着它可能仍然在XMM1中。

答案 4 :(得分:8)

对于16.16的表示,简单的三元组不太可能在速度上更好。

而对于双打,因为你需要它标准/便携式C,任何类型的小提琴都将以糟糕的方式结束。

即使可能有点小提琴(我怀疑),你仍然依赖于双打的二进制表示。这个(及其大小)是与实施有关的。

可能你可以使用sizeof(double)“猜测”这个,然后将各种双值的布局与它们常见的二进制表示进行比较,但我认为你的隐藏是什么。

最好的规则是告诉编译器你想要什么(即三元),让它为你优化。

编辑:谦逊的馅饼时间。我刚刚测试了quinmars的想法(如下),它可以工作 - 如果你有IEEE-754浮点数。这使得下面的代码加速了大约20%。显然是不可移植的,但我认为可能有一种标准化的方式询问你的编译器是否使用带有#IF的IEEE754浮点格式??

  double FMIN = 3.13;
  double FMAX = 300.44;

  double FVAL[10] = {-100, 0.23, 1.24, 3.00, 3.5, 30.5, 50 ,100.22 ,200.22, 30000};
  uint64  Lfmin = *(uint64 *)&FMIN;
  uint64  Lfmax = *(uint64 *)&FMAX;

    DWORD start = GetTickCount();

    for (int j=0; j<10000000; ++j)
    {
        uint64 * pfvalue = (uint64 *)&FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < Lfmin) ? Lfmin : (*pfvalue > Lfmax) ? Lfmax : *pfvalue;
    }

    volatile DWORD hacktime = GetTickCount() - start;

    for (int j=0; j<10000000; ++j)
    {
        double * pfvalue = &FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < FMIN) ? FMIN : (*pfvalue > FMAX) ? FMAX : *pfvalue;
    }

    volatile DWORD normaltime = GetTickCount() - (start + hacktime);

答案 5 :(得分:7)

IEEE 754浮点的位的排序方式是,如果比较解释为整数的位,则会得到相同的结果,就好像将它们直接比较为浮点数一样。因此,如果您找到或知道一种钳位整数的方法,您也可以将它用于(IEEE 754)浮点数。对不起,我不知道更快的方式。

如果您将浮点数存储在数组中,您可以考虑使用某些CPU扩展,如SSE3,正如rkj所说。你可以看一下liboil它为你做的所有肮脏的工作。保持程序可移植性,并尽可能使用更快的cpu指令。 (我不确定OS /编译器独立的liboil是如何的。)

答案 6 :(得分:7)

我通常使用这种格式进行钳制:

,而不是测试和分支
clampedA = fmin(fmax(a,MY_MIN),MY_MAX);

虽然我从未对已编译的代码进行任何性能分析。

答案 7 :(得分:4)

实际上,没有像样的编译器会在if()语句和?:表达式之间产生差异。代码很简单,他们将能够发现可能的路径。也就是说,你的两个例子并不相同。使用?:的等效代码是

a = (a > MAX) ? MAX : ((a < MIN) ? MIN : a);

因为避免A&lt;当一个&gt; MIN测试MAX。现在这可能会产生影响,因为编译器必须发现两个测试之间的关系。

如果夹紧很少,您可以通过一次测试来测试夹紧的需要:

if (abs(a - (MAX+MIN)/2) > ((MAX-MIN)/2)) ...

E.g。当MIN = 6且MAX = 10时,这将首先向下移动8,然后检查它是否在-2和+2之间。这是否可以节省任何费用取决于分支的相对成本。

答案 8 :(得分:2)

这是一个类似于@Roddy's answer的可能更快的实现:

typedef int64_t i_t;
typedef double  f_t;

static inline
i_t i_tmin(i_t x, i_t y) {
  return (y + ((x - y) & -(x < y))); // min(x, y)
}

static inline
i_t i_tmax(i_t x, i_t y) {
  return (x - ((x - y) & -(x < y))); // max(x, y)
}

f_t clip_f_t(f_t f, f_t fmin, f_t fmax)
{
#ifndef TERNARY
  assert(sizeof(i_t) == sizeof(f_t));
  //assert(not (fmin < 0 and (f < 0 or is_negative_zero(f))));
  //XXX assume IEEE-754 compliant system (lexicographically ordered floats)
  //XXX break strict-aliasing rules
  const i_t imin = *(i_t*)&fmin;
  const i_t imax = *(i_t*)&fmax;
  const i_t i    = *(i_t*)&f;
  const i_t iclipped = i_tmin(imax, i_tmax(i, imin));

#ifndef INT_TERNARY
  return *(f_t *)&iclipped;
#else /* INT_TERNARY */
  return i < imin ? fmin : (i > imax ? fmax : f); 
#endif /* INT_TERNARY */

#else /* TERNARY */
  return fmin > f ? fmin : (fmax < f ? fmax : f);
#endif /* TERNARY */
}

请参阅Compute the minimum (min) or maximum (max) of two integers without branchingComparing floating point numbers

  

IEEE浮点数和双精度格式   设计的数字是这样的   “按字典顺序排列”,其中 -   用IEEE建筑师威廉的话说   Kahan的意思是“如果两个浮点数   订购相同格式的数字   (比如x&lt; y),然后他们被命令   他们的位是一样的   重新解释为Sign-Magnitude   整数“。

测试程序:

/** gcc -std=c99 -fno-strict-aliasing -O2 -lm -Wall *.c -o clip_double && clip_double */
#include <assert.h> 
#include <iso646.h>  // not, and
#include <math.h>    // isnan()
#include <stdbool.h> // bool
#include <stdint.h>  // int64_t
#include <stdio.h>

static 
bool is_negative_zero(f_t x) 
{
  return x == 0 and 1/x < 0;
}

static inline 
f_t range(f_t low, f_t f, f_t hi) 
{
  return fmax(low, fmin(f, hi));
}

static const f_t END = 0./0.;

#define TOSTR(f, fmin, fmax, ff) ((f) == (fmin) ? "min" :       \
                  ((f) == (fmax) ? "max" :      \
                   (is_negative_zero(ff) ? "-0.":   \
                    ((f) == (ff) ? "f" : #f))))

static int test(f_t p[], f_t fmin, f_t fmax, f_t (*fun)(f_t, f_t, f_t)) 
{
  assert(isnan(END));
  int failed_count = 0;
  for ( ; ; ++p) {
    const f_t clipped  = fun(*p, fmin, fmax), expected = range(fmin, *p, fmax);
    if(clipped != expected and not (isnan(clipped) and isnan(expected))) {
      failed_count++;
      fprintf(stderr, "error: got: %s, expected: %s\t(min=%g, max=%g, f=%g)\n", 
          TOSTR(clipped,  fmin, fmax, *p), 
          TOSTR(expected, fmin, fmax, *p), fmin, fmax, *p);
    }
    if (isnan(*p))
      break;
  }
  return failed_count;
}  

int main(void)
{
  int failed_count = 0;
  f_t arr[] = { -0., -1./0., 0., 1./0., 1., -1., 2, 
        2.1, -2.1, -0.1, END};
  f_t minmax[][2] = { -1, 1,  // min, max
               0, 2, };

  for (int i = 0; i < (sizeof(minmax) / sizeof(*minmax)); ++i) 
    failed_count += test(arr, minmax[i][0], minmax[i][1], clip_f_t);      

  return failed_count & 0xFF;
}

在控制台中:

$ gcc -std=c99 -fno-strict-aliasing -O2 -lm *.c -o clip_double && ./clip_double 

打印:

error: got: min, expected: -0.  (min=-1, max=1, f=0)
error: got: f, expected: min    (min=-1, max=1, f=-1.#INF)
error: got: f, expected: min    (min=-1, max=1, f=-2.1)
error: got: min, expected: f    (min=-1, max=1, f=-0.1)

答案 9 :(得分:1)

我自己尝试了SSE方法,并且装配输出看起来相当清洁,所以我一开始受到鼓励,但经过几千次定时后,它实际上相当慢了。它看起来确实看起来VC ++编译器不够聪明,无法知道你真正想要的是什么,并且它似乎在不应该在XMM寄存器和内存之间来回移动。也就是说,我不知道为什么编译器在使用三元运算符上的SSE最小/最大指令时不够聪明,因为它似乎无论如何都要使用SSE指令进行所有浮点计算。另一方面,如果您正在为PowerPC进行编译,则可以使用FP寄存器上的fsel本机,并且速度更快。

答案 10 :(得分:0)

如果我理解正确,您希望将值“a”限制在MY_MIN和MY_MAX之间的范围内。 “a”的类型是双重的。您没有指定MY_MIN或MY_MAX的类型。

简单的表达方式:

clampedA = (a > MY_MAX)? MY_MAX : (a < MY_MIN)? MY_MIN : a;

应该这样做。

我认为如果MY_MAX和MY_MIN恰好是整数,可能会进行一些小的优化:

int b = (int)a;
clampedA = (b > MY_MAX)? (double)MY_MAX : (b < MY_MIN)? (double)MY_MIN : a;

通过更改为整数比较,您可能会获得轻微的速度优势。

答案 11 :(得分:0)

我认为您可以使用SSE3或类似的技术,但不知道究竟是哪些命令/如何... 您可以查看:Saturation arithmetic

答案 12 :(得分:0)

如果你想使用快速绝对值指令,请查看我在minicomputer中找到的代码片段,它将浮点数钳位到范围[0,1]

clamped = 0.5*(fabs(x)-fabs(x-1.0f) + 1.0f);

(我简化了一下代码)。我们可以将其视为取两个值,一个反映为> 0

fabs(x)

和另一个反映约1.0为<1.0

1.0-fabs(x-1.0)

我们取其平均值。如果它在范围内,则两个值都将与x相同,因此它们的平均值将再次为x。如果它超出范围,则其中一个值将为x,另一个将在“边界”点上翻转x,因此它们的平均值将精确地为边界点。

答案 13 :(得分:0)

如上所述,fmin / fmax函数运行良好(在gcc中,使用-ffast-math)。尽管gfortran具有使用对应于max / min的IA指令的模式,但g ++没有。在icc中,必须使用std :: min / max,因为icc不允许短切fmin / fmax如何与非有限操作数一起使用。

答案 14 :(得分:0)

我在C ++中的2美分。可能与使用三元运算符没什么不同,希望不会生成分支代码

template <typename T>
inline T clamp(T val, T lo, T hi) {
    return std::max(lo, std::min(hi, val));
}