自动矢量化随机播放指令

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=skylakeshuff5 的主循环包含 车道交叉vpermd shuffle 指令,我认为这非常令人印象深刻。 函数shuff6导致非车道交叉vpshufd ymm0, mem指令,完美。

示例 4

如果我们替换b[i+5] = a[i+1];shuff5 的组装会变得非常混乱 通过b[i+5] = 0;。然而,循环被矢量化了。另请参阅Godbolt link 对于此答案中讨论的所有示例。


如果数组ab 是16(或32)字节对齐的,那么我们可以使用 a = (int32_t*)__builtin_assume_aligned(a, 16);b = (int32_t*)__builtin_assume_aligned(b, 16); (或 32 而不是 16)。这有时会稍微改进汇编代码的生成。

【讨论】:

呃,-march=skylake,gcc 与vmovdvextractps 一起发狂到 EAX,vinsertps。和vpinsrd。 godbolt.org/z/-bkA2G clang 仍然使用 vmovsldupvpermilps 作为前 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 的自动矢量化确实相当不可靠。

以上是关于自动矢量化随机播放指令的主要内容,如果未能解决你的问题,请参考以下文章

c ++矢量随机洗牌部分

矢量化代码中随机减速的原因

NEON、SSE 和交错负载与随机播放

c# winform wmplayer简单播放器,实现自动下一首播放,随机播放,显示歌词,多选listbox

推荐一款手机端黑科技自动化脚本

用于矢量化的随机读取上的结构阵列 (AoS) 与阵列结构 (SoA)