局部变量未在内联函数中对齐

Local variable not aligned in inline function

在使用 Intrinsics 编程时出现了以下问题。 当我想在内联函数中加载或存储局部变量时,我得到了内存冲突错误,但前提是该函数是内联的。我不知道为什么在内联函数中堆栈变量没有对齐。

我已经用许多不同版本的 GCC 4.9、5.3、6.1 进行了测试。

失败的例子:

static inline foo(double *phi){
  double localvar[4];
  __m256d var = _mm256_load_pd (phi);
  __m256d res = _mm256_mul_pd(var, var);
  _mm256_store_pd (localvar, res); // <-  failed due to memory violation
  ...
}

如果我添加 __attribute__ ((aligned (32))) 或删除 inline,那么函数会正常工作。

所以有人可以解释一下(请详细说明),为什么一般情况下局部变量不加对齐 __attribute__ ((aligned (32))) 而内联函数中的局部变量不对齐?

_mm256_store_pd 要求您存储的内存地址必须与 32 字节边界对齐。但是在 C 中,我只认为 8 字节双精度和 8 字节双精度的标准对齐是 8 字节边界。

如果我不得不猜测函数何时未内联,它会在 32 字节边界上启动 localvar 数组。我不确定这是保证还是运气。我猜是运气,因为理论上内联一个函数不应该改变任何东西。编译器可能会将正确数量的字节压入堆栈,以便它对齐。我也看不出为什么它会保证 32 字节对齐。

当它被内联时,它的行为就好像代码只是在您调用函数的地方键入的一样。因此,您只能保证 localvar 是 8 字节对齐的,而不是保证的 32 字节对齐。我认为正确的解决方案是使用 aligned 属性来解决您的问题。您还可以使用 _mm256_storeu_pd 内在函数,它在没有对齐要求的情况下做同样的事情。根据我使用 haswell CPU 的经验,它同样快。

提供 32 字节对齐需要额外的指令(因为 ABI 只保证 16 字节对齐;只需查看 alignas(32)__attribute__((aligned(32))) 版本的 asm)。 如果你不要求编译器当然不会做,因为它不是免费的。 (另请参阅 gcc 的 -mpreferred-stack-boundary which controls this, and the 标签 wiki,以获取指向 ABI 文档的链接)。

double localvar[4];只需要8字节对齐,每个元素自然对齐。 SysV x86-64 ABI 确实保证 C99 可变大小数组的 16 字节对齐。我不确定正常的编译时间常数大小的数组是否默认获得 16-B 对齐。

但是,由于某些原因,当前版本的 gcc 在具有 __m256d 局部变量的测试函数中将堆栈对齐到 32B。在 -O3 它不会将它们溢出到堆栈,所以它们被浪费了(除了让像这样的错误代码碰巧起作用)。 gcc 没有删除这些东西的事实是一个错误的优化。 (在 -O0 处需要它,gcc 会将所有内容溢出到内存中。)

由于我的测试函数版本(实际编译)没有任何其他局部变量,双精度数组也是 32B 对齐的。据推测,您正在将其内联到具有其他一些局部变量的调用者中,这会导致数组的对齐方式不同。

Here's the code on the Godbolt compiler explorer:

extern void use_buffer(double*);
// static inline
void no_alignment(const double *phi){
  double localvar[4];
  __m256d var = _mm256_load_pd (phi);
  __m256d res = _mm256_mul_pd(var, var);
  _mm256_storeu_pd (localvar, res);         // use an unaligned store since we didn't request alignment for the buffer
  use_buffer(localvar);
}

    lea     r10, [rsp+8]                 // save old RSP (in a clumsy way)
    and     rsp, -32                     // truncate RSP to the next 32B boundary
    push    QWORD PTR [r10-8]            // save more stuff
    push    rbp
    mov     rbp, rsp
    push    r10
    sub     rsp, 40
    ...         vmovupd YMMWORD PTR [rbp-48], ymm0     ...   // function body
    add     rsp, 40
    pop     r10
    pop     rbp
    lea     rsp, [r10-8]

这就是为什么您的代码在未内联时恰好可以运行的原因。尽管奇怪的是它没有被内联,即使没有 inline 关键字,除非你编译时没有优化或者你没有使用 static 让编译器知道一个单独的定义不是需要。