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

Posted

技术标签:

【中文标题】如何使用 avx2 将 24 位 rgb 转换为 32 位?【英文标题】:How to transform 24bit rgb to 32bit using avx2? 【发布时间】:2018-02-11 12:21:49 【问题描述】:

我已经用 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

【问题讨论】:

您尝试过吗?如果是这样,请到目前为止发布您的代码。如果没有,那么也许您可以发布您现有的 SSE 代码作为起点? “24 位到 32 位”到底是什么意思?添加 alpha 组件?将每个通道的 8 位扩展到 11,10,11 或 10,12,10? 感谢 cmets 伙计们,我编辑了问题以添加详细信息。如果还不够清楚,请告诉我。 AVX2 的车道内特性意味着 SSSE3 pshufb 可能仍然是最佳选择。但是您应该考虑执行未对齐的加载,而不是使用_mm_alignr_epi8,因为现代英特尔将在每个时钟一个存储上出现瓶颈之前,每个时钟会出现瓶颈,而您的代码会在每个存储中执行多个洗牌。 @Mysticial - 我的测试显示重叠写入没有任何惩罚。当然,未对齐的写入与高速缓存行重叠可能会受到惩罚,但对于与较早写入重叠的写入没有特别的惩罚。这意味着您想要将一堆大小奇数的小字节段合并在一起,一系列重叠写入是一个很好的策略,并且每个周期运行一个段(如果每个段都适合一个寄存器,加上一些不可避免的惩罚)缓存线交叉)。 【参考方案1】:

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

您只需要对齐负载,以便您将处理的 24 字节像素块居中在 32 字节负载的中间,而不是通常的对齐负载的方法到像素块2的开头。这意味着 车道边界 将位于像素 4 和 5 之间,并且您将在每个车道中拥有正好 4 个像素的字节。结合适当的洗牌掩码,这应该是 SSE 效率的两倍。

例如:

给定一个输入指针uint8_t input[],您使用非 SIMD 代码处理前四个像素1,然后在 input[8] 处执行第一个 32 字节加载,以便低阶通道(字节0-15) 在其高位字节中获取像素 4、5、6、7 的 12 个有效负载字节,紧随其后的是高通道中接下来 4 个像素的下 12 个有效负载字节。然后您使用pshufb 将像素扩展到它们的正确位置(您需要为每个车道使用不同的蒙版,因为您将低车道中的像素移动到较低位置,而将高车道中的像素移动到较高位置,但是这不会造成问题)。然后下一次加载将在input[26](24 字节后)等等。

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

值得注意的是,这种类型的方法在 SIMD 指令集扩展方面“仅适用一次”:当您有 2 个通道时它有效,但不会更多(因此相同的想法不适用于 512 位 AVX512 4 个 128 位通道)。


1这样可以避免在输入数组之前读取越界:如果你知道这是安全的,你可以避免这一步。

2也就是说,如果您从 addr 加载,则应该在像素边界 ((addr + 16) % 12 == 0) 处的是 addr + 16,而不是 addr

【讨论】:

感谢您的回答。但是,虽然生成的输出确实给了我们正确的结果,但我实际上试了一下,它比 OP 引用的 SSE 版本慢了大约 50%。我猜这是因为它不可避免地会执行大多数未对齐的加载,并且它基本上会丢弃所有加载数据的 25%。我不推荐这种方法。 FWIW,通过使用相同的 SSE3 代码并简单地启用启用 vex 编码的 AVX 编译器标志,我实际上能够获得大约 40% 的性能提升。除非其他人拥有真正的 SSE3 版本的 AVX2 端口,否则这似乎会给我们带来最大的胜利。我们可以将 AVX 的车道边界限制归咎于英特尔。 @Kumputer - 什么类型的机器,你能分享你的代码和基准测试吗? 下面的答案中有更多详细信息。【参考方案2】:

这是原始的 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 编码并没有给我之前声称的一致的性能提升,所以这一定是侥幸。

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

【讨论】:

i7-6700HQ 是 Skylake,而不是 Haswell。无论如何,您是 sign-将 24 位扩展到 32 位。这是从 RGB 到 RGBA 或 RGB0 的不同问题,您想用固定值(在本例中为 0)填充 A 字节。此代码看起来很有用,但发布在错误的问题下。虽然我怀疑@BeeOnRope 进行未对齐负载的方法,所以每个srai 只需要一次洗牌会更好,尤其是在 Haswell/Skylake 上(1/时钟洗牌吞吐量,低于 IvyBridge 上的 2,但仍然是 2/时钟负载吞吐量) . 您已经为 AVX2 版本这样做了,看起来像,但不是 SSSE3,因此在只有 SSE4.2 的 Haswell/Skylake Pentium/Celeron CPU 上它会更慢。我还没有试图弄清楚在 SnB 或 Nehalem 上哪个会更快。具有缓慢未对齐负载的实际 Core 2(即使没有跨越缓存行边界)可能会受益于palignr,至少 Penryn,即使 Conroe 的洗牌速度很慢,也可能会受益于 Conroe。顺便说一句,您可以使用重叠向量代替标量清理,除非缓冲区大小可能小于一个展开的内循环。

以上是关于如何使用 avx2 将 24 位 rgb 转换为 32 位?的主要内容,如果未能解决你的问题,请参考以下文章

前端JS 实现将24位RGB颜色转换16位RGB颜色

前端JS 实现将24位RGB颜色转换16位RGB颜色

前端JS 实现将24位RGB颜色转换16位RGB颜色

如何从 24 位十六进制获取 RGB 值(不使用 .NET Framework)[重复]

如何在 AVX2 中将 32 位无符号整数转换为 16 位无符号整数?

C++ 中的 NV12 到 RGB24 转换代码