c++ - 将大型 char8 c 数组转换为 short16 的最快方法是什么?

标签 c++ c intel intrinsics

我的原始数据是一堆长度 > 1000000 的(无符号)字符(8 位)c 数组。 我想按照下面代码中的规则将它们加在一起( vector 加法)。 结果: (无符号)短(16 位)的 c 数组。

我已经阅读了所有 SSE 和 AVX/AVX2 但只有一个类似的调用 那多个256位的寄存器。前 4 个 32bit 将相乘,每对 32bit 的结果是一个 64bit 将放入 256 寄存器。( _mm256_mul_epi32, _mm256_mul_epu32)

Firgure

https://www.codeproject.com/Articles/874396/Crunching-Numbers-with-AVX-and-AVX

示例代码:

static inline void adder(uint16_t *canvas, uint8_t *addon, uint64_t count)
{
    for (uint64_t i=0; i<count; i++)
        canvas[i] += static_cast<uint16_t>(addon[i]);
}

谢谢

最佳答案

添加到@wim 答案(这是一个答案)并考虑@Bathsheba 评论,值得信任编译器也检查您的编译器输出既可以学习如何执行此操作,也可以检查它是否按照您的意愿进行。通过 godbolt 运行代码的略微修改版本(对于 msvc、gcc 和 clang)给出了一些不完美的答案。

如果您将自己限制在 SSE2 及低于此答案假定的值(以及我测试的内容),则尤其如此

所有编译器都对代码进行矢量化和展开,并使用 punpcklbwuint8_t“解压”为 uint16_t,然后运行SIMD 添加和保存。这很好。但是,MSVC 往往会在内部循环中出现不必要的溢出,而 clang 仅使用 punpcklbw 而不是 punpckhbw 这意味着它会加载源数据两次。 GCC 正确处理了 SIMD 部分,但循环约束的开销更高。

因此从理论上讲,如果您想改进这些版本,您可以使用类似于以下内容的内部函数来推出自己的版本:

static inline void adder2(uint16_t *canvas, uint8_t *addon, uint64_t count)
{
    uint64_t count32 = (count / 32) * 32;
    __m128i zero = _mm_set_epi32(0, 0, 0, 0);
    uint64_t i = 0;
    for (; i < count32; i+= 32)
    {
        uint8_t* addonAddress = (addon + i);

        // Load data 32 bytes at a time and widen the input
        // to `uint16_t`'sinto 4 temp xmm reigsters.
        __m128i input = _mm_loadu_si128((__m128i*)(addonAddress + 0));
        __m128i temp1 = _mm_unpacklo_epi8(input, zero);
        __m128i temp2 = _mm_unpackhi_epi8(input, zero);
        __m128i input2 = _mm_loadu_si128((__m128i*)(addonAddress + 16));
        __m128i temp3 = _mm_unpacklo_epi8(input2, zero);
        __m128i temp4 = _mm_unpackhi_epi8(input2, zero);

        // Load data we need to update
        uint16_t* canvasAddress = (canvas + i);
        __m128i canvas1 = _mm_loadu_si128((__m128i*)(canvasAddress + 0));
        __m128i canvas2 = _mm_loadu_si128((__m128i*)(canvasAddress + 8));
        __m128i canvas3 = _mm_loadu_si128((__m128i*)(canvasAddress + 16));
        __m128i canvas4 = _mm_loadu_si128((__m128i*)(canvasAddress + 24));

        // Update the values
        __m128i output1 = _mm_add_epi16(canvas1, temp1);
        __m128i output2 = _mm_add_epi16(canvas2, temp2);
        __m128i output3 = _mm_add_epi16(canvas3, temp3);
        __m128i output4 = _mm_add_epi16(canvas4, temp4);

        // Store the values
        _mm_storeu_si128((__m128i*)(canvasAddress + 0), output1);
        _mm_storeu_si128((__m128i*)(canvasAddress + 8), output2);
        _mm_storeu_si128((__m128i*)(canvasAddress + 16), output3);
        _mm_storeu_si128((__m128i*)(canvasAddress + 24), output4);
    }

    // Mop up
    for (; i<count; i++)
        canvas[i] += static_cast<uint16_t>(addon[i]);
}

检查它的输出,它比 gcc/clang/msvc 中的任何一个都要好。所以如果你想获得绝对的最后一滴性能(并且有一个固定的架构)那么像上面这样的东西是可能的。 但是这是一个非常小的改进,因为编译器已经几乎完美地处理了这个问题,所以我实际上建议不要这样做,而只是相信编译器。

如果您确实认为可以改进编译器,请记住始终进行测试和分析以确保您确实可以。

关于c++ - 将大型 char8 c 数组转换为 short16 的最快方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54233865/

相关文章:

c - 将比较例程转换为 Intel SIMD

performance - 关于超线程中 L1 Cache 的自适应模式

c++ - 类中的 Var 成员

c - C语言从文件中读取值

linux-kernel - 在进行系统调用时获取 'errno 38: function not implemented'

C 程序创建 3 个从 3 个不同文件读取的子进程

c - EOF 在...EOF 之前到达

c++ - 保留 `unique_ptr` 未设置,直到我准备好分配它 : Is this how I re-assign it?

c++ - 如何将数据推送到 C++ 中的第二个位置

c++ - 命名空间内的前向声明