如何使用avx2将24位rgb转换为32位?

时间:2018-02-11 12:21:49

标签: x86 rgb sse simd avx2

我用SSSE3完成了这个,现在我想知道是否可以用AVX2来实现更好的性能?

我使用Fast 24-bit array -> 32-bit array conversion?中的代码填充24位rgb,其中包含一个零字节。

    static const __m128i mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
    for (size_t row = 0; row < height; ++row)
    {
        for (size_t column = 0; column < width; column += 16)
        {
            const __m128i *src = reinterpret_cast<const __m128i *>(in + row * in_pitch + column + (column << 1));
            __m128i *dst = reinterpret_cast<__m128i *>(out + row * out_pitch + (column << 2));
            __m128i v[4];
            v[0] = _mm_load_si128(src);
            v[1] = _mm_load_si128(src + 1);
            v[2] = _mm_load_si128(src + 2);
            v[3] = _mm_shuffle_epi8(v[0], mask);
            _mm_store_si128(dst, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[1], v[0], 12), mask);
            _mm_store_si128(dst + 1, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[1], 8), mask);
            _mm_store_si128(dst + 2, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[2], 4), mask);
            _mm_store_si128(dst + 3, v[3]);
        }
    }

问题是_mm256_shuffle_epi8分别将高128位和低128位洗牌,因此掩码不能替换为

    _mm256_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1, 12, 13, 14, -1, 15, 16, 17, -1, 18, 19, 20, -1, 21, 22, 23, -1);

_mm_alignr_epi8也需要替换为_mm256_permute2x128_si256_mm256_alignr_epi8

2 个答案:

答案 0 :(得分:4)

使用AVX2可以合理有效地一次处理8个像素(24个输入字节和32个输出字节)。

你只需要对齐你的负载,这样你将处理的24字节像素块在32字节负载的中间居中,而不是通常的对齐负载的方法到像素块 2 的开始。这意味着通道边界将落在像素4和5之间,并且每个通道中的字节恰好为4个像素。结合适当的shuffle面罩,这应该是SSE的两倍。

例如:

给定一个输入指针uint8_t input[],你处理带有非SIMD代码 1 的前四个像素,然后在input[8]执行你的第一个32字节加载,这样低order lane(字节0-15)获得其高位字节中像素4,5,6,7的12个有效负载字节,紧接着是高通道中接下来的4个像素的下一个12个有效负载字节。然后使用pshufb将像素扩展到正确的位置(每个通道需要一个不同的掩码,因为你将低通道中的像素移向较低位置,而将高通道中的像素移动到较高位置,但这不会造成问题)。然后下一次加载将在input[26](24字节后),依此类推。

对于完美缓存的输入/输出,您应该获得每周期吞吐量大约8个像素的吞吐量 - 限制在1 /周期存储吞吐量和1 /周期随机吞吐量。幸运的是,这种方法与始终对齐的存储兼容(因为存储增量为32字节)。你会有一些未对齐的负载,但那些仍然可以在1 /周期发生,所以不应该成为瓶颈。

值得注意的是,就SIMD指令集扩展而言,这种类型的方法“只能工作一次”:当你有2个通道时,它可以工作,但不会更多(所以同样的想法不适用于512位AVX512有4个128位通道)。

1 这样可以避免在输入数组之前读出超出范围:如果您知道这是安全的,则可以避免此步骤。

2 也就是说,如果您从addr加载addr + 16,它应该位于像素边界((addr + 16) % 12 == 0),而不是addr }。

答案 1 :(得分:0)

这是原始的SSSE3代码,并抛出了我自己的一些调度信息。

void DspConvertPcm(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
    constexpr f32 fScale = static_cast<f32>(1.0 / (1<<23));

    size_t i = 0;
    size_t vecSampleCount = 0;

#if defined(SFTL_SSE2)
    if (CpuInfo::GetSupports_SIMD_I32x8())
    {
        vecSampleCount = DspConvertPcm_AVX2(pOutBuffer, pInBuffer, totalSampleCount);
    }
    else
    if (CpuInfo::GetSupports_SSE3())
    {
        const auto vScale = _mm_set1_ps(fScale);
        const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);

        constexpr size_t step = 16;
        vecSampleCount = (totalSampleCount / step) * step;

        for (; i < vecSampleCount; i += step)
        {
            const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer + i);
            auto* pDst = pOutBuffer + i;

            const auto sa = _mm_loadu_si128(pSrc + 0);
            const auto sb = _mm_loadu_si128(pSrc + 1);
            const auto sc = _mm_loadu_si128(pSrc + 2);

            const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
            const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
            const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb,  8), mask), 8);
            const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc,  4), mask), 8);

            //  Convert to float and store
            _mm_storeu_ps(pDst + 0,  _mm_mul_ps(_mm_cvtepi32_ps(da), vScale));
            _mm_storeu_ps(pDst + 4,  _mm_mul_ps(_mm_cvtepi32_ps(db), vScale));
            _mm_storeu_ps(pDst + 8,  _mm_mul_ps(_mm_cvtepi32_ps(dc), vScale));
            _mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScale));
        }
    }
#endif

    for (; i < totalSampleCount; i += 1)
    {
        pOutBuffer[i] = (static_cast<s32>(pInBuffer[i])) * fScale;
    }
}

如果存在AVX2,它将调用DspConvertPcm_AVX2,如下所示:

size_t DspConvertPcm_AVX2(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
    SFTL_ASSERT(CpuInfo::GetSupports_SIMD_I32x8());

    constexpr f32 fScale = static_cast<f32>(1.0 / (1 << 23));
    const auto vScale = _mm256_set1_ps(fScale);

    auto fnDo16Samples = [vScale](f32* pOutBuffer, const s24* pInBuffer)
    {
        const auto vScaleSSE = _mm256_castps256_ps128(vScale);
        const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);

        const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer);
        auto* pDst = pOutBuffer;

        const auto sa = _mm_loadu_si128(pSrc + 0);
        const auto sb = _mm_loadu_si128(pSrc + 1);
        const auto sc = _mm_loadu_si128(pSrc + 2);

        const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
        const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
        const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
        const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);

        //  Convert to float and store
        _mm_storeu_ps(pDst +  0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScaleSSE));
        _mm_storeu_ps(pDst +  4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScaleSSE));
        _mm_storeu_ps(pDst +  8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScaleSSE));
        _mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScaleSSE));
    };

    //  First 16 samples SSE style
    fnDo16Samples(pOutBuffer, pInBuffer);

    //  Next samples do AVX, where each load will discard 4 bytes at the start and end of each load
    constexpr size_t step = 16;
    const size_t vecSampleCount = ((totalSampleCount / step) * step) - 16;
    {
        const auto mask = _mm256_setr_epi8(-1, 4, 5, 6, -1, 7, 8, 9, -1, 10, 11, 12, -1, 13, 14, 15, -1, 16, 17, 18, -1, 19, 20, 21, -1, 22, 23, 24, -1, 25, 26, 27);
        for (size_t i = 16; i < vecSampleCount; i += step)
        {
            const byte* pByteBuffer = reinterpret_cast<const byte*>(pInBuffer + i);
            auto* pDst = pOutBuffer + i;

            const auto vs24_00_07 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer -  4));
            const auto vs24_07_15 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 24));

            const auto vf32_00_07 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_00_07, mask), 8);
            const auto vf32_07_15 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_07_15, mask), 8);

            //  Convert to float and store
            _mm256_storeu_ps(pDst + 0, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
            _mm256_storeu_ps(pDst + 8, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
        }
    }

    //  Last 16 samples SSE style
    fnDo16Samples(pOutBuffer + vecSampleCount, pInBuffer + vecSampleCount);

    return vecSampleCount;
}

请注意,我手动展开了AVX2主循环以尝试加快速度,但这并没有太大关系。

在调用DspConvertPcm之前附加了一个计时器,该计时器可以一次处理1024个样本,因此启用AVX2代码路径的平均处理时间在2.6到3.0微秒之间。另一方面,如果我禁用了AVX2代码路径,则平均时间将徘徊在2.0微秒左右。

另一方面,使用/ arch:AVX2启用VEX编码并没有像我之前声称的那样使我获得一致的性能提升,因此一定是偶然的。

此测试是使用Visual Studio 15.9.5上的默认MSVC编译器在2.6 GHz的Haswell核心i7-6700HQ上进行的,并启用了速度优化并使用/ fp:fast。