Gcc 使用AVX/AVX2内部函数进行对齐和未对齐的内存访问

Gcc 使用AVX/AVX2内部函数进行对齐和未对齐的内存访问,gcc,avx,avx2,Gcc,Avx,Avx2,根据英特尔软件开发人员手册(第14.9节),AVX放宽了内存访问的对齐要求。如果数据直接加载到处理指令中,例如 vaddps ymm0,ymm0,YMMWORD PTR [rax] 加载地址不必对齐。但是,如果使用专用的对齐加载指令,例如 vmovaps ymm0,YMMWORD PTR [rax] 加载地址必须对齐(32的倍数),否则会引发异常 让我困惑的是,在我的例子中,由gcc/g++(4.6.3,Linux)从内部函数自动生成代码。请查看以下测试代码: #include <x8

根据英特尔软件开发人员手册(第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;
}
在使用AVX的CPU上。如果我使用

objdump -S -M intel-mnemonic memtest | more
我发现编译器不会生成对齐的加载指令,而是直接在向量加法指令中加载数据:

vaddps ymm0,ymm0,YMMWORD PTR [rax]
即使内存地址没有对齐(偏移量为1),代码也可以毫无问题地执行。这一点很清楚,因为VADDP容忍未对齐的地址

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

vmovaps ymm0,YMMWORD PTR [rax]
vaddps ymm1,ymm0,ymm0
vaddps ymm0,ymm1,ymm0
现在程序seg出现故障,因为使用了专用的对齐加载指令,但内存地址没有对齐。(顺便说一下,如果我使用_mm256_loadu_ps,或者如果我将OFFSET设置为0,程序不会seg fault。)

在我看来,这让程序员任由编译器摆布,并使行为部分不可预测


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

没有办法通过内部函数显式控制加载的折叠。我认为这是本质上的弱点。如果要显式控制折叠,则必须使用assembly

在以前的GCC版本中,我能够使用对齐或未对齐的负载在一定程度上控制折叠。然而,情况似乎不再如此(GCC 4.9.2)。我的意思是,例如,在函数
AddDot4x4\u vec\u block\u 8wide
中,负载被折叠

vmulps  ymm9, ymm0, YMMWORD PTR [rax-256]
vaddps  ymm8, ymm9, ymm8
但是,荷载未折叠:

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

显然,正确的解决方案是,当您知道数据是对齐的,并且如果您真的想显式地控制折叠使用程序集时,只使用对齐的加载。

除了Z玻色子的答案之外,我可以看出问题可能是由编译器假定内存区域对齐引起的(因为
\uuuu属性\uuuu((对齐(32))
标记数组)。在运行时,该属性可能不适用于堆栈上的值,因为堆栈仅对齐了16字节(请参阅错误,在撰写本文时该错误仍处于打开状态,尽管某些修复程序已将其写入gcc 4.6)编译器有权选择实现内部函数的指令,因此它可能会也可能不会将内存负载折叠到计算指令中,并且在没有折叠的情况下,它也有权使用
vmovaps
(因为,如前所述,内存区域应该对齐)


通过指定
-mstackrealling
-mpreferenced stack boundary=5
,可以尝试在
main
中输入时强制编译器将堆栈重新对齐到32字节(请参阅)但是它会带来性能开销。

这样做的动机是什么?如果您不知道数据是否正确对齐,只需使用未对齐的加载。我不会说您受编译器的支配;如果您告诉它使用对齐的加载,如果它在指针未对齐的情况下发生故障,我也不会感到惊讶。Th事实上,在某些情况下,编译器会发出与您的错误相关的代码。最近,编译器开始从不生成对齐内存访问。这使得不进行区分变得更容易,而且从Nehalem开始的所有处理器都不会受到性能损失。就我个人而言,我宁愿它崩溃,让我知道这一点我在性能上有一个潜在的缺陷。@JasonR:我发现行为不一致。也许我应该包括另一个扭曲:如果我在原始代码上使用
\mm256\u loadu\ps
,gcc会生成一个未对齐的加载
vmovups
和一个
vaddps
处理寄存器操作数,而它可以完美地生成一个
vaddps
指令,其内存操作数允许未对齐的地址。@Ralf Visual Studio在VS2013前后开始执行此操作。英特尔编译器在ICC11和ICC13之间的某段时间开始执行此操作。但我不确定GCC(如果它确实执行此操作的话)。我相信当代版本的gcc和clang在被询问时以及移动是否自动生成时都会发出对齐的移动指令。在某些情况下,这可能会导致问题,例如堆栈未正确对齐;SSE/AVX寄存器类型溢出到堆栈可能会导致分段错误。没有编译器会折叠移动指令hat加载到
vaddps
,因为它需要来自内存的数据作为两个操作数。如果您没有使用AVX进行测试,则可能需要再次测试,因为此示例无法很好地测试编译器是否将加载作为内存操作数折叠到以后的指令中。(顺便说一句,融合是Intel的解码器对UOP所做的。一些带有内存操作数的INSN无论如何都不能进行微融合,例如
PINSRW
。我喜欢用“折叠”一词来描述用内存操作数替换负载。)@彼得·科德斯,你说得对。我也更喜欢你的术语折叠。我只是在研究这个问题。我必须修正我的答案。给我一点时间。@彼得·科德斯,我看了一下,似乎使用GCC折叠时对齐不再重要了(4.9.2).@PeterCordes我发现ICC15有时会折叠加载,即使这意味着复制加载。(多个折叠加载到同一地址)这通常是在寄存器压力的情况下。@Myst
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps  ymm9, ymm0, ymm9
vaddps  ymm8, ymm8, ymm9