【问题标题】:AVX2 SIMD Instrinsics 16-bit to 8-bit vice-versaAVX2 SIMD Instrinsics 16 位到 8 位,反之亦然
【发布时间】:2020-02-15 23:10:40
【问题描述】:

我有一个 c++(或类 c)函数,我正在尝试对其进行矢量化。该功能是图像合成的众多变体之一,它采用具有色度 444 子采样的 Y、U 或 V 图像平面,并将 src 图像合成/叠加到 dst 图像上(其中 src 图像还包含 alpha 透明度)。

#include <cstdint>


void composite(uint8_t *__restrict__ pSrc,  // Source plane
               uint8_t *__restrict__ pSrcA, // Source alpha plane 
               uint8_t *__restrict__ pDst,  // Destination plane
               const std::size_t nCount)    // Number of component pixels to process.
{
    for (std::size_t k = 0; k < nCount; ++k)
    {
        uint16_t w = (pSrc[k] * pSrcA[k]);
        uint16_t x = (255 - pSrcA[k]) * pDst[k];
        uint16_t y = w+x;
        uint16_t z = y / uint16_t{255};
        pDst[k] = static_cast<uint8_t>(z);
    }
}

在 AVX2 矢量化等效项中,我正在努力理解如何有效地读取 8 位转换为 16 位以及(在处理/合成之后)最终将 16 位样本转换回 8 位以存储回来到记忆中。在读取方面,我使用的是中间 xmm 寄存器——这似乎不是最好的方法;我猜在混合寄存器系列时会有性能损失。

我想出了(不完整):

#include <cstdint>

#include <immintrin.h>
#include <emmintrin.h>


///////////////////////////////////////////////////////////////////////////
// Credit: https://stackoverflow.com/questions/35285324/how-to-divide-16-bit-integer-by-255-with-using-sse
#define AVX2_DIV255_U16(x) _mm256_srli_epi16(_mm256_mulhi_epu16(x, _mm256_set1_epi16((short)0x8081)), 7)

///////////////////////////////////////////////////////////////////////////
/// Blends/composites/overlays two planes of Y, U, or V plane with 4:4:4 chroma subsampling over the other.
/// \param d The destination Y, U , or V component
/// \param s The source Y, U, or V component
/// \param sa The source alpha component
/// \param pixels The number of pixels that require processing.
/// \return The number of pixels processed.
int blend_plane_pixels_444_vectorized(uint8_t *__restrict__ d,
                                      uint8_t *__restrict__ s,
                                      uint8_t *__restrict__ sa,
                                      const int pixels)
{
    int n = 0; // Return number of component pixels processed.
    for (int k = 0; k + 32 <= pixels; k += 32)
    {
        // Load first 16 (unaligned) of d, s, sa
        // TODO: This efficient mixing xmm registers with ymm??
        auto vecD0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)d));
        auto vecS0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)s));
        auto vecSa0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)sa));

        // Load second 16 (unaligned) of d, s, sa
        auto vd1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)d+16));
        auto vs1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)s+16));
        auto vsa1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)sa+16));

        // Load 255 into register
        auto vec255 = _mm256_set1_epi16(255);

        // uint16_t w = (pSrc[k] * pSrcA[k]);
        auto vecW0 = _mm256_mullo_epi16(vecS0, vecSa0);
        auto vecW1 = _mm256_mullo_epi16(vs1, vsa1);

        // uint16_t x = (255 - pSrcA[k]) * pDst[k];
        auto vecX0 = _mm256_mullo_epi16(_mm256_subs_epu16(vec255, vecSa0), vecD0);
        auto vecX1 = _mm256_mullo_epi16(_mm256_subs_epu16(vec255, vsa1), vd1);

        // Load 127 into register
        auto vec127 = _mm256_set1_epi16(127);

        // uint16_t y = w+x;
        auto vecY0 = _mm256_adds_epu16(_mm256_adds_epu16(vecW0, vecX0), vec127);
        auto vecY1 = _mm256_adds_epu16(_mm256_adds_epu16(vecW1, vecX1), vec127);

        // uint16_t z = y / uint16_t{255};
        auto vecZ0 = AVX2_DIV255_U16(vecY0);
        auto vecZ1 = AVX2_DIV255_U16(vecY1);

        // TODO: How to get this back into 8-bit samples so that it can be stored
        //       back into array.
        auto vecResult = _mm256_blendv_epi8(vecZ0, vecZ1, _mm256_set1_epi16(127));

        // Write data back to memory (unaligned)
        _mm256_storeu_si256((__m256i*)d, vecResult);

        d += 32;
        s += 32;
        sa += 32;
        n += 32;
    }

    return n;
}

SIMD 不是我的强项,我需要在这方面做得更好 - 请保持温和。我想可能有很多调整可以应用于当前的矢量化代码(欢迎提出建议!)

开发环境:

  • Linux Ubuntu 18.04
  • G++ v8.3.0
  • c++14

【问题讨论】:

  • 通常你需要一个vpackuswb + shuffle 来解释车道交叉。或者首先使用 _mm256_setzero_si256() 解压缩 lo/hi 一对 256 位向量,这样重新打包就在通道内 vpackuswb
  • 或者为了节省指令,你可能想要交错(?)来设置vpmaddubsw,如果你能做到的话。 (它将一个输入视为有符号,另一个视为无符号,因此对于像素 * alpha 来说它不会很容易工作,除非可能将范围转移到有符号然后调整?但饱和度是一个问题,所以不,我不这么想)。
  • 感谢指点!前一个选项似乎更有吸引力,但我会看看。

标签: c++ simd intrinsics avx avx2


【解决方案1】:

通常,如果您需要将结果重新打包为 8 位整数,最好使用 punpcklbw/punpckhbw 将结果解包为零,然后使用 packuswb 重新打包结果。或者有时您可以将奇数和偶数字节屏蔽到单独的寄存器中,一起进行计算和位或结果。

_mm256_cvtepu8_epi16/vpmovzxbw 的“问题”是它是车道交叉的(即,它只从低 128 位半(或内存)获取输入,但结果在高位和低位一半),并且没有(简单的)解决方案将来自不同通道的 16 位值连接回一个(直到 AVX512 通道交叉单寄存器包指令具有饱和或截断)。

在您的情况下,您实际上可以将ds 值打包在一个寄存器中,将a255-a 值打包在另一个寄存器中,并使用vpmaddubsw 进行乘法和加法。在将 ds 值打包在一起之前,您需要从它们中减去 128,因为一个参数需要是带符号的 int8。结果将偏离128*255,但可以得到补偿,特别是如果您无论如何添加127 进行舍入。 (如果你不这样做,你可以在除法(有符号除法,向下舍入)和重新打包后将每个字节加 128。

未经测试的代码,使用与您的尝试相同的签名:

// https://stackoverflow.com/questions/35285324/how-to-divide-16-bit-integer-by-255-with-using-sse
inline __m256i div255_epu16(__m256i x) {
    __m256i mulhi = _mm256_mulhi_epu16(x, _mm256_set1_epi16(0x8081));
    return _mm256_srli_epi16(mulhi, 7);
}

int blend_plane_pixels_444_vectorized(uint8_t *__restrict__ d,
                                      uint8_t *__restrict__ s,
                                      uint8_t *__restrict__ sa,
                                      const int pixels)
{
    int n = 0; // Return number of component pixels processed.
    for (int k = 0; k + 32 <= pixels; k += 32)
    {
        // Load 32 (unaligned) of d, s, sa
        __m256i vecD = _mm256_loadu_si256((__m256i_u *)d);
        __m256i vecS = _mm256_loadu_si256((__m256i_u *)s );
        __m256i vecA = _mm256_loadu_si256((__m256i_u *)sa);

        // subtract 128 from D and S to have them in the signed domain
        // subtracting 128 is equivalent ot xor with 128
        vecD = _mm256_xor_si256(vecD, _mm256_set1_epi8(0x80));
        vecS = _mm256_xor_si256(vecS, _mm256_set1_epi8(0x80));

        // calculate 255-a (equivalent to 255 ^ a):
        __m256i vecA_ = _mm256_xor_si256(vecA, _mm256_set1_epi8(0xFF));

        __m256i vecAA_lo = _mm256_unpacklo_epi8(vecA, vecA_);
        __m256i vecSD_lo = _mm256_unpacklo_epi8(vecS, vecD);
        __m256i vecAA_hi = _mm256_unpackhi_epi8(vecA, vecA_);
        __m256i vecSD_hi = _mm256_unpackhi_epi8(vecS, vecD);

        // R = a * (s-128) + (255-a)*(d-128) = a*s + (255-a)*d - 128*255
        __m256i vecR_lo = _mm256_maddubs_epi16(vecAA_lo,vecSD_lo);
        __m256i vecR_hi = _mm256_maddubs_epi16(vecAA_hi,vecSD_hi);

        // shift back to unsigned domain and add 127 for rounding
        vecR_lo = _mm256_add_epi16(vecR_lo, _mm256_set1_epi16(127+128*255));
        vecR_hi = _mm256_add_epi16(vecR_hi, _mm256_set1_epi16(127+128*255));

        // divide (rounding down)
        vecR_lo = div255_epu16(vecR_lo);
        vecR_hi = div255_epu16(vecR_hi);

        // re-join lower and upper half:
        __m256i vecResult = _mm256_packus_epi16(vecR_lo, vecR_hi);
        // Write data back to memory (unaligned)
        _mm256_storeu_si256((__m256i*)d, vecResult);

        d += 32;
        s += 32;
        sa += 32;
        n += 32;
    }

    return n;
}

神栓链接:https://godbolt.org/z/EYzLw2 请注意,-march=haswell 或您想要支持的任何架构都至关重要,因为否则 gcc 将不会使用未对齐的数据作为内存源操作数。当然,通用矢量化规则适用,即,如果您可以控制对齐,则更喜欢分配对齐的数据。如果没有,您可以剥离第一个未对齐的字节(例如,从d)以至少有一个加载和存储对齐。

Clang 将展开循环(到两个内部迭代),这将略微提高足够大输入的性能。

【讨论】:

  • 请注意pmaddubsw 可能会使SaturateToSignedWord(SRC[15-8]* DEST[15-8]+SRC[7-0]*DEST[7-0]) 饱和,因此它可以处理您的范围移位无符号数据,不是吗?不过,我并没有仔细考虑过那个角落案例,看看它是否真的安全或可取。
  • 谢谢@chtz!很棒的答案。当您说“gcc 不会使用未对齐的数据作为内存源操作数”时,您能详细说明一下吗?我打算使用 'attribute ((target ("avx2")))' 来确保它只被支持 AVX2 的 CPU 调用。这还不够?
  • @PeterCordes 我不排除我错过了一个极端情况,但我认为这不会饱和(我确实检查了所有边界情况)。如果 a + a_ &gt; 255 可能会饱和,但总和等于 255。
  • @ZeroDefect:启用 AVX 后,GCC 可以将未对齐的负载折叠到 SIMD 指令的内存源操作数中。它可能会也可能不会总是选择这样做,具体取决于-march= 暗示的调整选项。哦,但是对于 256 位未对齐的加载,请使用 Haswell,因为由于 Sandybridge,tune=generic 默认是拆分 256 位未对齐的加载/存储。
  • @ZeroDefect: Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd? 解释了为什么您肯定需要 -march=haswell(或者可能是 -march=znver1)作为您的 AVX2 代码的详细信息。遗憾的是没有“generic-avx2”调优。
猜你喜欢
  • 2021-10-19
  • 2015-06-25
  • 2012-10-31
  • 2016-12-05
  • 2021-06-23
  • 1970-01-01
  • 2015-01-23
  • 2015-04-24
  • 1970-01-01
相关资源
最近更新 更多