c++ - 如何使用 SIMD 指令使预乘 alpha 函数更快?

标签 c++ x86 sse simd avx

我正在寻找一些 SSE/AVX 建议来优化将 RGB channel 与其 alpha channel 预乘的例程:RGB * alpha/255(+ 我们保留原始的 alpha channel )。

    for (int i = 0, max = width * height * 4; i < max; i+=4) {
        data[i] = static_cast<uint16_t>(data[i] * data[i+3]) / 255;
        data[i+1] = static_cast<uint16_t>(data[i+1] * data[i+3]) / 255;
        data[i+2] = static_cast<uint16_t>(data[i+2] * data[i+3]) / 255;
    }

您会在下面找到我当前的实现,但我认为它可能会快得多,而且我正在浪费宝贵的 CPU 周期。我在 quick-bench.com 上对其进行了测试,它显示了令人鼓舞的结果,但我应该如何更改才能使其快速运行?

谢谢

-------- 2019 年 9 月 6 日更新--------

根据@chtz 和@Peter Cordes 的评论,我整理了一个 repository 来评估不同的解决方案,这里是结果。你认为它可以更好吗?

Run on (8 X 3100 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 262K (x4)
  L3 Unified 8388K (x1)
Load Average: 1.24, 1.60, 1.68
-----------------------------------------------------------------------------
Benchmark                   Time             CPU   Iterations UserCounters...
-----------------------------------------------------------------------------
v1_plain_mean         1189884 ns      1189573 ns         1000 itr=840.865/s
v1_plain_median       1184059 ns      1183786 ns         1000 itr=844.747/s
v1_plain_stddev         20575 ns        20166 ns         1000 itr=13.4227/s

v1_simd_x86_mean       297866 ns       297784 ns         1000 itr=3.3616k/s
v1_simd_x86_median     294995 ns       294927 ns         1000 itr=3.39067k/s
v1_simd_x86_stddev       9863 ns         9794 ns         1000 itr=105.51/s

Thanks Dot and Beached (discord #include)
v2_plain_mean          323541 ns       323451 ns         1000 itr=3.09678k/s
v2_plain_median        318932 ns       318855 ns         1000 itr=3.13623k/s
v2_plain_stddev         13598 ns        13542 ns         1000 itr=122.588/s

Thanks Peter Cordes (stackoverflow)
v3_simd_x86_mean       264323 ns       264247 ns         1000 itr=3.79233k/s
v3_simd_x86_median     260641 ns       260560 ns         1000 itr=3.83788k/s
v3_simd_x86_stddev      12466 ns        12422 ns         1000 itr=170.36/s

Thanks chtz (stackoverflow)
v4_simd_x86_mean       266174 ns       266109 ns         1000 itr=3.76502k/s
v4_simd_x86_median     262940 ns       262916 ns         1000 itr=3.8035k/s
v4_simd_x86_stddev      11993 ns        11962 ns         1000 itr=159.906/s

-------- 2019 年 10 月 6 日更新--------

我添加了 AVX2 版本并使用了 chtz 的提示。在 color_odd 中使用 255 作为 alpha 值,我能够删除 _mm_blendv_epi8 并改进基准。

谢谢 Peter 和 chtz

v3_simd_x86_mean       246171 ns       246107 ns          100 itr=4.06517k/s
v3_simd_x86_median     245191 ns       245167 ns          100 itr=4.07885k/s
v3_simd_x86_stddev       5423 ns         5406 ns          100 itr=87.13/s

// AVX2
v5_simd_x86_mean       158456 ns       158409 ns          100 itr=6.31411k/s
v5_simd_x86_median     158248 ns       158165 ns          100 itr=6.3225k/s
v5_simd_x86_stddev       2340 ns         2329 ns          100 itr=92.1406/s

最佳答案

如果可以使用 SSSE3,_mm_shuffle_epi8 可让您创建 __m128i alpha vector ,而不是 AND/shift/OR。

pshufb 会将随机播放控制 vector 元素的高位设置为零的字节置零。 (Shuffle 吞吐量很容易成为 Intel Haswell 及更高版本的瓶颈,因此使用立即移位或 AND 仍然适用于您可以用一条指令完成的其他操作。)

在 Skylake 和更高版本上,使用 SSE4.1 pblendvb 合并 alpha 而不是 AND/ANDN/OR 可能是一个胜利。 (在 Haswell 上,pblendvb 的 2 微指令只能在端口 5 上运行。这实际上可能没问题,因为有足够多的其他微代码,这不会造成随机瓶颈。)

在 Skylake 上,非 VEX pblendvb 是在任何端口上运行的单 uop 指令。 (VEX 版本对于任何端口都是 2 uops,所以它仍然严格优于 AND/ANDN/OR,但不如 SSE 版本。虽然 SSE 版本使用隐式 XMM0 输入,所以它需要额外的 movdqa 指令,除非你的循环只使用具有相同混合掩码的 pblendvb。或者如果你展开它可能会分摊 movdqa 以设置 XMM0。)


此外,_mm_srli_epi16 by 7 和 _mm_slli_epi16(color_odd, 8) 可能只是一个类次,可能还有一个 AND。或者 pblendvb 避免了像在 OR 之前那样清除垃圾的需要。

我不确定你是否可以使用 _mm_mulhrs_epi16进行多重转移,但可能不会。这不是正确的转变,“舍入”的 +1 不是您想要的。


显然,AVX2 版本的每条指令可以完成两倍的工作量,主循环在 Haswell/Skylake 上的速度提高了 2 倍。在 Ryzen 上可能有点中立,其中 256b 指令解码为 2 微指令。 (或更多用于交叉车道洗牌,但你没有这些。)

最坏的情况下清理必须运行更多次,但这仍然可以忽略不计。

关于c++ - 如何使用 SIMD 指令使预乘 alpha 函数更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56430849/

相关文章:

c++ - 在 C++ 中调整卷文件的长度和宽度

c++ - 如何在宏中正确转发结构化绑定(bind)参数

c++ - 不能在 VirtualQuery 返回的空闲区域上使用 VirtualAlloc

c - 在 gdb 中查看 ASCII 格式的寄存器内容

c - SSE 移位整数

c++ - [[maybe_unused]] 在成员变量上,GCC 警告(错误地?)该属性被忽略

c++ - 我的汇编代码有什么问题

windows - 在什么情况下我需要为 x86-64 汇编函数设置 SEH 展开信息?

c++ - 如何正确地从水平阵列垂直读取数据?

c++ - 如何将单精度 float 的 XMM 寄存器转换为整数?