代码改变世界

Windows+GCC下内存对齐的常见问题

2013-11-27 20:10 空明流转 阅读(...) 评论(...) 编辑 收藏

结构/类对齐的声明方式

gcc和windows对于modifier/attribute的支持其实是差不多的。比如在gcc的例子中,内存对齐要写成:

class X
{
  //...
} __attribute__((aligned(16)));

但是实际上你写成

class __attribute__((aligned(16))) X 
{
    /*...*/
};

gcc一样可以识别。这样MSVC和gcc就可以使用宏完成跨平台编译。

对齐类型的变量在堆与栈上的分配

对齐在以下场合都能提示编译器为它的变量分配对齐的地址:

void foo()
{
    X v; // v是个栈上的16字节对齐的变量
    X* p = new X; // p是堆上的16字节对齐的指针
    X* a = new X[ARRAY_SIZE]; // 那么这个呢?
}

栈上的变量堆上分配出的变量,因为align这个hint的存在,都能满足16字节对齐的要求。但是数组呢?按照一般规律来分析,对齐后的sizeof(X),一定是对齐的整数倍。比如16字节对齐的话,那么X的大小只能是16的倍数。所以对于本例的数组而言,编译器应该也能知道a应该是16字节对齐的。

但是事实上挺奇怪。在MSVC上,p和a都很好的遵守了对齐的要求;在gcc上,p是对齐的,但是a却不是。其实这个问题在2004年便有人提出来,只是到目前为止一直都没有人动手过。当然,标准也没有规定X的数组就一定是要对齐的。要解决这个问题,要么重载class的operator new/delete,要么用memalign/aligned_malloc分配出对齐的内存,再placement new。出于易用性,我选择的是操作符重载。

clang对于对齐的支持更干脆:16B的对齐已经够用了。所以align完全被编译器忽视了。结果Intel出来了AVX,Clang就傻逼了。不知道这个问题3.4会不会修正。

编译器如何实现内存对齐

MSVC在x86下默认是支持的4B的内存对齐。也就是说在函数入口处,ESP和EBP只保证是4字节对齐的。这时,当前函数域栈上变量的地址都是ESP + 4 * x的形式。如果函数体内有对齐的变量,例如:

void foo()
{
    int __declspec(align(16)) x;
    // ...
}

那么编译器在代码生成时,会在函数的前部插入一段称为prolog的代码,这段代码会将堆栈修正为16B对齐,比如

PUSH EBP
MOV  EBP, ESP
SUB  ESP, XXX
AND  ESP, 0xFFFFFFF0h

这样ESP就一定是16字节对齐的。这个时候给x分配的地址,就可以是ESP + 0x10 * n的形式,这样就满足了对齐的需要。

在GCC上,gcc认为所有的函数都有义务在调用其它函数的时候,ESP是16字节对齐的(当然,可以通过编译选项修改这一要求)。不光是调用方会这样保证,被调用方也是这样默认的。所以GCC为了调用效率更高一点,便根据调用方的假设,去掉了“堆栈修正”这个步骤。

原来的代码可能就变成了

PUSH EBP             ; 假设这里的ESP是16B对齐的,Push了EBP,ESP就是16x-4了。
MOV  EBP, ESP
SUB  ESP, 0x0000023Ch ; 减完以后这里又是16字节对齐了

那么当被调用方遵守这个约定的时候,ESP当然就是16字节对齐的。但是有一种情况例外。在MinGW下,线程的入口函数是被API回调的。这个函数很可能是按照Windows的标准4个字节对齐的。这样,在没有堆栈修正的情况下,整个线程调用链16B对齐的默契就被打破了。如果这个时候出现了SSE代码试图存取“16字节对齐”的变量,那可能就会发生segment fault的异常,因为这些变量的地址并不是对齐的。

解决这个问题,有两种常见的办法:第一,写一个Wrapper函数,对齐ESP后转发调用;第二,使用编译选项-mstackrealign。这个选项会为所有函数增加堆栈修正的PROLOG代码,以保证函数栈帧一定是按照16字节或用户指定大小对齐。