使用GCC Vector Extensions存储,修改和检索字符串?

时间:2015-06-11 07:33:50

标签: c gcc vectorization simd

GCC Vector Extensions提供了SIMD指令的抽象。

我想知道如何将它们用于字符串处理,例如掩盖缓冲区的每个字节:

typedef uint8_t v32ui __attribute__ ((vector_size(32)));

void f(const uint8_t *begin, const uint8_t *end, uint8_t *o)
{
    for (; begin < end; begin += 32, o+=32)
      *(v32ui*) o = (*(v32ui*) begin) & 0x0fu;
}

假设输入和输出缓冲区正确对齐(32字节),是否支持并使用GCC verctor扩展定义了这种转换?

这是在字符串上使用向量扩展的最有效方法吗?

或者我是否必须将部分字符串显式存储/检索到向量中?

例如:

void f(const uint8_t *begin, const uint8_t *end, uint8_t *o)
{
    for (; begin < end; begin += 32, o+=32) {
      v32ui t;
      memcpy(&t, begin, 32);
      t &= 0f0u;
      memcpy(o, &t, 32);
    }
}

或者有更好/更有效的方式而不是memcpy

当假设输入或输出缓冲区(或两者)未对齐时,如何安全/有效地使用向量扩展进行字符串处理?

1 个答案:

答案 0 :(得分:1)

向量需要在寄存器中处理,因此memcpy在这里不可能有用。

如果自动矢量化不能生成良好的代码,标准技术就是使用矢量内在函数。如果你可以通过可以在多种体系结构上编译成SIMD指令的操作来完成所需的操作,那么,gcc vector语法可能是一种很好的方法。

我用gcc 4.9.2尝试了你的第一个版本。使用64位AVX,它可以完全满足您的需求。 (256位加载,矢量和存储)。

如果没有-march或任何东西,只需使用基线amd64(SSE2),它就会将输入复制到堆栈上的缓冲区,并从那里加载。我认为它是在未对齐的输入/输出缓冲区的情况下执行此操作,而不是仅使用movdqu。无论如何,这是非常可怕的慢速代码,GP寄存器中每次执行8个字节的速度比这个废话更快。

gcc -march=native -O3 -S v32ui_and.c(在Sandybridge(没有AVX2的AVX)上):

        .globl  f
f:
        cmpq    %rsi, %rdi
        jnb     .L6
        vmovdqa .LC0(%rip), %ymm1  # load a vector of 0x0f bytes
        .p2align 4,,10
        .p2align 3
.L3:
        vandps  (%rdi), %ymm1, %ymm0
        addq    $32, %rdi
        vmovdqa %ymm0, (%rdx)
        addq    $32, %rdx
        cmpq    %rdi, %rsi
        ja      .L3
        vzeroupper
.L6:
        ret

注意缺少标量清理或未对齐数据的处理。当地址对齐时,vmovdquvmovdqa一样快,所以不使用它有点傻。

gcc -O3 -S v32ui_and.c很奇怪。

        .globl  f
f:
.LFB0:
        cmpq    %rsi, %rdi
        movdqa  .LC0(%rip), %xmm0  # load a vector of 0x0f bytes
        jnb     .L9
        leaq    8(%rsp), %r10
        andq    $-32, %rsp
        pushq   -8(%r10)
        pushq   %rbp
        movq    %rsp, %rbp
        pushq   %r10
        .p2align 4,,10
        .p2align 3
.L5:
        movq    (%rdi), %rax
        addq    $32, %rdi
        addq    $32, %rdx
        movq    %rax, -80(%rbp)
        movq    -24(%rdi), %rax
        movq    %rax, -72(%rbp)
        movq    -16(%rdi), %rax
        movdqa  -80(%rbp), %xmm1
        movq    %rax, -64(%rbp)
        movq    -8(%rdi), %rax
        pand    %xmm0, %xmm1
        movq    %rax, -56(%rbp)
        movdqa  -64(%rbp), %xmm2
        pand    %xmm0, %xmm2
        movaps  %xmm1, -112(%rbp)
        movq    -112(%rbp), %rcx
        movaps  %xmm2, -96(%rbp)
        movq    -96(%rbp), %rax
        movq    %rcx, -32(%rdx)
        movq    -104(%rbp), %rcx
        movq    %rax, -16(%rdx)
        movq    -88(%rbp), %rax
        movq    %rcx, -24(%rdx)
        movq    %rax, -8(%rdx)
        cmpq    %rdi, %rsi
        ja      .L5
        popq    %r10
        popq    %rbp
        leaq    -8(%r10), %rsp
.L9:
        rep ret

所以我猜你不能安全地使用gcc向量扩展,如果它有时会生成这么糟糕的代码。使用内在函数,最简单的实现是:

#include <immintrin.h>
#include <stdint.h>
void f(const uint8_t *begin, const uint8_t *end, uint8_t *o)
{
    __m256i mask = _mm256_set1_epi8(0x0f);
    for (; begin < end; begin += 32, o+=32) {
        __m256i s = _mm256_loadu_si256((__m256i*)begin);
        __m256i d = _mm256_and_si256(s, mask);
        _mm256_storeu_si256( (__m256i*)o, d);
    }
}

这会生成与gcc-vector版本相同的代码(使用AVX2编译)。请注意,这使用VPAND,而不是VANDPS,因此需要AVX2。

对于大缓冲区,在输入或输出缓冲区对齐到16或32字节,然后是向量循环,然后需要任何标量清理之前,值得进行标量启动。使用小缓冲区,最好只使用未对齐的加载/存储和最后的简单标量清理。

由于你特意询问了字符串,如果你的字符串是以nul结尾的(隐式长度),你必须小心跨越页面边界,如果字符串在页面结束之前结束,你就不会出错;但是你的阅读跨越边界。