使用 GCC 向量扩展存储、修改和检索字符串?

Posted

技术标签:

【中文标题】使用 GCC 向量扩展存储、修改和检索字符串?【英文标题】:Store, modify and retrieve strings with GCC Vector Extensions? 【发布时间】:2015-06-11 07:33:50 【问题描述】:

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更好/更有效的方法吗?

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

【问题讨论】:

对齐的大小写应该没问题,但是您是否检查过 gcc 的矢量化器是否在没有明确使用矢量的情况下还没有生成合适的代码? @MarcGlisse,上面的遮罩只是一个玩具示例。实际上,我想对字符串缓冲区中的每个向量大小的增量应用一系列操作(按字节移位、字节混洗……)。 类似问题:***.com/questions/9318115/… 【参考方案1】:

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

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

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

请注意缺少标量清理或未对齐数据的处理。 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    $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 终止的(隐式长度),则在跨越页面边界时必须小心,如果字符串在页面结束之前结束,您不会出错,但是你的阅读跨越了边界。

【讨论】:

以上是关于使用 GCC 向量扩展存储、修改和检索字符串?的主要内容,如果未能解决你的问题,请参考以下文章

从向量中检索字符标签

GCC 向量扩展和 ARM NEON 的内存对齐问题

为啥我不能从数据库中保存和检索我的向量(二进制)和特殊字符?

C - 如何使用 GCC SSE 向量扩展访问向量的元素

xmmintrin.h 与 gcc 向量扩展

使用 Entity Framework Core 将数据存储和检索为 JSON 字符串?