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

Store, modify and retrieve strings with GCC Vector Extensions?

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 扩展是否支持这种转换并对其进行了良好定义?

这是对字符串使用矢量扩展的最有效方法吗?

或者我是否必须明确地将字符串的 store/retrieve 部分放入向量中?

例如像这样:

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);
    }
}

或者有 better/more 比 memcpy 更有效的方法吗?

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

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

如果自动矢量化不能生成好的代码,标准技术是使用向量内在函数。如果您可以使用可以在多种体系结构上编译为 SIMD 指令的操作来完成您需要的操作,那么是的,gcc 向量语法可能是一个不错的方法。

我用 gcc 4.9.2 试用了你的第一个版本。它使用 64 位 AVX 生成您所希望的。 (256 位加载、矢量和存储)。

没有 -march 或任何东西,仅使用基线 amd64 (SSE2),它将输入复制到堆栈上的缓冲区,并从那里加载。我认为它是在 input/output 缓冲区未对齐的情况下这样做的,而不是仅仅使用 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    , %rdi
        vmovdqa %ymm0, (%rdx)
        addq    , %rdx
        cmpq    %rdi, %rsi
        ja      .L3
        vzeroupper
.L6:
        ret

请注意缺少标量清理或未对齐数据的处理。 vmovdqu在地址对齐的时候和vmovdqa一样快,所以不用它有点傻

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    , %rdi
        addq    , %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 字节,然后是矢量循环,然后是需要的任何标量清理。对于小缓冲区,最好只使用未对齐的 loads/stores 和最后的简单标量清理。

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