对齐与未对齐 x86 SIMD 指令之间的选择

Choice between aligned vs. unaligned x86 SIMD instructions

SIMD指令一般有两种:

一个。使用对齐内存地址的那些,如果地址未在操作数大小边界对齐,将引发一般保护 (#GP) 异常:

movaps  xmm0, xmmword ptr [rax]
vmovaps ymm0, ymmword ptr [rax]
vmovaps zmm0, zmmword ptr [rax]

乙。那些使用未对齐内存地址的,不会引发此类异常:

movups  xmm0, xmmword ptr [rax]
vmovups ymm0, ymmword ptr [rax]
vmovups zmm0, zmmword ptr [rax]

但我很好奇,为什么我要搬起石头砸自己的脚并使用第一组的对齐内存指令?

  • 未对齐访问:只能使用movups/vmovups。在对齐访问案例(见下文)中讨论的相同惩罚也适用于此。此外,跨越高速缓存行或虚拟页面边界的访问总是对所有处理器造成惩罚。
  • 对齐访问:
    • 在 Intel Nehalem 及更高版本(包括 Silvermont 及更高版本)和 AMD Bulldozer 及更高版本上:预解码后,它们以完全相同的方式针对相同的操作数执行。这包括对移动消除的支持。对于获取和预解码阶段,它们为相同的操作数消耗完全相同的资源。
    • 在 Nehalem 和 Bonnell 之前以及 Bulldozer 之前:它们被解码为不同的融合域 uops 和未融合域 uops。 movups/vmovups 在管道的前端和后端消耗更多资源(最多两倍)。换句话说,就延迟 and/or 吞吐量而言,movups/vmovups 最多可以比 movaps/vmovaps 慢两倍。

因此,如果您不关心旧的微体系结构,两者在技术上是等同的。尽管如果您知道或期望数据对齐,您应该使用对齐指令来确保数据确实对齐,而不必在代码中添加显式检查。

我认为即使在 "Intel Nehalem and later (including Silvermont and later) and AMD Bulldozer and later" 上使用 _mm_loadu_ps_mm_load_ps 之间也存在细微差别,这会对性能产生影响。

折叠一个负载的操作和另一个操作(例如乘法)只能用 load,而不是 loadu 内在函数来完成,除非你编译时启用了 AVX 以允许未对齐的内存操作数。

考虑以下代码

#include <x86intrin.h>
__m128 foo(float *x, float *y) {
    __m128 vx = _mm_loadu_ps(x);
    __m128 vy = _mm_loadu_ps(y);
    return vx*vy;
}

这得到 converted to

movups  xmm0, XMMWORD PTR [rdi]
movups  xmm1, XMMWORD PTR [rsi]
mulps   xmm0, xmm1

然而,如果使用了对齐加载内在函数 (_mm_load_ps),它会被编译为

movaps  xmm0, XMMWORD PTR [rdi]
mulps   xmm0, XMMWORD PTR [rsi]

省了一条指令。但是如果编译器可以使用 VEX 编码加载,那就是 only two instructions for unaligned as well.

vmovups xmm0, XMMWORD PTR [rsi]
vmulps  xmm0, xmm0, XMMWORD PTR [rdi]

尽管在 Intel Nehalem 及更高版本或 Silvermont 及更高版本或 AMD Bulldozer 及更高版本上使用指令 movapsmovups 时性能没有差异,但因此用于对齐访问。

但是 使用 _mm_loadu_ps_mm_load_ps intrinsics 时性能会有所不同启用 AVX,在编译器的权衡不是 movapsmovups 的情况下,它介于 movups 或将加载折叠到 ALU 指令之间。 (当向量仅用作一个事物的输入时会发生这种情况,否则编译器将使用 mov* 加载将结果放入寄存器以供重用。)