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()")

alignofstd::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:-*---*---*---*---*---*--