使用 AVX/AVX2 内在函数的对齐和未对齐内存访问

Aligned and unaligned memory access with AVX/AVX2 intrinsics

根据 Intel 的软件开发人员手册(第 14.9 节),AVX 放宽了内存访问的对齐要求。如果直接在处理指令中加载数据,例如

vaddps ymm0,ymm0,YMMWORD PTR [rax]

加载地址不必对齐。但是,如果使用专用的对齐加载指令,比如

vmovaps ymm0,YMMWORD PTR [rax]

加载地址必须对齐(对齐到 32 的倍数),否则会引发异常。

让我困惑的是从内部函数自动生成代码,在我的例子中是 gcc/g++ (4.6.3, Linux)。请看下面的测试代码:

#include <x86intrin.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#define SIZE (1L << 26)
#define OFFSET 1

int main() {
  float *data;
  assert(!posix_memalign((void**)&data, 32, SIZE*sizeof(float)));
  for (unsigned i = 0; i < SIZE; i++) data[i] = drand48();
  float res[8]  __attribute__ ((aligned(32)));
  __m256 sum = _mm256_setzero_ps(), elem;
  for (float *d = data + OFFSET; d < data + SIZE - 8; d += 8) {
    elem = _mm256_load_ps(d);
    // sum = _mm256_add_ps(elem, elem);
    sum = _mm256_add_ps(sum, elem);
  }
  _mm256_store_ps(res, sum);
  for (int i = 0; i < 8; i++) printf("%g ", res[i]); printf("\n");
  return 0;
}

(是的,我知道代码有问题,因为我在未对齐的地址上使用对齐加载,但请耐心等待...)

我用

编译代码
g++ -Wall -O3 -march=native -o memtest memtest.C

在带有 AVX 的 CPU 上。如果我使用

检查 g++ 生成的代码
objdump -S -M intel-mnemonic memtest | more

我看到编译器没有生成对齐加载指令,而是直接在向量加法指令中加载数据:

vaddps ymm0,ymm0,YMMWORD PTR [rax]

代码执行没有任何问题,即使内存地址未对齐(OFFSET 为 1)。这很清楚,因为 vaddps 容忍未对齐的地址。

如果我取消注释带有第二个加法内在函数的行,编译器无法融合加载和加法,因为 vaddps 只能有一个内存源操作数,并生成:

vmovaps ymm0,YMMWORD PTR [rax]
vaddps ymm1,ymm0,ymm0
vaddps ymm0,ymm1,ymm0

现在程序出现段错误,因为使用了专用的对齐加载指令,但内存地址未对齐。 (顺便说一下,如果我使用 _mm256_loadu_ps,或者如果我将 OFFSET 设置为 0,程序不会出现段错误。)

以我的愚见,这使程序员受制于编译器,并使行为部分不可预测。

我的问题是:有没有办法强制C编译器在处理指令(如vaddps)中生成直接加载或生成专用加载指令(如vmovaps)?

无法通过内在函数显式控制负载的折叠。我认为这是内在的弱点。如果你想明确地控制折叠那么你必须使用汇编。

在以前版本的 GCC 中,我能够使用对齐或未对齐的加载在某种程度上控制折叠。但是,情况似乎不再如此(GCC 4.9.2)。我的意思是,例如在函数 AddDot4x4_vec_block_8wide here 中,负载被折叠

vmulps  ymm9, ymm0, YMMWORD PTR [rax-256]
vaddps  ymm8, ymm9, ymm8

但是in a previous verison of GCC货物没有折叠:

vmovups ymm9, YMMWORD PTR [rax-256]
vmulps  ymm9, ymm0, ymm9
vaddps  ymm8, ymm8, ymm9

显然,正确的解决方案是仅在您知道数据已对齐并且您确实想明确控制折叠时使用对齐加载。

除了 Z boson 的回答外,我还可以看出问题可能是由于编译器假设内存区域是对齐的(因为 __attribute__ ((aligned(32)))标记数组)。在运行时,该属性可能不适用于堆栈上的值,因为堆栈仅 16 字节对齐(参见 this 错误,在撰写本文时该错误仍然存​​在,尽管一些修复已将其纳入 gcc 4.6 ).编译器有权选择指令来实现内在函数,因此它可能会也可能不会将内存负载折叠到计算指令中,并且它也有权在折叠不发生时使用 vmovaps(因为,如前所述,内存区域应该对齐)。

您可以尝试通过指定 -mstackrealign-mpreferred-stack-boundary=5(请参阅 here)强制编译器在进入 main 时将堆栈重新对齐为 32 字节,但它会产生性能开销。