SSE指令集 - if/else优化比较

8

我一直在尝试优化处理原始像素数据的代码。目前,这段代码的C++实现速度太慢了,因此我一直在尝试使用MSVC 2008和SSE内置函数(SSE / 2/3而不使用4)来取得一些进展。考虑到这是我第一次涉足这个层面,我已经取得了一些进展。

不幸的是,我遇到了一个让我卡住的特定代码段:

//Begin bad/suboptimal SSE code
__m128i vnMask  = _mm_set1_epi16(0x0001);
__m128i vn1     = _mm_and_si128(vnFloors, vnMask);

for(int m=0; m < PBS_SSE_PIXELS_PROCESS_AT_ONCE; m++)
{
    bool bIsEvenFloor   = vn1.m128i_u16[m]==0;

    vnPxChroma.m128i_u16[m] = 
        m%2==0 
            ?
        (bIsEvenFloor ? vnPxCeilChroma.m128i_u16[m] : vnPxFloorChroma.m128i_u16[m])
            :
        (bIsEvenFloor ? vnPxFloorChroma.m128i_u16[m] : vnPxCeilChroma.m128i_u16[m]);
}

目前,我选择使用C++实现来处理此部分,因为我无法理解如何使用SSE进行优化-我发现比较的SSE内置函数有点棘手。

如果有任何建议/提示,将不胜感激。

编辑:每次处理一个像素的等效C++代码如下:

short pxCl=0, pxFl=0;
short uv=0; // chroma component of pixel
short y=0;  // luma component of pixel

for(int i = 0; i < end-of-line, ++i)
{
    //Initialize pxCl, and pxFL
    //...

    bool bIsEvenI       = (i%2)==0;
    bool bIsEvenFloor   = (m_pnDistancesFloor[i] % 2)==0;

    uv = bIsEvenI ==0 
        ?
    (bIsEvenFloor ? pxCl : pxFl)
        :
    (bIsEvenFloor ? pxFl : pxCl);

    //Merge the Y/UV of the pixel;
    //...
}

基本上,我正在进行从4:3到16:9的非线性边缘拉伸。

2
SSE指令集很难阅读。您介意添加一些注释/等效的C++代码块来解释这个部分吗? - Dr. Andrew Burnett-Thompson
你希望这段代码要做什么? - ronag
我对这段代码有些困惑(含糊的标识符和没有上下文),但是为什么不用乘法和加法替换比较呢? - zrslv
@ZeroDefect,你能否发布一下vnFloors的声明? - Dr. Andrew Burnett-Thompson
2个回答

7

好的,我不知道这段代码在做什么,但我知道你想要优化三元运算符并使这部分代码只在SSE中运行。作为第一步,我建议尝试使用整数标志和乘法来避免条件运算符。例如:

这个部分

for(int m=0; m < PBS_SSE_PIXELS_PROCESS_AT_ONCE; m++)
{
    bool bIsEvenFloor   = vn1.m128i_u16[m]==0;      

    vnPxChroma.m128i_u16[m] = m%2==0 ?  
      (bIsEvenFloor ? vnPxCeilChroma.m128i_u16[m] : vnPxFloorChroma.m128i_u16[m])  : 
      (bIsEvenFloor ? vnPxFloorChroma.m128i_u16[m] : vnPxCeilChroma.m128i_u16[m]); 
}

在语法上等同于这个

// DISCLAIMER: Untested both in compilation and execution

// Process all m%2=0 in steps of 2
for(int m=0; m < PBS_SSE_PIXELS_PROCESS_AT_ONCE; m+=2)
{
    // This line could surely pack muliple u16s into one SSE2 register
    uint16 iIsOddFloor = vn1.m128i_u16[m] & 0x1 // If u16[m] == 0, result is 0
    uint16 iIsEvenFloor = iIsOddFloor ^ 0x1 // Flip 1 to 0, 0 to 1

    // This line could surely perform an SSE2 multiply across multiple registers
    vnPxChroma.m128i_u16[m] = iIsEvenFloor * vnPxCeilChroma.m128i_u16[m] + 
                              iIsOddFloor * vnPxFloorChroma.m128i_u16[m]
}

// Process all m%2!=0 in steps of 2
for(int m=1; m < PBS_SSE_PIXELS_PROCESS_AT_ONCE; m+=2)
{
    uint16 iIsOddFloor = vn1.m128i_u16[m] & 0x1 // If u16[m] == 0, result is 0
    uint16 iIsEvenFloor = iIsOddFloor ^ 0x1 // Flip 1 to 0, 0 to 1

    vnPxChroma.m128i_u16[m] = iIsEvenFloor * vnPxFloorChroma.m128i_u16[m] + 
                              iIsOddFloor * vnPxCeilChroma.m128i_u16[m]
}

将其分成两个循环后,您失去了串行内存访问的性能提升,但可以省去模运算和两个条件运算符。

现在您可能会注意到每个循环中有两个布尔运算符以及乘法。 我要补充的是,这些乘法不是SSE内在实现。 您的vn1.m123i_u16 []数组中存储了什么? 它只包含0和1吗? 如果是,则不需要此部分,并且可以将其删除。 如果不是,请将此数组中的数据归一化为仅包含零和一。 如果vn1.m123i_u16数组仅包含1和0,则该代码变为

uint16 iIsOddFloor  = vn1.m128i_u16[m]
uint16 iIsEvenFloor = iIsOddFloor ^ 0x1 // Flip 1 to 0, 0 to 1

您还会注意到,我没有使用SSE乘法来执行isEvenFloor * vnPx...部分,也没有使用SSE寄存器来存储iIsEvenFlooriIsOddFloor。很抱歉我无法记住u16乘法/寄存器的SSE内在函数,但无论如何,我希望这种方法对您有所帮助。您应该考虑一些优化:

// This line could surely pack muliple u16s into one SSE2 register
uint16 iIsOddFloor = vn1.m128i_u16[m] & 0x1 // If u16[m] == 0, result is 0
uint16 iIsEvenFloor = iIsOddFloor ^ 0x1 // Flip 1 to 0, 0 to 1

// This line could surely perform an SSE2 multiply across multiple registers
vnPxChroma.m128i_u16[m] = iIsEvenFloor * vnPxCeilChroma.m128i_u16[m] + 
                          iIsOddFloor * vnPxFloorChroma.m128i_u16[m] 

在你所发布的代码和我的修改中,我们仍未充分利用SSE1/2/3内嵌函数,但这可能提供了一些关于如何向量化代码的思路。
最后我想说要测试所有东西。在进行更改和再次进行剖析之前运行上面的代码未经修改。实际的性能数字可能会让你感到惊讶!

更新1:

我已经查看了Intel SIMD Intrinsics文档,挑选出了与此相关的内在函数,这些函数可能对此有用。特别是要看一下按位异或、按位与和MULT/ADD。

__m128 数据类型 __m128i 数据类型可以容纳16个8位、8个16位、4个32位或2个64位整数值。
__m128i _mm_add_epi16(__m128i a, __m128i b) 将a中的8个有符号或无符号16位整数加到b中的8个有符号或无符号16位整数。
__m128i _mm_mulhi_epu16(__m128i a, __m128i b) 将a中的8个无符号16位整数乘以b中的8个无符号16位整数。 打包8个无符号32位结果的上16位。
R0=hiword(a0 * b0) R1=hiword(a1 * b1) R2=hiword(a2 * b2) R3=hiword(a3 * b3) .. R7=hiword(a7 * b7)
__m128i _mm_mullo_epi16(__m128i a, __m128i b) 将a中的8个有符号或无符号16位整数乘以b中的8个有符号或无符号16位整数。 打包8个有符号或无符号32位结果的上16位。
R0=loword(a0 * b0) R1=loword(a1 * b1) R2=loword(a2 * b2) R3=loword(a3 * b3) .. R7=loword(a7 * b7)
__m128i _mm_and_si128(__m128i a, __m128i b) 对m1中的128位值执行按位AND运算,与m2中的128位值。
__m128i _mm_andnot_si128(__m128i a, __m128i b) 计算b中的128位值和a中的128位值的按位NOT的按位AND。
__m128i _mm_xor_si128(__m128i a, __m128i b) 对m1中的128位值执行按位异或运算,与m2中的128位值。
也可以参考您的代码示例 uint16 u1 = u2 = u3 ... = u15 = 0x1 __m128i vnMask = _mm_set1_epi16(0x0001); //设置8个有符号16位整数值。 uint16 vn1[i] = vnFloors[i] & 0x1 __m128i vn1 = _mm_and_si128(vnFloors, vnMask); //计算a中的128位值和b中的128位值的按位AND。

可以使用按位与运算符代替乘法吗? - zrslv
谢谢,我已经按照你建议的将C++实现分解为两个(2)单独的for循环。我没有考虑使用乘法/加法进行比较。我想要的部分是将这两个for循环合并为一组指令。 - ZeroDefect

2

安德鲁,你的建议让我找到了接近最优解的路径。

通过真值表和卡诺图的组合,我发现代码

 uv = bIsEvenI ==0 
    ?
(bIsEvenFloor ? pxCl : pxFl)
    :
(bIsEvenFloor ? pxFl : pxCl);

简而言之,这是一个!xor(非异或)函数。从那时起,我就能够使用SSE向量化来优化解决方案:

//Use the mask with bit AND to check if even/odd
__m128i vnMask              = _mm_set1_epi16(0x0001);

//Set the bit to '1' if EVEN, else '0'
__m128i vnFloorsEven        = _mm_andnot_si128(vnFloors, vnMask);
__m128i vnMEven             = _mm_set_epi16
    (
        0,  //m==7
        1,
        0,
        1,
        0,
        1,
        0,  //m==1
        1   //m==0
    );


// Bit XOR the 'floor' values and 'm'
__m128i vnFloorsXorM        = _mm_xor_si128(vnFloorsEven, vnMEven);

// Now perform our bit NOT
__m128i vnNotFloorsXorM     = _mm_andnot_si128(vnFloorsXorM, vnMask);

// This is the C++ ternary replacement - using multipilaction
__m128i vnA                 = _mm_mullo_epi16(vnNotFloorsXorM, vnPxFloorChroma);
__m128i vnB                 = _mm_mullo_epi16(vnFloorsXorM, vnPxCeilChroma);

// Set our pixels - voila!
vnPxChroma                  = _mm_add_epi16(vnA, vnB);

感谢您的所有帮助...

哇!做得好,发布解决方案的工作也很棒!顺便问一下,SSE版本与普通C++代码相比性能如何?“真值表和卡诺图”我喜欢。我记得在GCSE电子学中做过这些! - Dr. Andrew Burnett-Thompson
1
谢谢。SSE实现的运行时间少于一半。在发布这篇文章之前,我本来想看一下已编译的C++实现,希望它会提供一些建议。不幸的是,由于所有的分支(和缓存未命中),C++实现受到了严重的限制 - 它没有充分利用!xor模式。是的,卡诺图真的很厉害。 - ZeroDefect
太好了!尽管你正在使用u16,但最大理论速度提升可达8倍。但要做到这一点并不容易,你可能已经发现了! - Dr. Andrew Burnett-Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接