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
?
当假设输入或输出缓冲区(或两者)未对齐时,如何安全/有效地使用向量扩展进行字符串处理?
答案 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
注意缺少标量清理或未对齐数据的处理。当地址对齐时,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结尾的(隐式长度),你必须小心跨越页面边界,如果字符串在页面结束之前结束,你就不会出错;但是你的阅读跨越边界。