转载:https://blog.csdn.net/markl22222/article/details/38051483
Wrote by mutouyun. (http://darkc.at/about-data-structure-alignment/)
在C++98/03里,对内存对齐的操作在不同的编译器里可能有不同的方法。
在MSVC中,一般使用#progma pack来指定内存对齐:
#pragma pack(1) // 指定后面的内容内存对齐为1
struct MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
#pragma pack() // 还原默认的内存对齐
这时,MyStruct由于按1字节对齐,其中的所有成员都将变为1字节对齐,因此sizeof(MyStruct)将等于16。
还有另外一个简单的方法:
__declspec(align(64)) struct MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
__declspec(align(64))将指定内存对齐为64。比较坑的是,这种方法不能指定内存对齐小于默认对齐,也就是说它只能调大不能调小(__declspec(align(#)) can only increase alignment restrictions)。因此下面这样写会忽略掉declspec:
__declspec(align(1)) struct MyStruct // ...
// warning C4359: 'MyStruct': Alignment specifier is less than actual alignment (8), and will be ignored.
微软的__declspec(align(#)),其#的内容可以是预编译宏,但不能是编译期数值:
#define XX 32
struct __declspec(align(XX)) MyStruct_1 {}; // OK
template
struct __declspec(align(YY)) MyStruct_2 {}; // error C2059: syntax error: 'identifier'
static const unsigned ZZ = 32;
struct __declspec(align(ZZ)) MyStruct_3 {}; // error C2057: expected constant expression
__declspec(align(#))最大支持对齐为8192(Valid entries are integer powers of two from 1 to 8192)。
下面再来看gcc。gcc和MSVC一样,可以使用#pragma pack:
#pragma pack(1)
struct MyStruct
{
// ...
};
#pragma pack()
另外,也可以使用__attribute__((__aligned__((#)))):
struct __attribute__((__aligned__((1)))) MyStruct_1
{
// ...
};
struct MyStruct_2
{
// ...
} __attribute__((__aligned__((1))));
这东西写上面写下面都是可以的,但是不能写在struct前面。
和MSVC一样,__attribute__也只能把字节对齐改大,不能改小(The aligned attribute can only increase the alignment)。比较坑的是当你试图改小的时候,gcc没有任何编译提示信息。
gcc可以接受一个宏或编译期数值:
#define XX 1
struct __attribute__((__aligned__((XX)))) MyStruct_1 {}; // OK
template
struct __attribute__((__aligned__((YY)))) MyStruct_2 {}; // OK
static const unsigned ZZ = 1;
struct __attribute__((__aligned__((ZZ)))) MyStruct_3 {};
// ^
// error: requested alignment is not an integer constant
gcc的__attribute__((__aligned__((#))))支持的上限受限于链接器(Note that the effectiveness of aligned attributes may be limited by inherent limitations in your linker)。
同样的,在C++98/03里,不同的编译器可能有不同的方法来获得一个类型的内存对齐。
MSVC使用__alignof操作符获得内存对齐大小:
MyStruct xx;
std::cout << __alignof(xx) << std::endl;
std::cout << __alignof(MyStruct) << std::endl;
gcc则使用__alignof__:
MyStruct xx;
std::cout << __alignof__(xx) << std::endl;
std::cout << __alignof__(MyStruct) << std::endl;
需要注意的是,不论是__alignof还是__alignof__,对于对齐的计算都发生在编译期。因此像下面这样写:
int a;
char& c = reinterpret_cast
std::cout << __alignof__(c) << std::endl;
得到的结果将是1。
如果需要在运行时动态计算一个变量的内存对齐,比如根据一个void*指针指向的内存地址来判断这个地址的内存对齐是多少,我们可以用下面这个简单的方法:
__declspec(align(128)) long a = 0;
size_t x = reinterpret_cast
x &= ~(x - 1); // 计算a的内存对齐大小
std::cout << x << std::endl;
用这种方式得到的内存对齐大小可能比实际的大,因为它是切实的获得这个内存地址到底能被多大的2^N整除。
我们在讨论内存对齐的时候很容易忽略掉堆内存。我们经常会使用malloc分配内存,却不理会这块内存的对齐方式,仿佛堆内存不需要考虑内存对齐一样。
实际上,malloc一般使用当前平台默认的最大内存对齐数对齐内存。比如MSVC在32位下一般是8字节对齐;64位下则是16字节(In Visual C++, the fundamental alignment is the alignment that's required for a double, or 8 bytes. In code that targets 64-bit platforms, it’s 16 bytes)。这样对于常规的数据都是没有问题的。
但是如果我们自定义的内存对齐超出了这个范围,则是不能直接使用malloc来获取内存的。
当我们需要分配一块具有特定内存对齐的内存块时,在MSVC下应当使用_aligned_malloc;而在gcc下一般使用memalign等函数。
其实自己实现一个简易的aligned_malloc是很容易的:
#include
inline void* aligned_malloc(size_t size, size_t alignment)
{
// 检查alignment是否是2^N
assert(!(alignment & (alignment - 1)));
// 计算出一个最大的offset,sizeof(void*)是为了存储原始指针地址
size_t offset = sizeof(void*) + (--alignment);
// 分配一块带offset的内存
char* p = static_cast(malloc(offset + size));
if (!p) return nullptr;
// 通过“& (~alignment)”把多计算的offset减掉
void* r = reinterpret_cast(reinterpret_cast(p + offset) & (~alignment));
// 将r当做一个指向void*的指针,在r当前地址前面放入原始地址
static_cast(r)[-1] = p;
// 返回经过对齐的内存地址
return r;
}
inline void aligned_free(void* p)
{
// 还原回原始地址,并free
free(static_cast(p)[-1]);
}
C++11标准里统一了内存对齐的相关操作。
指定内存对齐使用alignas说明符:
alignas(32) long long a = 0;
#define XX 1
struct alignas(XX) MyStruct_1 {}; // OK
template
struct alignas(YY) MyStruct_2 {}; // OK
static const unsigned ZZ = 1;
struct alignas(ZZ) MyStruct_3 {}; // OK
注意到MyStruct_3编译是OK的。在C++11里,只要是一个编译期数值(包括static const)都支持alignas(the assignment-expression shall be an integral constant expression,参考ISO/IEC-14882:2011,7.6.2 Alignment specifier,第2款)。
但是需要小心的是,目前微软的编译器(Visual C++ Compiler November 2013 CTP)在MyStruct_3的情况下仍然会报error C2057。
另外,alignas同前面介绍的__declspec、__attribute__一样,只能改大不能改小(参考ISO/IEC-14882:2011,7.6.2 Alignment specifier,第5款)。如果需要改小,比如设置对齐为1的话,仍然需要使用#pragma pack。或者,可以使用C++11里#pragma的等价物_Pragma(微软暂不支持这个):
_Pragma("pack(1)")
struct MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
_Pragma("pack()")
除了这些之外,alignas比__declspec、__attribute__强大的地方在于它还可以这样用:
alignas(int) char c;
这个char就按int的方式对齐了。
获取内存对齐使用alignof操作符:
MyStruct xx;
std::cout << alignof(xx) << std::endl;
std::cout << alignof(MyStruct) << std::endl;
相关注意点和前面介绍的__alignof、__alignof__并无二致。
除了alignas和alignof,C++11中还提供了几个有用的工具。
A. std::alignment_of
功能是编译期计算类型的内存对齐。
std里提供这个是为了补充alignof的功能。alignof只能返回一个size_t,而alignment_of则继承自std::integral_constant,因此拥有value_type、type、operator()等接口(或者说操作)。
B. std::aligned_storage
这是个好东西。我们知道,很多时候需要分配一块单纯的内存块,比如new char[32],之后再使用placement new在这块内存上构建对象:
char xx[32];
::new (xx) MyStruct;
但是char[32]是1字节对齐的,xx很有可能并不在MyStruct指定的对齐位置上。这时调用placement new构造内存块,可能会引起效率问题或出错,这时我们应该使用std::aligned_storage来构造内存块:
std::aligned_storage
::new (&xx) MyStruct;
需要注意的是,当使用堆内存的时候我们可能还是需要aligned_malloc。因为现在的编译器里new并不能在超出默认最大对齐后,还能保证内存的对齐是正确的。比如在MSVC 2013里,下面的代码:
struct alignas(32) MyStruct
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
long long d; // 8 bytes
char e; // 1 byte
};
void* p = new MyStruct;
// warning C4316: 'MyStruct' : object allocated on the heap may not be aligned 32
将会得到一个编译警告。
C. std::max_align_t
返回当前平台的最大默认内存对齐类型。malloc返回的内存,其对齐和max_align_t类型的对齐大小应当是一致的。
我们可以通过下面这个方式获得当前平台的最大默认内存对齐数:
std::cout << alignof(std::max_align_t) << std::endl;
D. std::align
这货是一个函数,用来在一大块内存当中获取一个符合指定内存要求的地址。
看下面这个例子:
char buffer[] = "------------------------";
void * pt = buffer;
std::size_t space = sizeof(buffer) - 1;
std::align(alignof(int), sizeof(char), pt, space);
意思是,在buffer这个大内存块中,指定内存对齐为alignof(int),找一块sizeof(char)大小的内存,并在找到这块内存后,将地址放入pt,将buffer从pt开始的长度放入space。
关于这个函数的更多信息,可以参考这里。
关于内存对齐,该说的就是这么多了。我们经常会看到内存对齐的应用,是在网络收发包中。一般用于发送的结构体,都是1字节对齐的,目的是统一收发双方(可能处于不同平台)之间的数据内存布局,以及减少不必要的流量消耗。
C++11中为我们提供了不少有用的工具,可以让我们方便的操作内存对齐。但是在堆内存方面,我们很可能还是需要自己想办法。不过在平时的应用中,因为很少会手动指定内存对齐到大于系统默认的对齐数,所以倒也不比每次new/delete的时候都提心吊胆。