为什么访问单个 SIMD 元素这么慢
Why Is it so Slow to Access Individual SIMD Elements
我正在学习 C++ 中的 SIMD 内在函数,但我有点困惑。假设我有一个 __m128 并且我想使用 __m128.m128_f32[0] 访问它的第一个元素(我知道这并不是所有编译器都实现的),为什么要这样做,据说,非常慢。这不就是像其他任何东西一样只是内存读取吗?我读过其他一些页面,其中提到了 Load-Hit-Store 之类的内容,但我并没有真正理解我的问题。我知道做这样的事情是不明智的,我不打算这样做,但我很好奇究竟是什么导致它如此缓慢。
SIMD 向量变量通常在 XMM 寄存器中,而不是内存中。向量存储/标量重载是编译器实现读取向量整数元素的一种策略,但绝对不是唯一的。而且通常不是一个好的选择。
这个建议的要点是,如果你想要一个水平和,用 shuffle / add intrinsics 编写它,而不是访问元素并使编译器产生可能比你从精心选择的 shuffle 中得到的更糟糕的 asm .请参阅 Fastest way to do horizontal float vector sum on x86 了解 C 实现,以及编译器生成的 asm。
通过内存写入向量的元素会更糟,因为向量存储/重叠标量存储/向量重新加载会导致存储转发停顿。但是相反,编译器并没有那么笨,可以使用 movd xmm0, eax
并使用向量洗牌将新元素合并到向量中。
你阅读 __m128.m128_f32[0]
的具体例子不是一个好例子:它实际上是免费的,因为标量 float
通常保存在 XMM 寄存器的低位元素中(除非你正在编译 32 -bit 代码,带有用于标量的旧 x87 浮点数)。因此 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 的 CPUs,通常具有低得多的延迟 int<->SIMD。)
(你没有标记 x86 或 SSE,但你确实提到了 __m128
用于 MSVC,所以我主要回答了 x86。
我正在学习 C++ 中的 SIMD 内在函数,但我有点困惑。假设我有一个 __m128 并且我想使用 __m128.m128_f32[0] 访问它的第一个元素(我知道这并不是所有编译器都实现的),为什么要这样做,据说,非常慢。这不就是像其他任何东西一样只是内存读取吗?我读过其他一些页面,其中提到了 Load-Hit-Store 之类的内容,但我并没有真正理解我的问题。我知道做这样的事情是不明智的,我不打算这样做,但我很好奇究竟是什么导致它如此缓慢。
SIMD 向量变量通常在 XMM 寄存器中,而不是内存中。向量存储/标量重载是编译器实现读取向量整数元素的一种策略,但绝对不是唯一的。而且通常不是一个好的选择。
这个建议的要点是,如果你想要一个水平和,用 shuffle / add intrinsics 编写它,而不是访问元素并使编译器产生可能比你从精心选择的 shuffle 中得到的更糟糕的 asm .请参阅 Fastest way to do horizontal float vector sum on x86 了解 C 实现,以及编译器生成的 asm。
通过内存写入向量的元素会更糟,因为向量存储/重叠标量存储/向量重新加载会导致存储转发停顿。但是相反,编译器并没有那么笨,可以使用 movd xmm0, eax
并使用向量洗牌将新元素合并到向量中。
你阅读 __m128.m128_f32[0]
的具体例子不是一个好例子:它实际上是免费的,因为标量 float
通常保存在 XMM 寄存器的低位元素中(除非你正在编译 32 -bit 代码,带有用于标量的旧 x87 浮点数)。因此 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 的 CPUs,通常具有低得多的延迟 int<->SIMD。)
(你没有标记 x86 或 SSE,但你确实提到了 __m128
用于 MSVC,所以我主要回答了 x86。