C++中的内存对齐
内存对齐简介
内存对齐,是一个数据所能存放的内存地址的属性,它是一个无符号整数,且必须是2的幂。一个数据类型的内存对齐为$n=2^k$是指这个数据类型定义出来的所有变量的内存地址都是$n$的倍数。
内存对齐是为了提高CPU读取数据的效率。并不是所有硬件平台都能够随意访问任意位置的内存,一些平台的CPU,若读取的数据是未对齐的,将拒绝访问或抛出硬件异常。
对于基本数据类型,其对齐大小与其数据类型大小相等,例如int
型数据的内存对齐值在默认情况下为4。
对于一个结构体类型,其内存对齐遵循如下规则:
- 假设结构体起始地址为
0x0
,结构体第一个成员相对结构个体首地址的偏移量为0,此后每个成员的对齐值为其自身对齐大小与#progama pack
指定的数值中较小的那个。
- 结构体中所有成员完成对齐后,结构体本身的内存对齐值为其所有成员中对齐值最大的那个。
稍后将说明,由于第二条规则的存在,结构体本身的起始地址将不会影响以内部成员的相对位置。即将每个成员的对齐值作为其相对结构体起始地址的偏移量时,无论结构体本身的起始地址是多少,其内部成员的地址总是满足内存对齐规则(即其地址是其内存对齐值的整数倍)。
例如,对于以下结构体:
1
2
3
4
5
6
7
|
struct A{
char a; //1 byte
int b; //4 bytes
short c; //2 bytes
long long d; //8 bytes
char e; //1 byte
};
|
按照上述规则,其在内存中的布局(64位)类似于这样:
1
2
3
4
5
6
7
8
9
10
|
struct A{
char a; //1 byte alignof(a)=1 offset:0x0=0*1
char padding1[3]; //padding
int b; //4 bytes alignof(b)=4 offset:0x4=1*4
short c; //2 bytes alignof(c)=2 offset:0x08=4*2
char padding2[6]; //padding
long long d; //8 bytes alignof(d)=8 offset:0x10=2*8
char e; //1 byte alignof(e)=1 offset:0x18=24*1
char padding3[7]; //padding
}; //alignof(A)=8
|
上述结构体本身的对齐值为8,即其成员对齐值得最大值,若我们强制令该结构体对对齐值小于8,则其成员的对齐值也将被强制不超过该值,因为只有当结构体本身的对齐值不小于其中成员的最大对齐值时,结构体中成员的相对位置才能保持固定。
例如,假设上述结构体对齐值为1,而其成员仍然按照自然对齐方式分布,则当该结构体起始地址为0x1
时,其内存布局如下所示:
1
2
3
4
5
6
7
8
9
|
struct A{ //address:0x1
char a; //1 byte alignof(a)=1 address:0x1 offset:0x0
char padding1[2]; //padding
int b; //4 bytes alignof(b)=4 address:0x4 offset:0x3
short c; //2 bytes alignof(c)=2 address:0x8 offset:0x7
char padding2[8]; //padding
long long d; //8 bytes alignof(d)=8 address:0x10 offset:0xE
char e; //1 byte alignof(e)=1 address:0x19 offset:0x18
}; //alignof(A)=1
|
而当该结构体的起始地址为0x3
时,其内存分布变为如下所示:
1
2
3
4
5
6
7
8
|
struct A{ //address:0x3
char a; //1 byte alignof(a)=1 address:0x3 offset:0x0
int b; //4 bytes alignof(b)=4 address:0x4 offset:0x1
short c; //2 bytes alignof(c)=2 address:0x8 offset:0x5
char padding2[8]; //padding
long long d; //8 bytes alignof(d)=8 address:0x10 offset:0xC
char e; //1 byte alignof(e)=1 address:0x19 offset:0x16
}; //alignof(A)=1
|
以此类推,当上述结构体按一字节对齐而不强制其成员按照一字节对齐时,结构体中成员的相对分布共有四种可能。结构体内部成员的相对位置分布随结构体本身的起始地址发生改变而发生变化显然不是我们所期望的。
而在保证了结构体本身的对齐值不小于其中成员的最大对齐值时,其成员的内存对齐总是可以满足的
1
2
3
4
5
6
7
8
9
10
|
struct A{ //address: n=8^k
char a; //1 byte alignof(a)=1 offset:0x0=0*1 address: n+1=(8^k+1)*1
char padding1[3]; //padding
int b; //4 bytes alignof(b)=4 offset:0x4=1*4 address: n+4=(2^(3k-2)+1)*4
short c; //2 bytes alignof(c)=2 offset:0x08=4*2 address: n+8=(2^(3k-1)+1)*2
char padding2[6]; //padding
long long d; //8 bytes alignof(d)=8 offset:0x10=2*8 address: n+16=(b^(k-1)+1)*8
char e; //1 byte alignof(e)=1 offset:0x18=24*1 address: n+24=(n+24)*1
char padding3[7]; //padding
}; //alignof(A)=8
|
C++中与内存对齐有关的操作
alignas(size_t sz)
可以用来改变内存对齐大小
1
2
3
4
5
6
7
8
9
10
11
|
alignas(32) long long a=0;
alignas(int) char cc;
#define sz 1
struct alignas(sz) A{};
template<size_t t=8>
struct alignas(t) B{};
static const unsigned int sz_2=16;
struct alignas(sz_2) C{};
|
alignas
只能改大不能改小。如需要改小,需要使用#pragma pack
或_Pragma
(MSVC不支持)
1
2
3
4
5
6
7
8
9
|
_Pragma("pack(1)")
struct A{
char a;
int b;
short c;
long long d;
char e;
};
_Pragma("pack()")
|
alignof
和std::alignment_of
可以获取内存对齐大小,std::alignment_of
原型为
1
2
3
4
5
|
// STRUCT TEMPLATE alignment_of
template <class _Ty>
struct alignment_of : integral_constant<size_t, alignof(_Ty)> {}; // determine alignment of _Ty
template <class _Ty>
_INLINE_VAR constexpr size_t alignment_of_v = alignof(_Ty);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct A{
char a;
int b;
short c;
long long d;
char e;
};
A a;
size_t a=alignof(A);
size_t b=alignof(a);
size_t c=std::alignment_of<A>::value;
size_t d=std::alignment_of_v<A>;
|
std::aligned_storage
可以看成内存对齐的缓冲区,通常与placement new
结合使用,其原型为
1
2
3
4
5
6
7
8
|
template <size_t _Len, size_t _Align = alignof(max_align_t)>
struct aligned_storage { // define type with size _Len and alignment _Align
using _Next = char;
static constexpr bool _Fits = _Align <= alignof(_Next);
using type = typename _Aligned<_Len, _Align, _Next, _Fits>::type;
};
template <size_t _Len, size_t _Align = alignof(max_align_t)>
using aligned_storage_t = typename aligned_storage<_Len, _Align>::type;
|
例如,我们要分配一块单独的内存块,之后在这块内存上构建对象:
1
2
|
char adr[32];
::new (adr) A;
|
但adr
是一字节对齐的,adr
有可能不在满足A
内存对齐的位置上,这时用placement new
可能会引起效率问题或出错,此时应该用std::aligned_storage
构造内存块
1
2
|
std::alilgned_storage_t<sizeof(A),std::alignment_of_v(A)> adr2;
::new (&adr2) A;
|
std::max_align_t
可以返回当前平台的最大默认内存对齐类型。对于malloc
返回的内存,其对齐与std::maxalign_t
是一致的。
1
|
std::cout<<alignof(std::max_align_t)<<std::endl;
|
std::align
可以用来在一块内存中获取一个符合要求的地址,其原型如下:
(注意:旧版gcc std::align
缺失,可能无法使用 Bug53814)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// FUNCTION align
inline void* align(size_t _Bound, size_t _Size, void*& _Ptr, size_t& _Space) noexcept /* strengthened */ {
// try to carve out _Size bytes on boundary _Bound
size_t _Off = static_cast<size_t>(reinterpret_cast<uintptr_t>(_Ptr) & (_Bound - 1));
if (_Off != 0) {
_Off = _Bound - _Off; // number of bytes to skip
}
if (_Space < _Off || _Space - _Off < _Size) {
return nullptr;
}
// enough room, update
_Ptr = static_cast<char*>(_Ptr) + _Off;
_Space -= _Off;
return _Ptr;
}
|
从_Ptr
所指的位置开始往后Space
大小的内存块中,找一块大小为_Size
,内存对齐大小为_Bound
的内存,并将其地址放入_Ptr
中,返回改地址。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// align example
#include <iostream>
#include <memory>
int main() {
char buffer[] = "------------------------";
void * pt = buffer;
std::size_t space = sizeof(buffer)-1;
while ( std::align(alignof(int),sizeof(char),pt,space) ) {
char* temp = static_cast<char*>(pt);
*temp='*'; ++temp; space-=sizeof(char);
pt=temp;
}
std::cout << buffer << '\n';
return 0;
}
//output:-*---*---*---*---*---*--
|