自动矢量化洗牌指令
Auto-vectorize shuffle instruction
我试图让编译器通过自动向量化生成 (v)pshufd
指令(或等效指令)。出乎意料的难。
例如,假设一个包含 4 uint32
个值的向量,转换:
A|B|C|D => A|A|C|C
应该使用单个指令来实现(对应的内在:_mm_shuffle_epi32()
)。
尝试仅使用普通操作来表达相同的转换,我可以这样写:
for (i=0; i<4; i+=2)
v32x4[i] = v32x4[i+1];
编译器似乎无法进行良好的转换,而是生成十多条指令的标量和矢量代码混合。
手动展开会产生更糟糕的结果。
有时,一些细节会妨碍编译器正确翻译。例如,数组中元素的 nb 应该是 2 的明确幂,指向 table 的指针应该保证不别名,应该明确表示对齐等。
在这种情况下,我没有找到任何类似的原因,我仍然坚持使用手动内部函数来生成合理的程序集。
有没有办法仅使用普通代码并依赖编译器的自动向量化器来生成 (v)pshufd
指令?
(更新:自 2019-02-07 以来的新答案。)
可以让编译器生成 (v)pshufd
指令,即使没有我在
previous answer to this question。
以下示例给出了可能性的印象。
这些示例是使用 gcc 8.2 和 clang 7 编译的。
示例 1
#include<stdint.h>
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff1(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
for (int32_t i = 0; i < n; i=i+4) {
b[i+0] = a[i+0];
b[i+1] = a[i+0];
b[i+2] = a[i+2];
b[i+3] = a[i+2];
}
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem Yes */
/* clang -m64 -O3 -march=skylake Yes */
void shuff2(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
for (int32_t i = 0; i < n; i=i+4) {
b[i+0] = a[i+1];
b[i+1] = a[i+2];
b[i+2] = a[i+3];
b[i+3] = a[i+0];
}
}
令人惊讶的是,clang 仅在数学意义上向量化排列,
不是一般的洗牌。 gcc -m64 -O3 -march=nehalem
,
shuff1
的主循环变为:
.L3:
add edx, 1
pshufd xmm0, XMMWORD PTR [rdi+rax], 160
movaps XMMWORD PTR [rsi+rax], xmm0
add rax, 16
cmp edx, ecx
jb .L3
示例 2
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem No */
/* gcc -m64 -O3 -march=skylake No */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff3(int32_t* restrict a, int32_t* restrict b){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
b[0] = a[0];
b[1] = a[0];
b[2] = a[2];
b[3] = a[2];
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem Yes */
/* clang -m64 -O3 -march=skylake Yes */
void shuff4(int32_t* restrict a, int32_t* restrict b){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
b[0] = a[1];
b[1] = a[2];
b[2] = a[3];
b[3] = a[0];
}
gcc -m64 -O3 -march=skylake
的程序集:
shuff3:
mov eax, DWORD PTR [rdi]
mov DWORD PTR [rsi], eax
mov DWORD PTR [rsi+4], eax
mov eax, DWORD PTR [rdi+8]
mov DWORD PTR [rsi+8], eax
mov DWORD PTR [rsi+12], eax
ret
shuff4:
vpshufd xmm0, XMMWORD PTR [rdi], 57
vmovaps XMMWORD PTR [rsi], xmm0
ret
同样,(0,3,2,1) 排列的结果与 (2,2,0,0) 随机排列的结果有本质区别。
示例 3
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff5(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
for (int32_t i = 0; i < n; i=i+8) {
b[i+0] = a[i+2];
b[i+1] = a[i+7];
b[i+2] = a[i+7];
b[i+3] = a[i+7];
b[i+4] = a[i+0];
b[i+5] = a[i+1];
b[i+6] = a[i+5];
b[i+7] = a[i+4];
}
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff6(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
for (int32_t i = 0; i < n; i=i+8) {
b[i+0] = a[i+0];
b[i+1] = a[i+0];
b[i+2] = a[i+2];
b[i+3] = a[i+2];
b[i+4] = a[i+4];
b[i+5] = a[i+4];
b[i+6] = a[i+6];
b[i+7] = a[i+6];
}
}
WIth gcc -m64 -O3 -march=skylake
shuff5
的主循环包含
车道交叉 vpermd
洗牌指令,我认为这非常令人印象深刻。
函数shuff6
引出非车道穿越vpshufd ymm0, mem
指令,完美。
示例 4
如果我们替换 b[i+5] = a[i+1];
,shuff5
的汇编会变得相当混乱
通过 b[i+5] = 0;
。然而,循环是矢量化的。另请参阅此 Godbolt link
对于此答案中讨论的所有示例。
如果数组 a
和 b
是 16(或 32)字节对齐的,那么我们可以使用
a = (int32_t*)__builtin_assume_aligned(a, 16);
b = (int32_t*)__builtin_assume_aligned(b, 16);
(或 32 而不是 16)。这有时会稍微改进汇编代码生成。
我试图让编译器通过自动向量化生成 (v)pshufd
指令(或等效指令)。出乎意料的难。
例如,假设一个包含 4 uint32
个值的向量,转换:
A|B|C|D => A|A|C|C
应该使用单个指令来实现(对应的内在:_mm_shuffle_epi32()
)。
尝试仅使用普通操作来表达相同的转换,我可以这样写:
for (i=0; i<4; i+=2)
v32x4[i] = v32x4[i+1];
编译器似乎无法进行良好的转换,而是生成十多条指令的标量和矢量代码混合。 手动展开会产生更糟糕的结果。
有时,一些细节会妨碍编译器正确翻译。例如,数组中元素的 nb 应该是 2 的明确幂,指向 table 的指针应该保证不别名,应该明确表示对齐等。 在这种情况下,我没有找到任何类似的原因,我仍然坚持使用手动内部函数来生成合理的程序集。
有没有办法仅使用普通代码并依赖编译器的自动向量化器来生成 (v)pshufd
指令?
(更新:自 2019-02-07 以来的新答案。)
可以让编译器生成 (v)pshufd
指令,即使没有我在
previous answer to this question。
以下示例给出了可能性的印象。
这些示例是使用 gcc 8.2 和 clang 7 编译的。
示例 1
#include<stdint.h>
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff1(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
for (int32_t i = 0; i < n; i=i+4) {
b[i+0] = a[i+0];
b[i+1] = a[i+0];
b[i+2] = a[i+2];
b[i+3] = a[i+2];
}
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem Yes */
/* clang -m64 -O3 -march=skylake Yes */
void shuff2(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
for (int32_t i = 0; i < n; i=i+4) {
b[i+0] = a[i+1];
b[i+1] = a[i+2];
b[i+2] = a[i+3];
b[i+3] = a[i+0];
}
}
令人惊讶的是,clang 仅在数学意义上向量化排列,
不是一般的洗牌。 gcc -m64 -O3 -march=nehalem
,
shuff1
的主循环变为:
.L3:
add edx, 1
pshufd xmm0, XMMWORD PTR [rdi+rax], 160
movaps XMMWORD PTR [rsi+rax], xmm0
add rax, 16
cmp edx, ecx
jb .L3
示例 2
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem No */
/* gcc -m64 -O3 -march=skylake No */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff3(int32_t* restrict a, int32_t* restrict b){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
b[0] = a[0];
b[1] = a[0];
b[2] = a[2];
b[3] = a[2];
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem Yes */
/* clang -m64 -O3 -march=skylake Yes */
void shuff4(int32_t* restrict a, int32_t* restrict b){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 16); b = (int32_t*)__builtin_assume_aligned(b, 16);
b[0] = a[1];
b[1] = a[2];
b[2] = a[3];
b[3] = a[0];
}
gcc -m64 -O3 -march=skylake
的程序集:
shuff3:
mov eax, DWORD PTR [rdi]
mov DWORD PTR [rsi], eax
mov DWORD PTR [rsi+4], eax
mov eax, DWORD PTR [rdi+8]
mov DWORD PTR [rsi+8], eax
mov DWORD PTR [rsi+12], eax
ret
shuff4:
vpshufd xmm0, XMMWORD PTR [rdi], 57
vmovaps XMMWORD PTR [rsi], xmm0
ret
同样,(0,3,2,1) 排列的结果与 (2,2,0,0) 随机排列的结果有本质区别。
示例 3
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff5(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
for (int32_t i = 0; i < n; i=i+8) {
b[i+0] = a[i+2];
b[i+1] = a[i+7];
b[i+2] = a[i+7];
b[i+3] = a[i+7];
b[i+4] = a[i+0];
b[i+5] = a[i+1];
b[i+6] = a[i+5];
b[i+7] = a[i+4];
}
}
/* vectorizes */
/* gcc -m64 -O3 -march=nehalem Yes */
/* gcc -m64 -O3 -march=skylake Yes */
/* clang -m64 -O3 -march=nehalem No */
/* clang -m64 -O3 -march=skylake No */
void shuff6(int32_t* restrict a, int32_t* restrict b, int32_t n){
/* this line is optional */ a = (int32_t*)__builtin_assume_aligned(a, 32); b = (int32_t*)__builtin_assume_aligned(b, 32);
for (int32_t i = 0; i < n; i=i+8) {
b[i+0] = a[i+0];
b[i+1] = a[i+0];
b[i+2] = a[i+2];
b[i+3] = a[i+2];
b[i+4] = a[i+4];
b[i+5] = a[i+4];
b[i+6] = a[i+6];
b[i+7] = a[i+6];
}
}
WIth gcc -m64 -O3 -march=skylake
shuff5
的主循环包含
车道交叉 vpermd
洗牌指令,我认为这非常令人印象深刻。
函数shuff6
引出非车道穿越vpshufd ymm0, mem
指令,完美。
示例 4
如果我们替换 b[i+5] = a[i+1];
,shuff5
的汇编会变得相当混乱
通过 b[i+5] = 0;
。然而,循环是矢量化的。另请参阅此 Godbolt link
对于此答案中讨论的所有示例。
如果数组 a
和 b
是 16(或 32)字节对齐的,那么我们可以使用
a = (int32_t*)__builtin_assume_aligned(a, 16);
b = (int32_t*)__builtin_assume_aligned(b, 16);
(或 32 而不是 16)。这有时会稍微改进汇编代码生成。