GCC __attribute__ 在 32 字节处对齐的 AVX 矢量化代码中的段错误

Posted

技术标签:

【中文标题】GCC __attribute__ 在 32 字节处对齐的 AVX 矢量化代码中的段错误【英文标题】:Seg Fault in AVX Vectorized Code with GCC __attribute__ aligned at 32 bytes 【发布时间】:2014-10-29 00:27:37 【问题描述】:

只有当循环在 AVX 机器(Intel(R) Core(TM) i5-3570K CPU @ 3.40GHz)上完全矢量化时,我才会在循环中遇到段错误。

使用 gcc -c -march=native MyClass.cpp -O3 -ftree-vectorizer-verbose=6 编译

我正在尝试对齐数组,以便避免来自 -ftree-vectorizer-verbose=6 的这些消息:

MyClass.cpp:352: note: dependence distance modulo vf == 0 between this_7(D)->x[i_101] and this_7(D)->x[i_101]
MyClass.cpp:352: note: vect_model_load_cost: unaligned supported by hardware.
MyClass.cpp:352: note: vect_get_data_access_cost: inside_cost = 2, outside_cost = 0.
MyClass.cpp:352: note: vect_model_store_cost: unaligned supported by hardware.
MyClass.cpp:352: note: vect_get_data_access_cost: inside_cost = 2, outside_cost = 0.
MyClass.cpp:352: note: Alignment of access forced using peeling.
MyClass.cpp:352: note: vect_model_load_cost: aligned.
MyClass.cpp:352: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .
MyClass.cpp:352: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 1 .
MyClass.cpp:352: note: vect_model_store_cost: aligned.
MyClass.cpp:352: note: vect_model_store_cost: inside_cost = 1, outside_cost = 0 .
MyClass.cpp:352: note: cost model: prologue peel iters set to vf/2.
MyClass.cpp:352: note: cost model: epilogue peel iters set to vf/2 because peeling for alignment is unknown .

我想看到(并且确实看到)的是:

MyClass.cpp:352: note: dependence distance modulo vf == 0 between this_7(D)->x[i_101] and this_7(D)->x[i_101]
MyClass.cpp:352: note: vect_model_load_cost: aligned.
MyClass.cpp:352: note: vect_get_data_access_cost: inside_cost = 1, outside_cost = 0.
MyClass.cpp:352: note: vect_model_store_cost: aligned.
MyClass.cpp:352: note: vect_get_data_access_cost: inside_cost = 2, outside_cost = 0.
MyClass.cpp:352: note: vect_model_load_cost: aligned.
MyClass.cpp:352: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .
MyClass.cpp:352: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 1 .
MyClass.cpp:352: note: vect_model_store_cost: aligned.
MyClass.cpp:352: note: vect_model_store_cost: inside_cost = 1, outside_cost = 0 .

现在,无论如何,我都不是 C/C++/Assembler 大师,但是当我遇到 seg 错误时,我认为我的代码中有一些指针/数组/其他错误,并且完全矢量化的循环只是暴露了这个.但是经过两天学习汇编程序后,我无法找到它。所以我在这里。

代码如下所示(希望我包含了所有相关内容——我无法在此处完整分享实际​​的 .cpp):

class MyClass 

private:
    static const long maxElems = 1024;
    static const double otherVar = 0.9;
    double x[maxElems] __attribute__ ((aligned (32)));  <-- gcc reports fully vectorized
    //double x[maxElems];   <-- leads to unaligned peeling

public:
    void myFunc() 
        // Always works
        for (int i=0; i<maxElems; ++i) printf("Test: %d %.4e\n", i, x[i]);

        // Seg fault if fully vectorized (no peeling)
        for (int i=0; i<maxElems; ++i) 
            x[i] = x[i] - 42;
         

        // Works if no seg fault earlier
        for (int i=0; i<maxElems; ++i) printf("Test: %d %.4e\n", i, x[i]);
    

当它完全矢量化时(使用 -Wa,-alh 标志查看汇编程序):

 989      00
 990 0b56 488B4424      movq    40(%rsp), %rax
 990      28
 991 0b5b C5FD280D      vmovapd .LC8(%rip), %ymm1
 991      00000000 
 992                    .p2align 4,,10
 993 0b63 0F1F4400      .p2align 3
 993      00
 994                .L153:
 995 0b68 C5FD2800      vmovapd (%rax), %ymm0
 996 0b6c C5FD5CC1      vsubpd  %ymm1, %ymm0, %ymm0
 997 0b70 C5FD2900      vmovapd %ymm0, (%rax)
 998 0b74 4883C020      addq    $32, %rax
 999 0b78 4C39E0        cmpq    %r12, %rax
 1000 0b7b 75EB             jne .L153

同样,关于“不知道汇编程序”的常见警告,但我确实花了相当多的时间打印指针并检查汇编程序,以说服自己这个循环在数组的开头和结尾开始和结束。但是当我得到段错误时,x 的起始地址不能被 32 整除。我认为这就是造成麻烦的原因。

是的,我知道我可以在堆上分配 x 并选择它最终的位置以使其对齐。但是我在这里的部分实验是让 MyClass 的大小固定,所有数据都在里面(想想:缓存效率),所以我在堆上分配了 MyClass 的实例,在集合中指向它们的指针,x 在 MyClass 里面.

难道 align attribute 不应该将 x 放在 32 字节的边界上吗?编译器假设,然后 vmovapd 正在爆炸,因为它不是,对吧?

关于对齐的 GCC 文档:https://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html

我是否必须以某种方式在堆上对齐 MyClass?我怎么做?我如何告诉 GCC 我这样做了,所以它像我想要的那样矢量化?

编辑:我已经解决了这个问题(部分感谢下面的 cmets 和答案)。通过覆盖默认的new 运算符,可以保证在堆上创建对象时对齐。当我这样做时,我没有遇到段错误,并且我的代码仍然可以按照我的意愿完美地矢量化。我是怎么做到的:

static void* operator new(size_t size) throw (std::bad_alloc) 
    void *alignedPointer;
    int alignError = 0;

    // Try to allocate the required amount of memory (using POSIX standard aligned allocation)
    alignError = posix_memalign(&alignedPointer, VECTOR_ALIGN_BYTES, size);

    // Throw/Report error if any
    if (alignError) 
        throw std::bad_alloc();
    

    // Return a pointer to this aligned memory location
    return alignedPointer;


static void operator delete(void* alignedPointer) 
    // POSIX aligned memory allocation can be freed normally with free()
    free(alignedPointer);

C++ 在调用运算符之后/之前为您调用构造函数/析构函数。因此对齐是由类本身控制的。如果您有不同的偏好,还有其他对齐的内存分配器。我使用了 POSIX。

两个警告:如果有人使用任意地址调用placement new,您仍然会不对齐。如果有人将你的类声明为他们类的成员,并且他们的类是在堆上分配的,那么你可能是未对齐的。我已经在我的构造函数中进行了检查,如果检测到,则会抛出错误。

【问题讨论】:

也尝试对齐MyClass。非静态数据成员的对齐属性只能控制偏移量,不能控制内存中的绝对位置。 通过“对齐 MyClass”我假设你的意思是当我在堆上创建它时,我确保它在 32 字节的边界上?数据成员上的 align 属性会确保它的偏移量保持对齐吗? 不,我的意思是坚持__attribute__ ((aligned (32))) class MyClass @BenVoigt,这不起作用。我将 align 属性放在类声明中并再次尝试但没有更改。该对象是在“任何地方”的堆中创建的,但我仍然遇到段错误。如果对象“恰好”分配在 32 字节边界上,则该实例可以正常工作,但此 align 属性不能确保这一点。如果数组是成员并存储在对象中,我还没有找到一种完全在类内强制对齐的方法。我将尝试在初始化时强制对齐,但这意味着一个类无法控制自己的对齐,我认为这是一件坏事。 我错过了关于您使用new 使用堆(或免费存储)的部分。当然,您需要将堆上的内存与posix_memalign 之类的东西对齐(我使用了_mm_malloc,因为它适用于GCC、MinGW、ICC 和MSVC)。但是对于静态分配和堆栈分配的数据,您应该只需要__attribute__ ((aligned (32))) 【参考方案1】:
__attribute__((aligned(32))

可能不会像我们认为的那样做(错误?功能?)。

它基本上告诉编译器它可以假设这个东西是对齐的,它可能不是。 如果它在堆上,则需要使用posix_memalign 或类似名称进行分配。

如果设置了 __attribute__((aligned(...)) 但分配未对齐,则 GCC 实际上会导致指针运算错误。

s2->aligned_var = 0x199c030
&s2->aligned_var % 0x40  = 0x0

https://gcc.gnu.org/ml/gcc/2014-06/msg00308.html

【讨论】:

__builtin_assume_aligned 告诉 GCC 假设对齐。 "强制编译器确保(尽可能)类型为 struct S 或 more_aligned_int 的每个变量将被分配并至少在 8 字节边界上对齐。在 Sparc 上,具有所有 struct S 类型的变量与 8 字节边界对齐,允许编译器在将一个 struct S 类型的变量复制到另一个变量时使用 ldd 和 std(双字加载和存储)指令,从而提高运行时效率。” gcc.gnu.org/onlinedocs/gcc-3.1/gcc/Type-Attributes.html 但它似乎就像 __builtin_assume_aligned 一样工作。我称之为 gcc 错误... 我不知道我是否同意这个属性“基本上告诉编译器它可以假设”对齐,除非你的意思是“除了尝试对齐”成员。我认为它确实对齐了成员,但仅相对于对象的其余部分......至少如果对象是在堆上分配的。在我的情况下,我可以确认 x 始终是堆中对象地址的 32 个字节,而不管我在它前面声明的成员的排列方式。我认为除了“尝试”对齐成员之外,gcc 假设它成功地这样做了,这就是我遇到 seg 错误的原因。 是的,我说的是 gcc 的实际作用,并链接到 gcc 列表邮件,示例显示了它的实际作用。是的,它应该对齐成员(或者至少文档不应该建议这样做),但通常它不会对齐成员。堆上似乎没有保证。当它不对齐时,它的行为就好像它已经对齐并且假设对齐,即使它没有这样做并且 BOOM。 0x199c030 % 0x40 == 0x0 ?真的吗? 我未能正确拼出的含义(对不起)是它可能没有对齐内存,因为它会覆盖地址算术假设它在失败时已经这样做了 - 你不能 检查它是否失败。 attribute(aligned(32)) 可能不会像我们认为的那样做,或者文档所说的那样。 posix_memalign 或类似的(例如滚动你自己的)是必需的。

以上是关于GCC __attribute__ 在 32 字节处对齐的 AVX 矢量化代码中的段错误的主要内容,如果未能解决你的问题,请参考以下文章

GCC 的 __attribute__((__packed__)) 是不是保留原始顺序?

GCC __attribute __((模式(XX))实际上做了什么?

__attribute__((packed))作用

使用 GCC 向量扩展存储、修改和检索字符串?

GCC __attribute__((constructor)) 在对象构造函数之前调用

GCC的__attribute__ ((constructor))和__attribute__ ((destructor))