为啥此 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 >> 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 代码运行速度比等效标量慢?的主要内容,如果未能解决你的问题,请参考以下文章