使用 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 结尾(隐式长度),则在跨越页面边界时必须小心,如果字符串在页面末尾之前结束,您不会出错, 但你的阅读跨越了边界。
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 结尾(隐式长度),则在跨越页面边界时必须小心,如果字符串在页面末尾之前结束,您不会出错, 但你的阅读跨越了边界。