是否有一种比使用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_MIN
和MY_MAX
我想要钳制的值相同(在上面的例子中加倍).
老问题,但我今天正在研究这个问题(有双打/花车).
最好的方法是使用SSE MINSS/MAXSS作为浮点数,使用SSE2 MINSD/MAXSD作为双打.它们是无分支的,每个都需要一个时钟周期,并且由于编译器内在函数而易于使用.与使用std :: min/max进行夹紧相比,它们可以使性能提高一个数量级以上.
你可能会发现这令人惊讶.我当然做到了!不幸的是,即使启用了/ arch:SSE2和/ FP:fast,VC++ 2010也会对std :: min/max使用简单的比较.我不能代表其他编译器.
这是在VC++中执行此操作的必要代码:
#includefloat 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++编译器不够聪明,无法剔除冗余移动.少说一指.:)
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),没有分支.优秀.
这是使用GCC 4.7和clang 3.2在Ubuntu 13.04上使用Core i3 M 350进行测试的.另外,调用std :: min和std :: max的简单C++代码生成了相同的程序集.
这是双打.对于int,GCC和clang都会生成具有五个指令(不计算ret)和没有分支的汇编.也很棒.
我目前不使用定点,所以我不会对定点发表意见.
如果你的处理器有一个绝对值的快速指令(就像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 }
三元运算符真的是要走的路,因为大多数编译器都能够将它们编译成使用条件移动而不是分支的本机硬件操作(因此避免了错误预测惩罚和管道气泡等).位操作可能会导致加载命中存储.
特别是,带有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中.
对于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);
IEEE 754浮点的位的排序方式是,如果比较解释为整数的位,则会得到相同的结果,就像将它们直接比较为浮点数一样.因此,如果您找到或知道一种钳位整数的方法,您也可以将它用于(IEEE 754)浮点数.对不起,我不知道更快的方式.
如果您将浮点数存储在数组中,您可以考虑使用某些CPU扩展,如SSE3,正如rkj所说.你可以看一下liboil它为你做的所有肮脏的工作.保持程序可移植性,并尽可能使用更快的cpu指令.(我不确定OS /编译器独立的liboil是怎样的).
我通常使用这种格式进行夹紧,而不是测试和分支:
clampedA = fmin(fmax(a,MY_MIN),MY_MAX);
虽然我从未对编译过的代码进行任何性能分析.