zoukankan      html  css  js  c++  java
  • 深入探讨用位掩码代替分支(3):VC6速度测试

      wuhanbingwhdx提到了数据相关也会影响流水线(http://blog.csdn.net/zyl910/article/details/1330614)。
      他的说法是有一定道理的。但是,在很多时候我们并不仅仅处理一个数值。比如将循环展开,在内循环处理2个或更多个的数值。而现代编译器面对循环展开时,在编译优化操作中会调整指令顺序,错开有相关性指令。因现代处理器支持超标量,这样的指令顺序调整能获得较好的指令级并行度,从而优化了性能。
      其次,就算编译器对循环展开优化的不够彻底,没将相关性指令错开。但因现代处理器支持乱序执行,当遇到相关性指令需要等待时,处理器会处理后面未相关的指令,从而保持处理器满载尽量减轻相关性等待造成的性能损失。
      第三,现代处理器还支持寄存器重命名技术——当两处代码用到同名的寄存器时,译码器会做寄存器重命名处理给它们分配不同的寄存器,使数据不会干扰,从而获得更高的指令级并行度。

      上面说了很多理论知识,实际性能到底怎么样呢?还是写代码测一测吧。


    一、目标——将64位像素转为32位像素

      将64位像素转为32位像素,是饱和处理的最典型应用。
      64位像素有4个通道,每个通道16位(带符号16位整数)。每像素8个字节。
      32位像素有4个通道,每个通道8位(无符号8位整数)。每像素4个字节。

      转换方法为——将每个像素的4个通道由16位(带符号16位整数)转为8位(无符号8位整数)。因为每次都是对4个通道进行处理,所以能获得较高的指令级并行度。

      具体的存储格式为——

    注:
    1.内存地址由低到高(从下到上),垂直方向的每一格是一个字节。+0代表数据基址,+1代表数据基址+1,以此类推。
    2.左为“64位像素”数组,右为“32位像素”数组。
    3.图中使用了用双边线来分隔像素。因“64位像素”是8字节,而“32位像素”是4字节。所以对于16个字节的空间,左侧能存放2个像素、右侧能存放4个像素。
    4.图中使用了用实线来分隔通道。“64位像素”的通道是16位,占用2个字节。“32位像素”的通道是8位,只占用1个字节。
    5.图中使用了用虚线来分隔字节。主要用于“64位像素”。
    6.这里采用了Windows位图通道规则,即通道顺序为B、G、R、A(从低到高)。例如:“B0”代表像素0的B(蓝色)通道、“A0”代表像素0的A(不透明度)通道、“A1”代表像素1的A通道……以此类推。
    7.这里采用了小端(Little Endian)方式的字节序(Endianness),即最低地址存放的最低字节。采用小写的“h”、“l”来表示“64位像素”通道的高、低字节。例如:“B0l”代表像素0的B通道的低字节、“A0h”代表像素0的A通道的高字节……以此类推。

      上面貌似挺复杂,又是图表又是大段文字的。其实,代码写起来很简单的,一般情况下不需要理会通道顺序与字节序问题——

    // 用if分支做饱和处理
    void f0_if(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = (pS[0]<0) ? 0 : ( (pS[0]>255) ? 255 : (BYTE)pS[0] );
    pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );
    pD[2] = (pS[2]<0) ? 0 : ( (pS[2]>255) ? 255 : (BYTE)pS[2] );
    pD[3] = (pS[3]<0) ? 0 : ( (pS[3]>255) ? 255 : (BYTE)pS[3] );
    // next
    pS += 4;
    pD += 4;
    }
    }



      参数说明——
    pbufD:目标缓冲区的地址。如“64位像素”数组的首地址。
    pbufS:源缓冲区的地址。如“32位像素”数组的首地址。
    cnt:像素个数。

      使用方法——

    signed short    bufS[DATASIZE*4];    // 源缓冲区。64位的颜色(4通道,每通道16位)
    BYTE bufD[DATASIZE*4]; // 目标缓冲区。32位的颜色(4通道,每通道8位)

    f0_if(bufD, bufS, DATASIZE);



      对于数据处理来说,用指针比用数组写起来更简洁,而且执行速度更快。
      而且C语言中的指针支持下标运算符,能够用下标访问后面的元素(“pD[1]”相当于“*(pD + 1)”),简化了不少代码。(指针下标可参考 http://www.lupaworld.com/home-space-uid-77885-do-blog-id-28843.html
      例如“pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );”这行代码的数组写法为——

    pbufD[i*4+1] = (pbufS[i*4+1]<0) ? 0 : ( (pbufS[i*4+1]>255) ? 255 : (BYTE)pbufS[i*4+1] );

      因为条件语句“if”的代码写起来比较繁琐,所以这里用到了条件运算符“?:”来简化代码。例如“pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );”这行代码的“if”写法为——

    if (pS[1]<0)
    pD[1]=0
    else
    if (pS[1]>255)
    pD[1]=255
    else
    pD[1]=(BYTE)pS[1];


    二、测试方法——测试程序的框架

      前面已经编写了一个函数f0_if,随后我们会编写多个函数,分别测试性能。具体怎么测试呢?难道是为每一个函数都写一套测试代码……不,那样的话太糟糕了。
      我们可以利用函数指针进行统一的测试。函数指针定义如下,与f0_if的参数列相同——

    // 测试时的函数类型
    typedef void (*TESTPROC)(BYTE* pbufD, const signed short* pbufS, int cnt);

      有了函数指针后,进行测试就很简单了,只需要将要测试的函数传递过去就行了。例如这样测试f0_if——

    runTest("f0_if", f0_if);

      runTest代码如下——

    // 进行测试
    void runTest(char* szname, TESTPROC proc)
    {
    int i,j;
    DWORD tm0, tm1; // 存储时间
    for(i=1; i<=3; ++i) // 多次测试
    {
    //tm0 = GetTickCount();
    tm0 = timeGetTime();
    // main
    for(j=1; j<=4000; ++j) // 重复运算几次延长时间,避免计时精度问题
    {
    proc(bufD, bufS, DATASIZE);
    }
    // show
    //tm1 = GetTickCount() - tm0;
    tm1 = timeGetTime() - tm0;
    printf("%s[%d]:\t%u\n", szname, i, tm1);

    }
    }

      printf输出的是测试时间,单位毫秒。值越小,表示所花时间越少、运行速度越快、性能越高。
      这里用到了timeGetTime来计算时间,要注意加上winmm.lib库——

      对于bufD、bufS、DATASIZE,我是这样定义的——

    // 数据规模
    #define DATASIZE 16384 // 128KB / (sizeof(signed short) * 4)

    // 缓冲区
    signed short bufS[DATASIZE*4]; // 源缓冲区。64位的颜色(4通道,每通道16位)
    BYTE bufD[DATASIZE*4]; // 目标缓冲区。32位的颜色(4通道,每通道8位)

      缓冲区的尺寸是特意规定的。对于现在主流CPU来说,Intel处理器的二级缓存一般是每核心256KB,而AMD处理器的二级缓存一般是每核心512KB。所以数据最好不要超过256KB,这样就能在二级缓存上完成处理,避免了内存访问延时造成的干扰。
      于是我给bufS分配了128KB,给bufD分配了64KB。


    三、更多的测试

      用min、max做饱和处理——

    // 用min、max饱和处理
    void f1_min(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = min(max(0, pS[0]), 255);
    pD[1] = min(max(0, pS[1]), 255);
    pD[2] = min(max(0, pS[2]), 255);
    pD[3] = min(max(0, pS[3]), 255);
    // next
    pS += 4;
    pD += 4;
    }
    }

      用位掩码做饱和处理,用求负生成掩码——

    // 用位掩码做饱和处理.用求负生成掩码
    void f2_neg(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = LIMITSU_BYTE(pS[0]);
    pD[1] = LIMITSU_BYTE(pS[1]);
    pD[2] = LIMITSU_BYTE(pS[2]);
    pD[3] = LIMITSU_BYTE(pS[3]);
    // next
    pS += 4;
    pD += 4;
    }
    }



      用位掩码做饱和处理,用带符号右移生成掩码——

    // 用位掩码做饱和处理.用带符号右移生成掩码
    void f3_sar(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = LIMITSW_BYTE(pS[0]);
    pD[1] = LIMITSW_BYTE(pS[1]);
    pD[2] = LIMITSW_BYTE(pS[2]);
    pD[3] = LIMITSW_BYTE(pS[3]);
    // next
    pS += 4;
    pD += 4;
    }
    }


    四、全部代码

      全部代码为——

    // 用位掩码做饱和处理.用求负生成掩码
    #define LIMITSU_FAST(n, bits) ( (n) & -((n) >= 0) | -((n) >= (1<<(bits))) )
    #define LIMITSU_SAFE(n, bits) ( (LIMITSU_FAST(n, bits)) & ((1<<(bits)) - 1) )
    #define LIMITSU_BYTE(n) ((BYTE)(LIMITSU_FAST(n, 8)))

    // 用位掩码做饱和处理.用带符号右移生成掩码
    #define LIMITSW_FAST(n, bits) ( ( (n) | ((signed short)((1<<(bits)) - 1 - (n)) >> 15) ) & ~((signed short)(n) >> 15) )
    #define LIMITSW_SAFE(n, bits) ( (LIMITSW_FAST(n, bits)) & ((1<<(bits)) - 1) )
    #define LIMITSW_BYTE(n) ((BYTE)(LIMITSW_FAST(n, 8)))


    // 数据规模
    #define DATASIZE 16384 // 128KB / (sizeof(signed short) * 4)

    // 缓冲区
    signed short bufS[DATASIZE*4]; // 源缓冲区。64位的颜色(4通道,每通道16位)
    BYTE bufD[DATASIZE*4]; // 目标缓冲区。32位的颜色(4通道,每通道8位)

    // 测试时的函数类型
    typedef void (*TESTPROC)(BYTE* pbufD, const signed short* pbufS, int cnt);

    // 用if分支做饱和处理
    void f0_if(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = (pS[0]<0) ? 0 : ( (pS[0]>255) ? 255 : (BYTE)pS[0] );
    pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );
    pD[2] = (pS[2]<0) ? 0 : ( (pS[2]>255) ? 255 : (BYTE)pS[2] );
    pD[3] = (pS[3]<0) ? 0 : ( (pS[3]>255) ? 255 : (BYTE)pS[3] );
    // next
    pS += 4;
    pD += 4;
    }
    }

    // 用min、max饱和处理
    void f1_min(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = min(max(0, pS[0]), 255);
    pD[1] = min(max(0, pS[1]), 255);
    pD[2] = min(max(0, pS[2]), 255);
    pD[3] = min(max(0, pS[3]), 255);
    // next
    pS += 4;
    pD += 4;
    }
    }

    // 用位掩码做饱和处理.用求负生成掩码
    void f2_neg(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = LIMITSU_BYTE(pS[0]);
    pD[1] = LIMITSU_BYTE(pS[1]);
    pD[2] = LIMITSU_BYTE(pS[2]);
    pD[3] = LIMITSU_BYTE(pS[3]);
    // next
    pS += 4;
    pD += 4;
    }
    }

    // 用位掩码做饱和处理.用带符号右移生成掩码
    void f3_sar(BYTE* pbufD, const signed short* pbufS, int cnt)
    {
    const signed short* pS = pbufS;
    BYTE* pD = pbufD;
    int i;
    for(i=0; i<cnt; ++i)
    {
    // 分别对4个通道做饱和处理
    pD[0] = LIMITSW_BYTE(pS[0]);
    pD[1] = LIMITSW_BYTE(pS[1]);
    pD[2] = LIMITSW_BYTE(pS[2]);
    pD[3] = LIMITSW_BYTE(pS[3]);
    // next
    pS += 4;
    pD += 4;
    }
    }

    // 进行测试
    void runTest(char* szname, TESTPROC proc)
    {
    int i,j;
    DWORD tm0, tm1; // 存储时间
    for(i=1; i<=3; ++i) // 多次测试
    {
    //tm0 = GetTickCount();
    tm0 = timeGetTime();
    // main
    for(j=1; j<=4000; ++j) // 重复运算几次延长时间,避免计时精度问题
    {
    proc(bufD, bufS, DATASIZE);
    }
    // show
    //tm1 = GetTickCount() - tm0;
    tm1 = timeGetTime() - tm0;
    printf("%s[%d]:\t%u\n", szname, i, tm1);

    }
    }

    int main(int argc, char* argv[])
    {
    int i; // 循环变量

    //printf("Hello World!\n");
    printf("== noif:VC6 ==");

    // 初始化
    srand( (unsigned)time( NULL ) );
    for(i=0; i<DATASIZE*4; ++i)
    {
    bufS[i] = (signed short)((rand()&0x1FF) - 128); // 使数值在 [-128, 383] 区间
    }

    // 准备开始。可以将将进程优先级设为实时
    if (argc<=1)
    {
    printf("<Press any key to continue>");
    getch();
    printf("\n");
    }

    // 进行测试
    runTest("f0_if", f0_if);
    runTest("f1_min", f1_min);
    runTest("f2_neg", f2_neg);
    runTest("f3_sar", f3_sar);

    // 结束前提示
    if (argc<=1)
    {
    printf("<Press any key to exit>");
    getch();
    printf("\n");
    }

    return 0;
    }


    五、测试结果

      将程序编译为“Release”版,然后分别在不同的系统环境中进行测试。

      在32位winXP上的测试结果——

    == noif:VC6 ==<Press any key to continue>
    f0_if[1]: 2016
    f0_if[2]: 2016
    f0_if[3]: 2015
    f1_min[1]: 2063
    f1_min[2]: 2062
    f1_min[3]: 2063
    f2_neg[1]: 718
    f2_neg[2]: 719
    f2_neg[3]: 719
    f3_sar[1]: 672
    f3_sar[2]: 687
    f3_sar[3]: 672

      在64位win7上的测试结果——

    == noif:VC6 ==<Press any key to continue>
    f0_if[1]: 2075
    f0_if[2]: 2012
    f0_if[3]: 2028
    f1_min[1]: 2059
    f1_min[2]: 2075
    f1_min[3]: 2075
    f2_neg[1]: 717
    f2_neg[2]: 718
    f2_neg[3]: 718
    f3_sar[1]: 670
    f3_sar[2]: 687
    f3_sar[3]: 686

      硬件环境——
    CPU:Intel Core i3-2310M, 2100 MHz
    内存:DDR3-1066


    源码下载——
    https://files.cnblogs.com/zyl910/noifVC6.rar
    (建议阅读编译器生成的汇编代码,位于Release\noifVC6.asm )

  • 相关阅读:
    iOS开发-文件管理(一)
    浅析栈区和堆区内存分配的区别
    浅谈Block传值-匿名函数(代码块)
    cell的各种使用和赋值 总结
    类方法和对象方法的区别
    属性传值 ,代理传值,单例
    类目,延展,协议
    任意点 并查集
    Codeforces 145E. Lucky Queries 线段树
    Codeforces 103B. Cthulhu 并查集运用
  • 原文地址:https://www.cnblogs.com/zyl910/p/noifopex3.html
Copyright © 2011-2022 走看看