c++ 内存对齐

警告
本文最后更新于 2024-03-12,文中内容可能已过时。

我们在设计结构体时,需要慎重考虑内存对齐的问题,因为不同的内存对齐方式对程序的性能有极大的影响。

CPU 读取内存的最小有效值

计算机的内存是按照 byte(8 bits) 进行有序排序,理论上,我们可以在最小有效值为 1 byte 进行随机内存读取。然而,如果每次都是按照这个节奏,每取一个 int(假设为 4byte) 变量,CPU 都需要进行 4 次操作,毫无疑问效率极低。

我们知道 CPU 架构有 32bit64bit 两种,其含义是在 CPU 每次从内存地址取值时,是以对应最小有效内存地址进行快速操作的。简单讲

  • 对于 32bit,最优操作为每次取 4 byte 的内存空间进行识别
  • 对于 64bit,最优操作为每次取 8 byte 的内存空间进行识别

现在的操作系统一般都是 64bit 了,所以每次的最佳取内存地址为 8byte,这也是我们经常说的,内存对齐按照 8byte,也是编译器 gcc 默认采用的大小。

1
#pragama pack(8)

如果计算 struct 的内存大小

  1. 先计算操作系统 CPU 的对齐单位(一般为 8byte),根据结构体成员最大的内存单位,取 两者的最小值作为有效对齐单位(x)
  2. 对于结构体的每一个成员,取其大小与有效对齐单位的较小者,作为单个成员的有效单位(e)。第一个成员变量的偏移值从 0 开始,按照每个成员的有效对齐单位排列,不足之处进行补位填充
  3. 最后,整个结构体还需要按照有效对齐单位(x)的整数倍进行填充
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
using namespace std;

#pragma pack(push)
#pragma pack(4)
struct
{
    int a;
    double b;
    short c;
} u;
#pragma pack(pop)

#pragma pack(push)
#pragma pack(1)
struct
{
    int a;
    double b;
    short c;
} v;
#pragma pack(pop)

struct
{
    int a;
    double b;
    short c;
} x;

struct
{
    int a;
    char b;
    short c;
} y;

struct
{
    int a;
    char b;
    short c;
} __attribute__((packed)) z;

int main()
{
    cout << "sizeof(u):" << sizeof(u) << endl;
    cout << "sizeof(v):" << sizeof(v) << endl;
    cout << "sizeof(x):" << sizeof(x) << endl;
    cout << "sizeof(y):" << sizeof(y) << endl;
    cout << "sizeof(z):" << sizeof(z) << endl;
    return 0;
}
  • sizeof(u):16, int(4) + double(8) + short(2) = (14/4+1) * 4 = 16

  • sizeof(v):14, int(4) + double(8) + short(2) = 14/1 = 14

  • sizeof(x):24

      1. 结构体 x 的最大成员 (double)b 的内存占用 8,操作系统64位,则有效对齐单位 x = min(8,8) = 8
      1. 对于结构体的每个成员
      • sizeof(a) = 4 <= 8, 则按照4的倍数进行偏移(4byte),占用4个字节,已用:4
      • sizeof(b) = 1 <= 8, 则按照1的倍数进行偏移,从(4byte)开始,先对齐 8 byte,占用 8 byte,已用: 4 + 4(填充) + 8 = 16
      • sizeof(c) = 2 <= 8, 则按照2的倍数进行偏移,从上面 16 后面按照 2 的倍数,占用两个字节,已用:16 + 2 = 18
      1. 最后,整个结构体再按照 x=8 的倍数对齐,(18/8+1) * 8 = 24,所以整个结构体大小为24字节
      1. 内存空间为
    1
    2
    
    xxxx  ____   xxxxxxxx xx ______
     4    pad(4) 8        2  pad(6)
  • sizeof(y):8

      1. 结构体 y 的最大成员 a 的内存占用 4,操作系统64位,则有效对齐单位 x = min(4,8) = 4
      1. 对于结构体的每个成员
      • sizeof(a) = 4 <= 4, 则按照4的倍数进行偏移(4byte),占用4个字节,已用:4
      • sizeof(b) = 1 <= 4, 则按照1的倍数进行偏移,从(4byte)开始,占用一个字节,已用: 4 + 1 = 5
      • sizeof(c) = 2 <= 4, 则按照2的倍数进行偏移,从上面 5 后面按照 2 的倍数,占用两个字节,已用:5 + 1(填充) + 2 = 8
      1. 最后,整个结构体再按照 x=4 的倍数对齐,刚好为 8,所以整个结构体大小为8字节
  • sizeof(z):7

    • 我们告诉编译器不用对齐,所以按照实际占用的字节占用 7 byte.

使用 pack 指定对齐

可以使用编译器提供的 pack(n) 指定对齐大小:

1
#pragma pack(n)

或者使用压栈的方式

1
2
3
#pragma pack(push, n)

#pragma pop()

不同内存对齐影响程序性能

1
2
3
4
5
## 可以看到,在 64 位操作系统中,最佳实践为 8byte 对齐
SampleStructPack1: 1000000000000000000 bytes allocated in 8202 nanoseconds
SampleStructPack2: 1200000000000000000 bytes allocated in 276 nanoseconds
SampleStructPack4: 1600000000000000000 bytes allocated in 205 nanoseconds
SampleStructPack4: 1600000000000000000 bytes allocated in 131 nanoseconds

程序如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#include <iostream>
#include <chrono>

#pragma pack (1)
struct SampleStructPack1
{
    bool flag;
    unsigned int timeout;
};
//#pragma pack(0)

#pragma pack (2)
struct SampleStructPack2
{
    bool flag;
    unsigned int timeout;
};
// #pragma pack(0)

#pragma pack (4)
struct SampleStructPack4
{
    bool flag;
    unsigned int timeout;
};
// #pragma pack(0)

#pragma pack (8)
struct SampleStructPack8
{
    bool flag;
    unsigned int timeout;
};
// #pragma pack(0)

struct SampleStruct
{
    bool flag;
    unsigned int timeout;
};

static const long long MAX_ELEMENTS = 200000000000000000;
using namespace std;
using namespace std::chrono;

void allocate1()
{
    SampleStructPack1 elements [MAX_ELEMENTS];
    cout << "SampleStructPack1: " << sizeof(elements) << " bytes allocated";
}

void allocate2()
{
    SampleStructPack2 elements [MAX_ELEMENTS];
    cout << "SampleStructPack2: " << sizeof(elements) << " bytes allocated";
}

void allocate4()
{
    SampleStructPack4 elements [MAX_ELEMENTS];
    cout << "SampleStructPack4: " << sizeof(elements) << " bytes allocated";
}

void allocate8()
{
    SampleStructPack8 elements [MAX_ELEMENTS];
    cout << "SampleStructPack8: " << sizeof(elements) << " bytes allocated";
}

void chrono1()
{
    auto begin = high_resolution_clock::now() ;
    allocate1();
    cout << " in " << duration_cast<nanoseconds>(high_resolution_clock::now() - begin).count() << " nanoseconds" << endl;
}

void chrono2()
{
    auto begin = high_resolution_clock::now() ;
    allocate2();
    cout << " in " << duration_cast<nanoseconds>(high_resolution_clock::now() - begin).count() << " nanoseconds" << endl;
}

void chrono4()
{
    auto begin = high_resolution_clock::now() ;
    allocate4();
    cout << " in " << duration_cast<nanoseconds>(high_resolution_clock::now() - begin).count() << " nanoseconds" << endl;
}

void chrono8()
{
    auto begin = high_resolution_clock::now() ;
    allocate4();
    cout << " in " << duration_cast<nanoseconds>(high_resolution_clock::now() - begin).count() << " nanoseconds" << endl;
}

int main(int argc, char *argv[])
{
    chrono1();
    chrono2();
    chrono4();
    chrono8();

    return 0;
}

相关内容

william 支付宝支付宝
william 微信微信
0%