最快的方法来钳制一个真正的(固定/浮点)值?

有没有比使用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; 

固定点版本将使用函数/macros进行比较。

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

编辑:它必须是标准/便携式C,特定于平台的function在这里没有任何利益。 而且, MY_MINMY_MAX与我想要的值相同(在上面的例子中双打)。

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

而对于双打,因为你需要它的标准/便携式的C,任何types的小提琴将严重结束。

即使一个小提琴是可能的(我怀疑),你会依靠双打的二进制表示。 这个(和他们的大小)是实施相关的。

可能你可以使用sizeof(double)“猜测”这个,然后比较各种double值的布局和普通的二进制表示法,但是我认为你只能隐藏起来。

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

编辑:谦逊的馅饼时间。 我刚刚testing了quinmars的想法(如下),它工作 – 如果你有IEEE-754浮点数。 下面的代码加快了20%左右。 IO显然是不可移植的,但我认为可能有一个标准的方式来问你的编译器,如果它使用带有#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 <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,双精度代码是相同的。

编辑:最初我写钳位function评论。 但是看着汇编程序的输出,我注意到VC ++编译器不够聪明,无法清除多余的移动。 less一条指令。 🙂

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

铿锵生成的程序集:

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

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

在Ubuntu 13.04上用Core i3 M 350对GCC 4.7和clang 3.2进行了testing。另外,直接调用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 } 

三元运算符是真的要走的路,因为大多数编译器能够将它们编译成本地硬件操作,使用条件移动而不是分支(从而避免误预处理和pipe道气泡等)。 位操作很可能会导致加载存储

具体来说,PPC和x86与SSE2有一个硬件操作可以表示为一个内在的东西是这样的:

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

优点是它在pipe道内完成,而不会引起分支。 事实上,如果你的编译器使用内在的,你可以直接使用它来实现你的钳制:

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

我强烈build议你避免使用整数运算对双精度进行位操纵 。 在大多数现代的CPU上,除了通过往返于dcache之外,没有直接的方式在double和int寄存器之间移动数据。 这将导致一个称为load-hit-store的数据危险,基本上清空CPUstream水线,直到内存写入完成(通常约40个周期左右)。

这是一个例外,如果双值已经在内存中,而不是在一个寄存器中:在这种情况下,没有负载存储的危险。 然而,你的例子表明你刚刚计算了double,并从函数返回,这意味着它可能仍然在XMM1中。

IEEE 754浮点的位的排列方式是,如果比较解释为整数的位,则会得到与直接将其作为浮点数进行比较的结果相同的结果。 所以,如果你发现或者知道一种钳制整数的方法,你也可以用它来用于(IEEE 754)浮点数。 对不起,我不知道更快的方法。

如果你的浮点数存储在一个数组中,你可以考虑使用一些像SSE3这样的CPU扩展,正如rkj所说的那样。 你可以看看liboil,它为你做所有的肮脏的工作。 保持你的程序的可移植性,并尽可能使用更快的CPU指令。 (我不知道如何操作系统/编译器无关的liboil)。

我通常使用这种格式进行钳位,而不是testing和分支:

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

尽pipe我从来没有对编译过的代码做过任何性能分析。

实际上,没有象样的编译器会在if()语句和?:expression式之间做出区别。 代码很简单,他们将能够find可能的path。 也就是说,你们两个例子并不完全相同。 使用?:的等效代码是

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

因为那样当a> MAX时避免A <MINtesting。 现在这可能会有所作为,因为编译器要另外发现两个testing之间的关系。

如果夹紧是罕见的,您可以testing需要夹紧一个单一的testing:

 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浮点数和双精度格式的devise使数字是“字典顺序排列”,用IEEE架构师威廉·卡汉(William Kahan)的话来说,意思是“如果相同格式的两个浮点数是有序的(比如x <y),那么当它们的位被重新解释为符号量级整数时,它们以相同的方式sorting。

testing程序:

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

我自己尝试了SSE方法,assembly输出看起来相当清洁一些,所以起初我很鼓舞,但是经过数千次的计时后,实际上是比较慢的。 它确实看起来像VC ++编译器不够聪明,知道你真正想要的是什么,它似乎在XMM寄存器和内存之间来回移动,当它不应该。 也就是说,我不知道为什么编译器在三态运算符上使用SSE最小/最大值指令时,似乎对所有浮点计算都使用SSE指令,所以不够聪明。 另一方面,如果您正在编译PowerPC,则可以在FP寄存器上使用fsel内部函数,速度更快。

如果我理解正确,你想限制一个值“a”到MY_MIN和MY_MAX之间的范围。 “a”的types是双倍的。 您没有指定MY_MIN或MY_MAX的types。

简单的expression:

 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或类似的技术,但不知道究竟哪些命令/如何…你可以看看: 饱和算术

如果要使用快速绝对值指令,请查看我在小型机中find的代码,该代码将浮点数限制在[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)。 尽pipegfortran有使用对应于max / min的IA指令的模式,但是g ++没有。 在icc中,必须使用std :: min / max,因为icc不允许简化fmin / fmax如何处理非有限操作数的规范。

我在C ++ 2美分。 也许和使用三元运算符没有什么不同,希望没有生成分支代码

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