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

时间:2019-01-17 10:28:30

标签: c++ c intel intrinsics

我的原始数据是一串c数组(长度为1000000)的(无符号)char(8位)。 我想将它们加在一起(矢量加法),遵循以下代码中的规则。 结果: (无符号)短(16位)的c数组。

我已经阅读了所有SSE和AVX / AVX2,但那里只有一个类似的电话 2个256bit的寄存器。前4个32位将相乘,每对32位的结果是一个64位,将适合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]);
}

谢谢

3 个答案:

答案 0 :(得分:4)

实际上,注释是正确的:编译器可以为您进行矢量化。 我对您的代码进行了一些修改,以改善自动矢量化。 对于gcc -O3 -march=haswell -std=c++14(gcc版本8.2),以下代码:

#include <cstdint>
#include <immintrin.h>

void cvt_uint8_int16(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
    int64_t i;
    /* If you know that n is always a multiple of 32 then insert       */
    /* n = n & 0xFFFFFFFFFFFFFFE0u;                                    */
    /* This leads to cleaner code. Now assume n is a multiple of 32:   */
    count = count & 0xFFFFFFFFFFFFFFE0u;                               
    for (i = 0; i < count; i++){
        canvas[i] += static_cast<uint16_t>(addon[i]);
    }
}

编译为:

cvt_uint8_int16(unsigned short*, unsigned char*, long):
        and     rdx, -32
        jle     .L5
        add     rdx, rsi
.L3:
        vmovdqu ymm2, YMMWORD PTR [rsi]
        add     rsi, 32
        add     rdi, 64
        vextracti128    xmm1, ymm2, 0x1
        vpmovzxbw       ymm0, xmm2
        vpaddw  ymm0, ymm0, YMMWORD PTR [rdi-64]
        vpmovzxbw       ymm1, xmm1
        vpaddw  ymm1, ymm1, YMMWORD PTR [rdi-32]
        vmovdqu YMMWORD PTR [rdi-64], ymm0
        vmovdqu YMMWORD PTR [rdi-32], ymm1
        cmp     rdx, rsi
        jne     .L3
        vzeroupper
.L5:

编译器Clang产生的code有点不同:它加载128位(字符)向量,并用vpmovzxbw进行转换。 编译器gcc加载256位(char)向量,并转换高128位和低128位 分别,效率可能会略低。 但是,无论如何,您的问题可能是带宽有限(因为长度> 1000000)。

您还可以使用内在函数对代码进行矢量化(未测试):

void cvt_uint8_int16_with_intrinsics(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
    int64_t i;
    /* Assume n is a multiple of 16  */
    for (i = 0; i < count; i=i+16){
        __m128i x     = _mm_loadu_si128((__m128i*)&addon[i]);
        __m256i y     = _mm256_loadu_si256((__m256i*)&canvas[i]);
        __m256i x_u16 = _mm256_cvtepu8_epi16(x);
        __m256i sum   = _mm256_add_epi16(y, x_u16);
                _mm256_storeu_si256((__m256i*)&canvas[i], sum);
    }
}

这导致与results类似的自动矢量化代码。

答案 1 :(得分:4)

添加到@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中的任何一个都要严格。因此,如果您想获得绝对的性能下降(并且具有固定的体系结构),则可能会出现上述情况。 但是的确是很小的改进,因为编译器已经可以非常完美地处理此问题了,所以我实际上建议不要这样做,而应该信任编译器。

如果您确实认为可以改善编译器,请记住始终进行测试和配置文件以确保您确实是。

答案 2 :(得分:3)

与wim和Mike的出色答案中介绍的手动优化方法相反,让我们快速了解一下完全原始的C ++实现将为我们带来什么:

std::transform(addon, addon + count, canvas, canvas, std::plus<void>());

Try it out here。您会发现,即使您不花力气,编译器也已经能够生成很好的矢量化代码,因为它不能对缓冲区的对齐方式和大小做任何假设,而且还存在一些潜在的别名问题(由于使用了uint8_t,不幸的是,这迫使编译器假定指针可以别名为任何其他对象。另外,请注意,该代码与从C样式实现中获得的代码基本相同(取决于编译器,C ++版本的指令多或少。)

void f(uint16_t* canvas, const uint8_t* addon, size_t count)
{
    for (size_t i = 0; i < count; ++i)
        canvas[i] += addon[i];
}

但是,只要可以添加元素类型,通用C ++解决方案就可以对不同种类的容器和元素类型进行任意组合。因此,正如在其他答案中所指出的那样,虽然当然有可能通过手动优化获得稍微更有效的实现,但是仅通过编写简单的C ++代码(如果做得正确)就可以走很长一段路。在诉诸于手动编写SSE内部函数之前,请考虑通用C ++解决方案更灵活,更易于维护,尤其是更具可移植性。通过简单切换目标体系结构,您不仅可以为SSE生成质量相似的代码,还可以为AVX甚至是带有NEON的ARM以及可能碰巧要运行的任何其他指令集生成代码。如果您需要代码在一个特定的CPU上完美到最后一个指令的最后一条指令,那么是的,内在函数甚至是内联汇编都是可行的方法。但总的来说,我还建议改用以使能和指导编译器生成所需程序集的方式来编写C ++代码,而不是自己生成程序集。例如,通过使用(非标准但普遍可用的)限制限定符并通过使编译器知道您的count始终是32的倍数来借用该技巧

void f(std::uint16_t* __restrict__ canvas, const std::uint8_t* __restrict__ addon, std::size_t count)
{
    assert(count % 32 == 0);
    count = count & -32;
    std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
}

you get-std=c++17 -DNDEBUG -O3 -mavx

f(unsigned short*, unsigned char const*, unsigned long):    
        and     rdx, -32
        je      .LBB0_3
        xor     eax, eax
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
        vpmovzxbw       xmm0, qword ptr [rsi + rax] # xmm0 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm1, qword ptr [rsi + rax + 8] # xmm1 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm2, qword ptr [rsi + rax + 16] # xmm2 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm3, qword ptr [rsi + rax + 24] # xmm3 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpaddw  xmm0, xmm0, xmmword ptr [rdi + 2*rax]
        vpaddw  xmm1, xmm1, xmmword ptr [rdi + 2*rax + 16]
        vpaddw  xmm2, xmm2, xmmword ptr [rdi + 2*rax + 32]
        vpaddw  xmm3, xmm3, xmmword ptr [rdi + 2*rax + 48]
        vmovdqu xmmword ptr [rdi + 2*rax], xmm0
        vmovdqu xmmword ptr [rdi + 2*rax + 16], xmm1
        vmovdqu xmmword ptr [rdi + 2*rax + 32], xmm2
        vmovdqu xmmword ptr [rdi + 2*rax + 48], xmm3
        add     rax, 32
        cmp     rdx, rax
        jne     .LBB0_2
.LBB0_3:
        ret

这还不错...