内存对齐

转载: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:

 
  1. __declspec(align(1)) struct MyStruct // ...

  2. // 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)。

5. 获得内存对齐

同样的,在C++98/03里,不同的编译器可能有不同的方法来获得一个类型的内存对齐。

MSVC使用__alignof操作符获得内存对齐大小:

 
  1. MyStruct xx;

  2. std::cout << __alignof(xx) << std::endl;

  3. std::cout << __alignof(MyStruct) << std::endl;


gcc则使用__alignof__:

 
  1. MyStruct xx;

  2. std::cout << __alignof__(xx) << std::endl;

  3. std::cout << __alignof__(MyStruct) << std::endl;


需要注意的是,不论是__alignof还是__alignof__,对于对齐的计算都发生在编译期。因此像下面这样写:

 
  1. int a;

  2. char& c = reinterpret_cast(a);

  3. std::cout << __alignof__(c) << std::endl;

 

得到的结果将是1。

如果需要在运行时动态计算一个变量的内存对齐,比如根据一个void*指针指向的内存地址来判断这个地址的内存对齐是多少,我们可以用下面这个简单的方法:

 
  1. __declspec(align(128)) long a = 0;

  2. size_t x = reinterpret_cast(&a);

  3. x &= ~(x - 1); // 计算a的内存对齐大小

  4. std::cout << x << std::endl;

 

用这种方式得到的内存对齐大小可能比实际的大,因为它是切实的获得这个内存地址到底能被多大的2^N整除。

6. 堆内存的内存对齐

我们在讨论内存对齐的时候很容易忽略掉堆内存。我们经常会使用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]);

}

7. C++11中对内存对齐的操作

C++11标准里统一了内存对齐的相关操作。

指定内存对齐使用alignas说明符:

 
  1. alignas(32) long long a = 0;

  2.  
  3. #define XX 1

  4. struct alignas(XX) MyStruct_1 {}; // OK

  5.  
  6. template

  7. struct alignas(YY) MyStruct_2 {}; // OK

  8.  
  9. static const unsigned ZZ = 1;

  10. 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(微软暂不支持这个):

 
  1. _Pragma("pack(1)")

  2. struct MyStruct

  3. {

  4. char a; // 1 byte

  5. int b; // 4 bytes

  6. short c; // 2 bytes

  7. long long d; // 8 bytes

  8. char e; // 1 byte

  9. };

  10. _Pragma("pack()")


除了这些之外,alignas比__declspec、__attribute__强大的地方在于它还可以这样用:

alignas(int) char c;


这个char就按int的方式对齐了。
获取内存对齐使用alignof操作符:

 
  1. MyStruct xx;

  2. std::cout << alignof(xx) << std::endl;

  3. 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在这块内存上构建对象:

 
  1. char xx[32];

  2. ::new (xx) MyStruct;


但是char[32]是1字节对齐的,xx很有可能并不在MyStruct指定的对齐位置上。这时调用placement new构造内存块,可能会引起效率问题或出错,这时我们应该使用std::aligned_storage来构造内存块:

 
  1. std::aligned_storage::type xx;

  2. ::new (&xx) MyStruct;


需要注意的是,当使用堆内存的时候我们可能还是需要aligned_malloc。因为现在的编译器里new并不能在超出默认最大对齐后,还能保证内存的对齐是正确的。比如在MSVC 2013里,下面的代码:

 
  1. struct alignas(32) MyStruct

  2. {

  3. char a; // 1 byte

  4. int b; // 4 bytes

  5. short c; // 2 bytes

  6. long long d; // 8 bytes

  7. char e; // 1 byte

  8. };

  9.  
  10. void* p = new MyStruct;

  11. // 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

这货是一个函数,用来在一大块内存当中获取一个符合指定内存要求的地址。
看下面这个例子:

 
  1. char buffer[] = "------------------------";

  2. void * pt = buffer;

  3. std::size_t space = sizeof(buffer) - 1;

  4. std::align(alignof(int), sizeof(char), pt, space);

 

意思是,在buffer这个大内存块中,指定内存对齐为alignof(int),找一块sizeof(char)大小的内存,并在找到这块内存后,将地址放入pt,将buffer从pt开始的长度放入space。

关于这个函数的更多信息,可以参考这里。


关于内存对齐,该说的就是这么多了。我们经常会看到内存对齐的应用,是在网络收发包中。一般用于发送的结构体,都是1字节对齐的,目的是统一收发双方(可能处于不同平台)之间的数据内存布局,以及减少不必要的流量消耗。

C++11中为我们提供了不少有用的工具,可以让我们方便的操作内存对齐。但是在堆内存方面,我们很可能还是需要自己想办法。不过在平时的应用中,因为很少会手动指定内存对齐到大于系统默认的对齐数,所以倒也不比每次new/delete的时候都提心吊胆。

你可能感兴趣的:(C语言)