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

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

显然,我可以这样做:

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,平台特定的function在这里没有任何兴趣。 此外, MY_MINMY_MAX与我想要钳制的值相同(在上面的示例中加倍)。

对于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); 

老问题,但我今天正在研究这个问题(有双打/花车)。

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

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

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

 #include  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之外,双精度代码是相同的。

编辑:最初我写了钳位function评论。 但是看看汇编程序输出,我注意到VC ++编译器不够聪明,无法剔除冗余移动。 少说一指。 🙂

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生成的assembly:

 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)和没有分支的汇编。 也很棒。

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

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

 min(a,b) = (a + b - abs(ab)) / 2 max(a,b) = (a + b + abs(ab)) / 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 } 

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

特别是,带有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中。

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

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

我通常使用这种格式进行夹紧,而不是测试和分支:

 clampedA = fmin(fmax(a,MY_MIN),MY_MAX); 

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

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

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

因为当a> MAX时避免A

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

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

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

这是一个可能更快的实现类似于@Roddy的答案 :

 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 */ } 

请参阅计算没有分支的两个整数的最小值(最小值)或最大值(最大值)以及比较浮点数

IEEE浮点数和双精度格式的设计是为了使数字按照“按字典顺序排列”,用IEEE架构师William Kahan的话来说“如果两个相同格式的浮点数被排序(比如x

测试程序:

 /** gcc -std=c99 -fno-strict-aliasing -O2 -lm -Wall *.c -o clip_double && clip_double */ #include  #include  // not, and #include  // isnan() #include  // bool #include  // int64_t #include  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) 

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

如果我理解正确,您希望将值“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; 

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

我认为你可以使用SSE3或类似的技术,但不知道究竟是哪些命令/如何…你可以看看: 饱和算术

如果你想使用快速绝对值指令,请查看我在小型机中找到的这段代码,它将浮动夹紧到范围[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,因此它们的平均值将精确地为边界点。

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

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

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