优化分支代码——避免跳转指令堵塞流水线

File:      noifop.txt
Name:      优化分支代码——避免跳转指令堵塞流水线
Author:    zyl910
Blog:      http://blog.csdn.net/zyl910/
Version:   V2.00
Updata:    2006-10-11

(注意修改下载后的扩展名)


一、起因——饱和处理

  在编写图象处理程序时,经常出现RGB值超过[0, 255]范围的情况。这时,得做饱和处理,将越界数值饱和到边界,即这样的代码:
if (r <   0) r =   0;
if (r > 255) r = 255;
if (g <   0) g =   0;
if (g > 255) g = 255;
if (b <   0) b =   0;
if (b > 255) b = 255;


  但这样的代码执行效率是很低的。这是因为if区块会编译成跳转指令,而跳转指令对于严重影响现代超流水线CPU的流水线效率。
  这时CPU产商提出了两个解决方案:一是增加了分支预测硬件,尽量减少跳转指令对流水线的影响;二是设计了MMX/SSE等SIMD指令集,它们天生就有饱和运算指令,而且还可以并行计算。
   分支预测对于循环语句所编译的跳转效果较好,因为在循环时一定会执行跳转,只有最后一次循环结束时才会预测失败。而对于做饱和处理效果就不怎么好了,这 是因为每次循环计算出的RGB值都是不相同的(因为本来就不是同一个像素),预测失败的可能性很大。所以分支预测对饱和处理做的贡献微乎其微。
   而使用SIMD指令呢。那是一种非常完美的解决方案,在有条件的情况下极力推荐SIMD指令。但由于高级语言无法描述SIMD指令,所以只能手工用汇编 编写SIMD代码,这无疑给理解代码带来困难。再者,我们有时需要在像Java、.Net这样的虚拟机平台下编写图象处理程序,而此时是无法直接使用 SIMD指令集的。
  所以,我们需要一种在常规指令集下(能用高级语言表述)做饱和处理的算法,且能避开if跳转。


  先尝试小于零时的饱和处理算法。

  还记得“与”运算有什么作用吗?当一个整数与掩码经行与运算时,全1掩码会保留原值,全0掩码会返回零。
  然后想想如何生成所需的掩码。C语言比较运算的的结果是0和1,怎样将它们变成全0或全1的掩码呢?答案很简单,求负 或 减一。由于求负写法简洁,所以喜欢用求负:
n &= -(n >= 0)

  首先将n与0进行比较。当 n>=0 时,比较结果为1;当 n<0 时,比较结果为0。
  然后求负。当 n>=0 时,结果为-1(全1);当 n<0 时,结果为全0。
  再将原数与上面求得的掩码进行“与”运算。

  有了上面那个算法启发思维后,我们很容易想出处理>255情况的算法:
n = (n | -(n >= 256) ) & 0xFF

  首先将n与256进行比较。当 n<256 时,比较结果为0;当 n>=256 时,比较结果为1。
  然后求负。当 n>=256 时,结果为-1(全1);当 n<256 时,结果为0。
  再将原数与上面求得的掩码进行“或”运算。
  最后跟0xFF进行与运算,使全1变成0xFF(十进制的255)。至于 n<256 的,本来就在[0, 255]范围内,结果不变。

  还可不可以再优化呢?由于一个数不可能同时小于0和大于255,所以可以将着两行代码合并成一行。由于我们一般是将结果保存到一个BYTE型变量中,进行一次强制类型转换就行了,不需要“& 0xFF”。最后,每次写那么长代码太麻烦了,应该将它封装成宏:
#define LIMITSU_FAST(n, bits) ( (n) & -((n) >= 0) | -((n) >= (1<<(bits))) )
#define LIMITSU_SAFE(n, bits) ( (LIMITSU_FAST(n, bits)) & ((1<<(bits)) - 1) )

  bits代表限制多少位,如BYTE就是8。如果觉得参数过多,可以再定义宏,或定义内联函数:
#define LIMITSU_BYTE(n) ((BYTE)(LIMITSU_FAST(n, 8)))

  现在分析一下运算复杂度:
    if办法需要2次比较和2次跳转;
    本办法需要2次比较(、2次将比较结果转为数值)、2次求负、1次与运算、1次或运算。

  可以看出,本办法在减少2次跳转的情况下,增加了共6次的位运算。幸好位运算都是简单指令,都是1个时钟周期内就能完成的指令。所以在现代超流水线CPU情况下,这6次位运算比if跳转开销少。


  上面讨论的只是像C语言那样“比较运算结果是0或1”情况下的算法,如果是像BASIC那样“比较运算结果是0或-1”怎么办呢?其实很简单,我们需要的正是0和-1,BASIC也支持AND、OR、XOR等位运算符。代码就这样写:
by =  ((n And (n >= 0) Or (n >= 256)) And &HFF)


  测试:略。请看old目录下的V100.rar

 

二、推广

  该方法不仅合适做饱和处理,还可以推广到其他方面去。

2.1 条件掩码

#define IFMASKNUM(n, c) ( (n) & -(c) )

参数:
  n: 掩码
  c: 条件。为0或1

返回值: 若c为1,则返回值是n;若c为0,则返回值为0。

 

2.2 MIN与MAX

#define FASTMIN(a, b) ( (a) + ( ((b)-(a)) & -((b)<(a)) ) )
#define FASTMAX(a, b) ( (a) + ( ((b)-(a)) & -((b)>(a)) ) )


解释:
  将b与a进行比较时实际上会执行减法操作,而且前面有“b-a”,这会被编译器优化的。
  注意该方法会产生溢出,所以只有像C语言这样不会抛出整数溢出异常的语言才行。

 

2.3 大小写转换

#define CHARUCASE(c) ( (c) ^ (0x20 & -((c)>='a' && (c)<='z') ) )
#define CHARLCASE(c) ( (c) ^ (0x20 & -((c)>='A' && (c)<='Z') ) )


解释:
'A'的ASCII码是0x41
'a'的ASCII码是0x61
它们相差了0x20,就是D5不同。
所以我在条件满足的时候将该位取反就行了(注意“^”是异或运算符)。

 

2.5 转为十六进制字符

#define TOHEXCHAR(i) ('0' + (i) + ( ('A' - ('0'+10)) & -((i)>=10) ))


解释:
其中的“( ('A' - ('0'+10))”会编译优化成常数的。
至于如何进行逆转换,估计只有用查表法了。 
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值