如何使用SIMD指令使预乘alpha功能更快?

时间:2019-06-03 15:59:56

标签: c++ x86 sse simd avx

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

    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上对其进行了测试,结果令人鼓舞,但我应该做些什么更改才能使其快速发展呢?

谢谢

--------更新日期09/06/2019 --------

基于@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

--------更新10/06/2019 --------

我添加了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

2 个答案:

答案 0 :(得分:3)

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

pshufb将零位设置随机控制矢量元素的高位。 (无序吞吐量很容易成为Intel Haswell及更高版本的瓶颈,因此,使用立即移位或AND仍可用于其他操作(只需一条指令即可完成操作)。)

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

在Skylake上,非VEX pblendvb是在任何端口上运行的单uup指令。 (任何端口的VEX版本均为2 uops,因此它仍然严格比AND / ANDN / OR好,但不如SSE版本好。尽管SSE版本使用了隐式XMM0输入,所以要多花{{{1 }}指令,除非您的循环仅使用具有相同混合掩码的movdqa。否则,如果展开,则可以分摊pblendvb来设置XMM0。)


此外,movdqa乘7和_mm_srli_epi16可能只是一个移位,可能是AND。或者_mm_slli_epi16(color_odd, 8)避免像在OR之前一样清除垃圾。

我不确定您是否可以使用_mm_mulhrs_epi16进行mul-and-shift,但可能不会。这不是正确的选择,pblendvb也不是您想要的。


很明显,AVX2版本的每条指令可以完成两倍的工作,从而使Haswell / Skylake的主循环速度提高了2倍。在Ryzen上可能是中性的,其中256b指令解码为2微妙。 (或者更多用于过道洗牌,但您没有这些。)

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

答案 1 :(得分:3)

我在玩这个。我认为最好的解决方案是将输入从两个寄存器分成16位整数(即0x00交织的8位整数)通道。然后进行实际缩放(对于原来的方法,仅进行6次乘法+ 3个移位(8个像素,而不是原来的8 + 4)),然后将通道重新合并为像素。

概念验证(未经测试),假设输入对齐并且像素数是8(2.0版)的倍数(以前的版本,请参见history

void alpha_premultiply(__m128i *input, int length)
{
    for(__m128i* last = input + (length & ~1); input!=last; input+=2)
    {
        // load data and split channels:
        __m128i abgr = _mm_load_si128(input);
        __m128i ABGR = _mm_load_si128(input+1);
        __m128i __ab = _mm_srli_epi32(abgr,16);
        __m128i GR__ = _mm_slli_epi32(ABGR,16);
        __m128i ABab = _mm_blend_epi16(ABGR, __ab, 0x55);
        __m128i GRgr = _mm_blend_epi16(GR__, abgr, 0x55);
        __m128i A_a_ = _mm_and_si128(ABab, _mm_set1_epi16(0xFF00));
        __m128i G_g_ = _mm_and_si128(GRgr, _mm_set1_epi16(0xFF00));
        __m128i R_r_ = _mm_slli_epi16(GRgr, 8);
        __m128i B_b_ = _mm_slli_epi16(ABab, 8);

        // actual alpha-scaling:
        __m128i inv = _mm_set1_epi16(0x8081); // = ceil((1<<(16+7))/255.0)
        G_g_ = _mm_mulhi_epu16(_mm_mulhi_epu16(G_g_, A_a_), inv);
        // shift 7 to the right and 8 to the left, or shift 1 to the left and mask:
        G_g_ = _mm_and_si128(_mm_add_epi16(G_g_, G_g_), _mm_set1_epi16(0xFF00));
        __m128i _R_r = _mm_mulhi_epu16(_mm_mulhi_epu16(R_r_, A_a_), inv);
        _R_r = _mm_srli_epi16(_R_r,7);
        __m128i _B_b = _mm_mulhi_epu16(_mm_mulhi_epu16(B_b_, A_a_), inv);
        _B_b = _mm_srli_epi16(_B_b,7);

        // re-assemble channels:
        GRgr = _mm_or_si128(_R_r, G_g_);
        ABab = _mm_or_si128(A_a_, _B_b);

        __m128i __GR = _mm_srli_epi32(GRgr, 16);
        __m128i ab__ = _mm_slli_epi32(ABab, 16);

        ABGR = _mm_blend_epi16(ABab, __GR, 0x55);
        abgr = _mm_blend_epi16(ab__, GRgr, 0x55);

        // store result
        _mm_store_si128(input, abgr);
        _mm_store_si128(input+1, ABGR);
    }
}

变量名使用_标记为0,而最低的地址字节在右边(以减少对移位和位操作的混淆)。每个寄存器将保存4个连续像素,或4 + 4个交错的通道。小写字母和大写字母来自不同的输入位置。 (Godbolt:https://godbolt.org/z/OcxAfJ

在Haswell(或更早版本)上,这将成为端口0的瓶颈(移位和乘法),但是使用SSSE3,您可以用_mm_alignr_epi8替换所有8和16移位。并且最好将_R_r_B_b留在较低的字节上(使用pand而不是psllw,但需要将A_a_移到{{ 1}})。可能的陷阱:clang被相应的移位指令https://godbolt.org/z/BhEZoV取代_A_a(也许有禁止clang替换的标志。GCC使用_mm_alignr_epi8https://godbolt.org/z/lu-jNQ

在Skylake上,这可能是最佳选择(当然,除了移植到AVX2之外)。有8个移位,6个乘法和1个加法,即在端口0和1上进行15次运算。此外,在端口5和5上进行4次混合和/或运算(p5上为4,p0或p1上为另一个),即8每个端口uops为8像素(对于AVX2,为16像素)。

代码应该非常容易移植到AVX2(并且单独使用AVX1将保存一些寄存器副本)。最后,为了使代码SSE2兼容,只需将混合指令替换为相应的和/或操作即可。