【问题标题】:SSE intrinsics - comparison if/else optimizationSSE 内在函数 - 比较 if/else 优化
【发布时间】:2012-02-17 16:22:21
【问题描述】:

我一直在尝试优化一些处理原始像素数据的代码。目前,代码的 C++ 实现太慢了,所以我一直在尝试使用 SSE 内在函数(SSE/2/3 不使用 4)和 MSVC 2008 来建立一些基础。考虑到这是我第一次在这么低的地方挖掘,我已经取得了一些不错的进展。

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

//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 进行非线性边缘拉伸。

【问题讨论】:

  • SSE 内在函数很难阅读。您介意添加一些 cmets / 等效 C++ 代码块来解释本节吗?
  • 你想让代码做什么?
  • 我对这个 sn-p(神秘的标识符,没有上下文)有些困惑,但是为什么不将比较替换为乘法和加法呢?
  • @ZeroDefect 你能发布你的 vnFloors 声明吗?

标签: c++ sse intrinsics


【解决方案1】:

好的,所以我不知道这段代码在做什么,但是我知道您在问如何优化三元运算符并让这部分代码仅在 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]); 
}

在语法上等价于 this

// 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[] 数组中存储了什么?只有零和一吗? 如果是这样,您不需要这部分并且可以取消它。如果没有,您可以将此数组中的数据标准化为只有零和一吗?如果 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... part,也没有存储 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 documentation 挑选出可能对此有用的相关内在函数。具体看一下按位异或、与和多/加

__m128 数据类型
__m128i 数据类型可以保存 16 个 8 位、8 个 16 位、4 个 32 位或两个 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=低字(a0 * b0)
R1=低字(a1 * b1)
R2=低字(a2 * b2)
R3=低字(a3 * b3)
..
R7=低字(a7 * b7)

__m128i _mm_and_si128(__m128i a, __m128i b)
将 m1 中的 128 位值与 m2 中的 128 位值进行按位与。

__m128i _mm_andnot_si128(__m128i a, __m128i b)
计算 b 中的 128 位值的按位与和 128- 的按位非 a中的位值。

__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 位值的按位与。

【讨论】:

  • 是否可以使用按位与而不是乘法?
  • 谢谢,我已经按照您的建议将 C++ 实现分解为两 (2) 个单独的 for 循环。我没有考虑使用乘法/加法进行比较。我热衷的部分是将两个 for 循环组合成一 (1) 组指令。
【解决方案2】:

Andrew,您的建议使我找到了接近最佳的解决方案。

结合使用真值表和卡诺图,我发现代码

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

归结为一个 !xor(不是 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++ 代码的性能如何? truth table and karnaugh map 喜欢。我记得为 GCSE 电子产品做过这些!
  • 谢谢。 SSE 实施的运行时间不到一半。最初(在发布这篇文章之前),我查看了组装的 C++ 实现,希望它能分享一些建议。不幸的是,C++ 实现受到所有分支(和缓存未命中)的严重阻碍——它没有充分利用 !xor 模式。是的,卡诺图就是炸弹。
  • 太棒了!尽管 c 您使用的是 u16,但捕获的最大理论速度提高了 8 倍。您可能已经发现,说起来容易做起来难!
猜你喜欢
  • 1970-01-01
  • 2011-09-11
  • 1970-01-01
  • 2019-12-08
  • 2013-03-24
  • 1970-01-01
  • 2020-11-23
  • 2023-03-28
  • 2010-12-29
相关资源
最近更新 更多