自动矢量化随机播放指令
Posted
技术标签:
【中文标题】自动矢量化随机播放指令【英文标题】:Auto-vectorize shuffle instruction 【发布时间】:2019-02-02 01:08:39 【问题描述】:我正在尝试让编译器通过自动矢量化生成 (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 的明确幂,应该保证指向表的指针没有别名,应该明确表示对齐等。 在这种情况下,我没有找到任何类似的原因,并且我仍然坚持使用手动内在函数来生成合理的程序集。
有没有办法仅使用普通代码并依赖编译器的自动矢量化器来生成 (v)pshufd
指令?
【问题讨论】:
问题出在哪里? 如何使用普通代码生成(v)pshufd
您的意思是在您的示例中使用两个不同的变量名称吗?
对我来说,您似乎在问为什么您的 C 代码没有向量化到特定指令。给出该示例的答案是,您的 C 代码不会做与指令相同的事情。也许我错过了一些东西。 耸耸肩
我知道它并不能完全回答这个问题,但是使用__builtin_shuffle
和/或__builtin_shufflevector
来至少加速 GCC/clang 怎么样?请小心,因为 ICC 不支持 __builtin_shuffle
,即使它声称是 GCC 的版本(或者至少在我上次检查时没有)。 github.com/nemequ/simde/blob/master/simde/simde-common.h#L173 如果你想要一个宏。
【参考方案1】:
(更新:自 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) shuffle 的情况。
示例 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];
gcc -m64 -O3 -march=skylake
shuff5
的主循环包含
车道交叉vpermd
shuffle 指令,我认为这非常令人印象深刻。
函数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)。这有时会稍微改进汇编代码的生成。
【讨论】:
呃,-march=skylake
,gcc 与vmovd
或vextractps
一起发狂到 EAX,vinsertps
。和vpinsrd
。 godbolt.org/z/-bkA2G clang 仍然使用 vmovsldup
和 vpermilps
作为前 2 个,但是对于没有本地向量的那个 clang 仍然使用像 gcc 这样的标量。愚蠢的编译器。
好戏法!当然,它可以工作,如程序集中所示。但是使用“正常”指令而不是特定指令编写算法的全部意义在于 1- 保持代码可移植性,以及 2- 允许任意适应目标功能。例如,让代码转换为 AVX 的 256 位或 AVX512 的 512 位。不幸的是,__attribute__((vector_size(16)))
似乎使这两个目标都变得困难。
@Cyan:你是对的。这就是我写的原因:“但可能不是您正在寻找的方式”。我已经用“正常”指令进行了其他尝试,但不幸的是,它们都没有成功。即使是一个简单的for
循环,交换偶数和奇数条目,也不会向量化。但请注意,使用typedef float v16sf __attribute__((vector_size(64)));
,clang 甚至可以生成vpermps zmm0, zmm1, zmm0
。我很惊讶没有其他人提出更好的解决方案。
clang 以外的编译器通常在自动矢量化随机播放方面非常糟糕。我还没有真正试图找到答案。
@PeterCordes:在这些示例中,我很惊讶 gcc 实际上比 clang 表现更好。但总的来说,shuffle 的自动矢量化确实相当不可靠。以上是关于自动矢量化随机播放指令的主要内容,如果未能解决你的问题,请参考以下文章