为啥此 SIMD 代码运行速度比等效标量慢?

Posted

技术标签:

【中文标题】为啥此 SIMD 代码运行速度比等效标量慢?【英文标题】:Why does this SIMD code run slower than scalar equivalent?为什么此 SIMD 代码运行速度比等效标量慢? 【发布时间】:2020-07-29 10:35:28 【问题描述】:

这是我做错了但我还不完全理解的那些 n00b 问题之一。

xxhash32 算法有一个不错的 16 字节内部循环,可以使用 SIMD 加快速度,因此,作为我自己的练习,这就是我想要做的。

循环体如下所示(numBytes 是 16 的倍数):

// C# that gets auto-vectorized.  uint4 is a vector of 4 elements
uint4 state = new uint4(Prime1 + Prime2, Prime2, 0, (uint)-Prime1) + seed;

int count = numBytes >> 4;
for (int i = 0; i < count; ++i) 
    state += *p++ * Prime2;
    state = (state << 13) | (state >> 19);
    state *= Prime1;


hash = rol(state.x, 1) + rol(state.y, 7) + rol(state.z, 12) + rol(state.w, 18);

我已将其翻译成以下 SSE2/SSE4.1 内在函数:

auto prime1 = _mm_set1_epi32(kPrime1);
auto prime2 = _mm_set1_epi32(kPrime2);

auto state = _mm_set_epi32(seed + kPrime1 + kPrime2, seed + kPrime2, seed, seed - kPrime1);

int32_t count = size >> 4;  // =/16
for (int32_t i = 0; i < count; i++) 
    state = _mm_add_epi32(state, _mm_mullo_epi32(_mm_loadu_si128(p128++), prime2));
    state = _mm_or_si128(_mm_sll_epi32(state, _mm_cvtsi32_si128(13)), _mm_srl_epi32(state, _mm_cvtsi32_si128(19)));
    state = _mm_mullo_epi32(state, prime1);


uint32_t temp[4];
_mm_storeu_si128(state, temp);
hash = _lrotl(temp[0], 1) + _lrotl(temp[1], 7) + _lrotl(temp[2], 12) + _lrotl(temp[3], 18);

下面是内循环体的拆解:

mov         rax,qword ptr [p128]  
mov         qword ptr [rsp+88h],rax  
mov         rax,qword ptr [rsp+88h]  
movdqu      xmm0,xmmword ptr [rax]  
movdqa      xmmword ptr [rsp+90h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+90h]  
movdqa      xmmword ptr [rsp+120h],xmm0  
mov         rax,qword ptr [p128]  
add         rax,10h  
mov         qword ptr [p128],rax  
movdqa      xmm0,xmmword ptr [prime2]  
movdqa      xmmword ptr [rsp+140h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+120h]  
movdqa      xmmword ptr [rsp+130h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+130h]  
pmulld      xmm0,xmmword ptr [rsp+140h]  
movdqa      xmmword ptr [rsp+150h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+150h]  
movdqa      xmmword ptr [rsp+160h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+160h]  
movdqa      xmmword ptr [rsp+170h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+20h]  
movdqa      xmmword ptr [rsp+100h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+100h]  
paddd       xmm0,xmmword ptr [rsp+170h]  
movdqa      xmmword ptr [rsp+180h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+180h]  
movdqa      xmmword ptr [rsp+190h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+190h]  
movdqa      xmmword ptr [rsp+20h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+20h]  
movdqa      xmmword ptr [rsp+1A0h],xmm0  
mov         eax,13h  
movd        xmm0,eax  
movdqa      xmmword ptr [rsp+1B0h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+1A0h]  
psrld       xmm0,xmmword ptr [rsp+1B0h]  
movdqa      xmmword ptr [rsp+1C0h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+1C0h]  
movdqa      xmmword ptr [rsp+200h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+20h]  
movdqa      xmmword ptr [rsp+1D0h],xmm0  
mov         eax,0Dh  
movd        xmm0,eax  
movdqa      xmmword ptr [rsp+1E0h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+1D0h]  
pslld       xmm0,xmmword ptr [rsp+1E0h]  
movdqa      xmmword ptr [rsp+1F0h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+1F0h]  
movdqa      xmmword ptr [rsp+210h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+200h]  
movdqa      xmmword ptr [rsp+230h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+210h]  
movdqa      xmmword ptr [rsp+220h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+220h]  
por         xmm0,xmmword ptr [rsp+230h]  
movdqa      xmmword ptr [rsp+240h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+240h]  
movdqa      xmmword ptr [rsp+250h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+250h]  
movdqa      xmmword ptr [rsp+20h],xmm0  
movdqa      xmm0,xmmword ptr [prime1]  
movdqa      xmmword ptr [rsp+280h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+20h]  
movdqa      xmmword ptr [rsp+270h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+270h]  
pmulld      xmm0,xmmword ptr [rsp+280h]  
movdqa      xmmword ptr [rsp+290h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+290h]  
movdqa      xmmword ptr [rsp+2A0h],xmm0  
movdqa      xmm0,xmmword ptr [rsp+2A0h]  
movdqa      xmmword ptr [rsp+20h],xmm0 

关于反汇编的一些问题:

为什么会有这么多 movdqa 指令(我认为内在函数的目的是它们映射到特定的硬件指令。)? 为什么只使用了xmm0,在我看来它正在将内存移入和移出向量管道(我希望使用更多的 xmmN 寄存器)?

这是用 Visual C++ 2017 编译的,我没有启用额外的优化。

当我在一个 64 MiB 的块上运行这两个 sn-ps 时,多次运行,标量代码大约快 3 个计时器。这不是我所期望的,我错过了什么?

【问题讨论】:

_mm_sll_epi32 带有向量 13 可能是性能问题的一部分(一旦启用优化)。使用_mm_slli_epi32,它使用直接形式pslld xmm, 13。 MSVC 对内在函数非常直白,不会为您进行此优化。额外的移位 uops 可能正在竞争 _mm_mullo_epi32 需要的一些相同的执行端口 - 不幸的是它很慢。 IDK 如果值得使用 SIMD 来尝试模拟 4x _lrotl。也许只有当您使用 AVX2 进行可变计数班次时。你确定你的循环计数是正确的,并且你没有在 SIMD 版本中做 4 倍的工作吗?两者都使用size &gt;&gt; 4;,但是每个向量做4个元素应该意味着你只做1/4的迭代。 哦,等等,两个循环都像 SIMD 一样编写。编译器可以自动向量化第一个。但第一个看起来甚至不像 C++。 uint4 state = new ... 不会编译,除非 uint4 实际上是指针类型的 typedef。也许那是C#?如果您想讨论速度比较,最好用评论指出不同的语言。 @PeterCordes 是的,第一个示例是 Unity 的 C# 参考实现,当使用 Burst 编译器基础结构编译时,它会进行自动矢量化。我正在尝试更多地了解这是如何发生的,并且我想自己编写一些 SIMD。 那么您应该将由此生成的 asm 与您从手动矢量化中获得的结果进行比较。 【参考方案1】:

好的,这与编译器优化标志有关,完全是 Visual C++ 特定的。

当我启用额外的编译器优化开关时,代码会变得更快。

内循环变成这样:

pmulld      xmm0,xmm5  
paddd       xmm0,xmm3  
movdqa      xmm3,xmm0  
pslld       xmm3,xmm2  
psrld       xmm0,xmm1  
por         xmm3,xmm0  
pmulld      xmm3,xmm4  

虽然文档说/Ox 等同于其他一些开关,但直到我实际使用/Ox/O2 编译时,代码才最终看起来像这样。

编辑:SIMD 结果最终只快了 8%。 xxhash32 算法是非常好的超标量代码,所以虽然我期待更多,但这就是我得到的。 original source 中有一些关于此的注释。

我电脑中的一些数字(Ryzen 1700)。

memcpy 11.334895 GiB/s
SIMD    5.737743 GiB/s
Scalar  5.286924 GiB/s

我希望尝试使xxhash32 算法几乎与 memcpy 一样快。我看到一些基准表明这可以改进,但如果没有可比较的基准就很难进行比较,这就是为什么我以我的计算机 memcpy 性能为基准。

【讨论】:

是的,问题中的代码看起来像调试模式代码,在每个语句之间溢出/重新加载每个对象。 Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? 是的,这对于内部代码来说太可怕了。 GCC 和 clang 是一样的:反优化调试模式是特殊的,不能被其他选项 AFAIK 的组合覆盖。

以上是关于为啥此 SIMD 代码运行速度比等效标量慢?的主要内容,如果未能解决你的问题,请参考以下文章

Cortex-M4 SIMD 比 Scalar 慢

为啥向量长度 SIMD 代码比普通 C 慢

为啥 SIMD 比蛮力慢

对于高基数分组,为啥使用 dplyr 管道 (%>%) 比等效的非管道表达式慢?

VBA宏运行速度为啥比Excel自带函数慢

为啥 OMP 任务运行速度比 OMP 慢?