为啥在某些 CPU 上 SSE 对齐读取 + shuffle 比未对齐读取慢,而在其他 CPU 上则不然?

Posted

技术标签:

【中文标题】为啥在某些 CPU 上 SSE 对齐读取 + shuffle 比未对齐读取慢,而在其他 CPU 上则不然?【英文标题】:Why is SSE aligned read + shuffle slower than unaligned read on some CPUs but not on others?为什么在某些 CPU 上 SSE 对齐读取 + shuffle 比未对齐读取慢,而在其他 CPU 上则不然? 【发布时间】:2014-04-22 07:11:49 【问题描述】:

在尝试优化我的有限差分代码所需的未对齐读取时,我更改了未对齐的负载,如下所示:

__m128 pm1 =_mm_loadu_ps(&H[k-1]);

进入这个对齐的读取 + 洗牌代码:

__m128 p0   =_mm_load_ps(&H[k]);
__m128 pm4  =_mm_load_ps(&H[k-4]);
__m128 pm1  =_mm_shuffle_ps(p0,p0,0x90);   // move 3 floats to higher positions
__m128 tpm1 =_mm_shuffle_ps(pm4,pm4,0x03); // get missing lowest float
       pm1  =_mm_move_ss(pm1,tpm1);        // pack lowest float with 3 others

H 是 16 字节对齐的; H[k+1]H[k±3]movlhpsmovhlpsH[k±2] 的优化也有类似的变化(here 是循环的完整代码)。

我发现在我的 Core i7-930 上,阅读 H[k±3] 的优化似乎是富有成效的,而为 ±1 添加下一个优化会减慢我的循环速度(以百分比为单位)。在±1±3 优化之间切换不会改变结果。

同时,在 Core 2 Duo 6300 和 Core 2 Quad 上启用这两种优化(±1±3)提高了性能(提高了 10%),而对于 Core i7-4765T,这两者都减慢了性能下降(以百分比为单位)。

在 Pentium 4 上,所有优化未对齐读取的尝试,包括 movlhps/movhlps 的尝试都会导致速度变慢。

为什么不同的 CPU 会有如此不同?是因为代码大小增加导致循环可能不适合某些指令缓存吗?或者是因为一些 CPU 对未对齐的读取不敏感,而另一些则更敏感?或者,某些 CPU 上的随机播放等操作可能很慢?

【问题讨论】:

P4 上的一切都很慢,包括movhlpsshufps,甚至是movaps reg, reg。它是可怕的。我只是假装 P4 从未存在并专注于 Core2 和更新版本。 相关:Cacheline splits, take two,来自 Dark Shikari 的博客(x264 首席开发人员),针对 Core2 与早期版本。 palignr 适用于 Core 2;使用 3 次随机播放是不好的(包括 movss,它实际上是一个混合但在随机播放端口上运行)。另请参阅How can I accurately benchmark unaligned access speed on x86_64 以获取有关 x86 上未对齐负载性能的更多链接;是的,它已从 Core 2 更改为 Haswell,尤其是对于没有缓存行拆分的未对齐负载。 【参考方案1】:

英特尔每两年推出一种新的微架构。执行单元的数量可能会改变,以前只能在一个执行单元中执行的指令在较新的处理器中可能有 2 或 3 个可用。指令的延迟可能会发生变化,例如添加shuffle 执行单元时。

英特尔在他们的优化参考手册中进行了详细介绍,这是链接,下面我复制了相关部分。

http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

3.5.2.7 浮点/SIMD 操作数部分

来自内存的 MOVUPD 指令执行两个 64 位加载,但需要额外的微操作来调整地址并将加载合并到单个寄存器中。使用 MOVSD XMMREG1, MEM 可以获得相同的功能; MOVSD XMMREG2,MEM+8; UNPCKLPD XMMREG1,XMMREG2,使用更少的μop,可以更有效地打包到跟踪缓存中。已发现后一种替代方案在某些情况下可提供几个百分比的性能改进。它的编码需要更多的指令字节,但这对于 Pentium 4 处理器来说很少有问题。 MOVUPD 的存储版本复杂且缓慢,以至于应该始终使用具有两个 MOVSD 和一个 UNPCKHPD 的序列。

汇编/编译器编码规则 44.(ML 影响,L 通用性)不要使用 MOVUPD XMMREG1, MEM 进行未对齐的 128 位加载,而是使用 MOVSD XMMREG1, MEM; MOVSD XMMREG2,MEM+8; UNPCKLPD XMMREG1,XMMREG2。如果附加寄存器不可用,则使用 MOVSD XMMREG1, MEM; MOVHPD XMMREG1, MEM+8.

Assembly/Compiler Coding Rule 45.(M 影响,ML 通用性)不要使用 MOVUPD MEM,XMMREG1 进行存储,而是使用 MOVSD MEM,XMMREG1; UNPCKHPD XMMREG1,XMMREG1; MOVSD MEM+8, XMMREG1 代替。

6.5.1.2 数据混合

部分

从 SoA 转换为 AoS 格式的数据可以应用于许多应用领域,包括 3D 几何、视频和成像。可以采用两种不同的混合技术来处理浮点和整数数据。示例 6-3 演示了一个使用 SHUFPS、MOVLHPS、MOVHLPS 指令的 swizzle 函数。

示例 6-3 中的技术(加载 16 个字节,使用 SHUFPS 并复制 XMM 寄存器的一半)优于在较新的微架构上使用 MOVLPS/MOVHPS 加载每个向量的一半的替代方法。这是因为使用 MOVLPS/MOVHPS 加载 8 个字节会产生代码依赖性并降低执行引擎的吞吐量。示例 6-3 和示例 6-4 的性能考虑通常取决于每个微架构的特性。例如,在英特尔酷睿微架构中,执行 SHUFPS 往往比 PUNPCKxxx 指令慢。在增强型 Intel Core 微架构中,SHUFPS 和 PUNPCKxxx 指令都以 1 个周期的吞吐量执行,这是由于 128 位 shuffle 执行单元。然后下一个重要的考虑是只有一个端口可以执行 PUNPCKxxx 与 MOVLHPS/MOVHLPS 可以在多个端口上执行。由于 3 个用于执行 SIMD 指令的端口,这两种技术在英特尔酷睿微架构上的性能都比以前的微架构有所提高。由于 128 位随机播放单元,这两种技术都进一步改进了增强型英特尔酷睿微架构。

【讨论】:

【参考方案2】:

在较旧的 CPU 上,未对齐的负载会带来很大的性能损失 - 它们会产生两个总线读取周期,然后在两个读取周期之后进行一些额外的修复。这意味着未对齐的负载通常比对齐的负载慢 2 倍或更多。然而,对于更新的 CPU(例如 Core i7),未对齐负载的损失几乎可以忽略不计。因此,如果您需要支持旧 CPU 和新 CPU,您可能希望以不同方式处理未对齐的负载。

【讨论】:

但是未对齐的加载比 Pentium 4 上的对齐加载+shuffle 更快,我认为它已经足够老了,不是吗? 是的,我不知道为什么您会在 Pentium 4 上看到这种效果 - 您需要查看其他指令的延迟,看看它们与 2-3 周期相比如何未对齐负载的成本。

以上是关于为啥在某些 CPU 上 SSE 对齐读取 + shuffle 比未对齐读取慢,而在其他 CPU 上则不然?的主要内容,如果未能解决你的问题,请参考以下文章

Ubuntu - 如何判断 CPU 应用程序当前正在使用 AVX 还是 SSE?

C++ SSE:存储到数组后的未定义行为

为啥我的手动调优、支持 SSE 的代码这么慢?

SSE / AVX 对齐内存上的 valarray

SSE向量化与内存对齐的关系

GCC - 如何重新对齐堆栈?