【问题标题】: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 与您从手动矢量化中获得的结果进行比较。

标签: visual-c++ sse simd intrinsics


【解决方案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 性能为基准。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-25
  • 2021-08-18
  • 2016-10-23
  • 1970-01-01
  • 2017-02-03
  • 2020-01-02
  • 1970-01-01
相关资源
最近更新 更多