zoukankan      html  css  js  c++  java
  • 优化分支代码——避免跳转指令堵塞流水线

    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))”会编译优化成常数的。
    至于如何进行逆转换,估计只有用查表法了。
     

    作者:zyl910
    版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0.
  • 相关阅读:
    SSH框架测试
    Top 20 IoT Platforms in 2018
    基于Helm和Operator的K8S应用管理
    五大开源 Web 代理服务器横评:Squid、Privoxy、Varnish、Polipo、Tinyproxy
    Https单向认证和双向认证
    CNCF Landscape Summary
    走,去出海,一起“Copy to World” | 36氪出海行业报告
    猎豹全球智库执行院长:中国App出海的三大规律和最具代表的五大垂直品类
    闷声发大财,中国 App 出海编年史及方法论
    软件出海的四种模式
  • 原文地址:https://www.cnblogs.com/zyl910/p/2186637.html
Copyright © 2011-2022 走看看