为啥访问单个 SIMD 元素这么慢

Posted

技术标签:

【中文标题】为啥访问单个 SIMD 元素这么慢【英文标题】:Why Is it so Slow to Access Individual SIMD Elements为什么访问单个 SIMD 元素这么慢 【发布时间】:2018-08-30 01:49:26 【问题描述】:

我正在学习 C++ 中的 SIMD 内在函数,我有点困惑。假设我有一个 __m128 并且我想使用 __m128.m128_f32[0] 访问它的第一个元素(我知道这并不是对所有编译器都实现的),为什么这样做,据说非常慢。它不只是一个内存读取,就像其他任何东西一样吗?我已经阅读了其他一些页面,其中提到了诸如 Load-Hit-Store 之类的内容,但在我的问题的上下文中我并没有真正理解它。我知道做这样的事情是不明智的,我不打算这样做,但我很好奇究竟是什么导致它如此缓慢。

【问题讨论】:

【参考方案1】:

SIMD 向量变量通常位于 XMM 寄存器中,而不是内存中。向量存储/标量重新加载是编译器实现读取向量的整数元素的一种策略,但绝对不是唯一的。而且通常不是一个好的选择。

这个建议的重点是,如果您想要一个水平总和,请使用 shuffle / add 内在函数来编写它,而不是访问元素并使编译器产生的 asm 可能比您从精心挑选的 shuffle 中得到的更差。请参阅 Fastest way to do horizontal float vector sum on x86 了解 C 实现,以及编译器生成的 asm。


通过内存写入向量元素会更糟糕,因为向量存储/重叠标量存储/向量重新加载会导致存储转发停止。但相反,编译器并没有那么笨,可以使用 movd xmm0, eax 并使用向量 shuffle 将新元素合并到向量中。

您阅读 __m128.m128_f32[0] 的具体示例不是一个好的示例:它实际上是免费的,因为标量 float 通常保存在 XMM 寄存器的低元素中(除非您使用旧版 x87 编译 32 位代码标量的浮点数)。因此,XMM 寄存器中__m128 向量的低元素已经一个标量浮点数,编译器可以将其与addss 指令一起使用。调用约定在 XMM 寄存器中传递 float,并且不需要将上面的元素归零,因此那里没有额外的成本。


在 x86 上,它并不是非常昂贵,但您肯定希望避免在内部循环中使用它。对于 float,一个好的编译器会将其转换为 shuffle,您可以使用最终执行 float _mm_cvtss_f32 (__m128 a) 的内在函数自己编写(编译为零指令,如上所述)。

对于整数,使用 SSE4.1,您有望获得 pextrd eax, xmm0, 3 或其他任何值(或者更便宜的 movd eax, xmm0 用于低元素)。


在 ARM 上,整数和向量寄存器之间的传输比 x86 上的昂贵。至少更高的延迟,如果不是糟糕的吞吐量。在某些 ARM CPU 上,CPU 的整数部分和向量部分根本没有紧密耦合,当一方必须等待另一方的结果时会出现停顿。 (我想我读过最近的 ARM,比如支持 AArch64 的 CPU,通常具有更低的延迟 intSIMD。)

(您没有标记 x86 或 SSE,但您确实提到了 MSVC 的 __m128,所以我主要回答的是 x86。

【讨论】:

好的。所以我必须说我不完全明白。具体来说,您能否重申一下您所说的向量存储/标量重新加载是一种糟糕的策略,以及存储转发在这里如何应用 我现在知道访问第 0 个 f32 元素不应该很昂贵,但是为什么访问更高的元素会很昂贵,不应该几乎相同。对不起,如果这些是微不足道的问题,但我真的很想理解。为什么你需要洗牌到第 0 个元素,为什么不直接从第一个、第二个等读取。作为一个单独的问题,如果您可以非常便宜地洗牌到第 0 个元素,那么从 __m128 中读取单个元素而不修改它是一件好事吗? @Blu342:它是标量存储的向量 reload,您可以在其中获得存储转发停顿。就像你用 4 个标量存储和一个向量加载实现 _mm_set_epi32 一样。有关 gcc 的次优代码生成策略的详细讨论,请参阅 gcc.gnu.org/bugzilla/show_bug.cgi?id=80820 和 gcc.gnu.org/bugzilla/show_bug.cgi?id=80833,以及 _mm_set 如何编译和应该编译的示例。 @Blu342:访问更高的元素并不是很昂贵;它只需花费movaps / shufps。但这也意味着您只为一个标量元素做所有这些,而不是如果您可以保持代码矢量化,那么您可以在相同或更少的指令/微指令/延迟/其他 asm 成本指标中一次操作更多元素.这就是为什么我提到水平和作为例子:对于一个 N 元素向量,SIMD shuffle/add 具有 O(log2(N)) 成本与 O(N) 成本,用于将每个元素提取到标量和相加。 正确。所以基本上如果你访问一个高阶元素,它必须做一个 movaps 来复制 m128,然后是一个掩码,以获得相关位?或者洗牌到第0个索引,这样就可以直接访问了。是对的吗?感谢所有的帮助顺便说一句。我真的很感激。

以上是关于为啥访问单个 SIMD 元素这么慢的主要内容,如果未能解决你的问题,请参考以下文章

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

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

为啥wordpress反应这么慢

如何使用 NEON SIMD 合并 2 行的元素?

为啥 SIMD 比蛮力慢

HTML:为啥元素占用这么多宽度?