探究OS的内存分配对齐策略 (转)
问题:
我们在写程序的时候经常发现程序使用的内存往往比我们申请的多,为了优化程序的内存占用,搅尽脑汁想要优化内存占用,可是发现自己的代码也无从优化了,怎么办?现在我们把我们的焦点放到malloc上,毕竟我们向系统申请的内存都是通过它完成了,不了解他,也就不能彻底的优化内存占用。
来个小例子
//g++ -o malloc_addr_vec mallc_addr_vec.cpp 编译
2 #include<iostream>
3 using namespace std;
4 int main(int argc, char *argv[])
5 {
6 int malloc_size = atoi(argv[1]);
7 char * malloc_char;
8 for (size_t i = 0; i < 1024*1024; ++i) {
9 malloc_char = new char[malloc_size];
10 }
11 while (1) {}//此时查看内存占用
12 return 0;
13 }
本文的测试环境为Linux 64Bit ,使用G++编译为可执行文件后,使用不同的启动参数启动,使用top命令查看程序占用的内存,这里我们主要是看RES指标
RES -- Resident size (kb)
The non-swapped physical memory a task has used.
测试案例:
1.每次new 1 Byte Do 1024*1024次
./malloc_addr_vec 1
启动程序后的内存占用
内存消耗 32MB
2.每次new 24 Byte Do 1024*1024次
./malloc_addr_vec 24
启动程序后的内存占用
内存消耗32MB
3.每次new 25 Byte Do 1024*1024次
./malloc_addr_vec 25
启动程序后的内存占用
内存消耗48MB
为什么我们每次new 1Byte 和每次 new 24Byte系统消耗的内存一样呢?,为什么每次new 25Byte和 每次new 24Byte占用的内存完全不同呢?
不知道大家在写程序的时候有没有关注过这个问题。我一次遇到时,吐槽一句:What the fuck malloc.
原因分析:
在大多数情况下,编译器和C库透明地帮你处理对齐问题。POSIX 标明了通过malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。
对齐参数(MALLOC_ALIGNMENT) 大小的设定并需满足两个特性
1.必须是2的幂
2.必须是(void *)的整数倍
至于为什么会要求是(void *)的整数倍,这个目前我还不太清楚,等你来发现...
根据这个原理,在32位和64位的对齐单位分别为8字节和16字节
但是这并解释不了上面的测试结果,这是因为系统malloc分配的最小单位(MINSIZE)并不是对齐单位
为了进一步了解细节,从GNU网站中把glibc源码下载下来,查看其 malloc.c文件
View Code
1 #ifndef INTERNAL_SIZE_T
2 #define INTERNAL_SIZE_T size_t
3 #endif
4 #define SIZE_SZ (sizeof(INTERNAL_SIZE_T))
5 #ifndef MALLOC_ALIGNMENT
6 #define MALLOC_ALIGNMENT (2 * SIZE_SZ)
7 #endif
8
9
10 struct malloc_chunk {
11 INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
12 INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
13 struct malloc_chunk* fd; /* double links -- used only if free. */
14 struct malloc_chunk* bk;
15 };
16
17 An allocated chunk looks like this:
18 chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19 | Size of previous chunk, if allocated | |
20 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21 | Size of chunk, in bytes |M|P|
22 mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23 | User data starts here... .
24 . .
25 . (malloc_usable_size() bytes) .
26 . |
27 nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
28 | Size of chunk |
29 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
30
31
32 #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
33 #define MIN_CHUNK_SIZE (sizeof(struct malloc_chunk))
34 #define MINSIZE /
35 (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
36 /* pad request bytes into a usable size -- internal version */
37 #define request2size(req) /
38 (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? /
39 MINSIZE : /
40 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
其中request2size这个宏就是glibc的内存对齐操作,MINSIZE就是使用malloc时占用内存的最小单位。根据宏定义可推算在32位系统中MINSIZE为16字节,在64位系统中MINSIZE一般为32字节。从request2size还可以知道,如果是64位系统,申请内存为1~24字节时,系统内存消耗32字节,当申请内存为25字节时,系统内存消耗48字节。 如果是32位系统,申请内存为1~12字节时,系统内存消耗16字节,当申请内存为13字节时,系统内存消耗24字节。
一般他们的差距是一个指针大小,计算公式是
max(MINSIZE,in_use_size)
其中in_use_size=(要求大小+2*指针大小-指针大小)align to MALLOC_ALIGNMENT
(对于上面计算的由来可以参见glibc 内存池管理 ptmalloc这篇文章的第4节chuck部分以及搜一下malloc的内部实现源码 )
为了证明这个理论的正确性,我们需要计算一次malloc到底花掉了多少内存,我们用如下代码分别在32bit Linux和 64bit Linux上做测试
2 #include<stdio.h>
3 #include<stdlib.h>
4 int main()
5 {
6 char * p1;
7 char * p2;
8 int i=1;
9 printf("%d\n",sizeof(char *));
10 for(;i<100;i++)
11 {
12 p1=NULL;
13 p2=NULL;
14 p1=(char *)malloc(i*sizeof(char));
15 p2=(char *)malloc(1*sizeof(char));
16 printf("i=%d %d\n",i,(p2-p1));
17 }
18
19 getchar();
20 }
其测试结果如下:
32bit
View Code
1 ---------------------
2 Linux 32bit
3 ---------------------
4 4
5 i=1 16
6 i=2 16
7 i=3 16
8 i=4 16
9 i=5 16
10 i=6 16
11 i=7 16
12 i=8 16
13 i=9 16
14 i=10 16
15 i=11 16
16 i=12 16
17 i=13 24
18 i=14 24
19 i=15 24
20 i=16 24
21 i=17 24
22 i=18 24
23 i=19 24
24 i=20 24
25 i=21 32
26 i=22 32
27 i=23 32
28 i=24 32
29 i=25 32
30 i=26 32
31 i=27 32
32 i=28 32
33 i=29 40
34 i=30 40
35 i=31 40
36 i=32 40
37 i=33 40
38 i=34 40
39 i=35 40
40 i=36 40
41 i=37 48
42 i=38 48
43 i=39 48
44 i=40 48
45 i=41 48
46 i=42 48
47 i=43 48
48 i=44 48
49 i=45 56
50 i=46 56
51 i=47 56
52 i=48 56
53 i=49 56
54 i=50 56
55 i=51 56
56 i=52 56
57 i=53 64
58 i=54 64
59 i=55 64
60 i=56 64
61 i=57 64
62 i=58 64
63 i=59 64
64 i=60 64
65 i=61 72
66 i=62 72
67 i=63 72
68 i=64 72
69 i=65 72
70 i=66 72
71 i=67 72
72 i=68 72
73 i=69 80
74 i=70 80
75 i=71 80
76 i=72 80
77 i=73 80
78 i=74 80
79 i=75 80
80 i=76 80
81 i=77 88
82 i=78 88
83 i=79 88
84 i=80 88
85 i=81 88
86 i=82 88
87 i=83 88
88 i=84 88
89 i=85 96
90 i=86 96
91 i=87 96
92 i=88 96
93 i=89 96
94 i=90 96
95 i=91 96
96 i=92 96
97 i=93 104
98 i=94 104
99 i=95 104
100 i=96 104
101 i=97 104
102 i=98 104
103 i=99 104
64bit
View Code
1 -------------------
2 Linux 64bit
3 -------------------
4 8
5 i=1 32
6 i=2 32
7 i=3 32
8 i=4 32
9 i=5 32
10 i=6 32
11 i=7 32
12 i=8 32
13 i=9 32
14 i=10 32
15 i=11 32
16 i=12 32
17 i=13 32
18 i=14 32
19 i=15 32
20 i=16 32
21 i=17 32
22 i=18 32
23 i=19 32
24 i=20 32
25 i=21 32
26 i=22 32
27 i=23 32
28 i=24 32
29 i=25 48
30 i=26 48
31 i=27 48
32 i=28 48
33 i=29 48
34 i=30 48
35 i=31 48
36 i=32 48
37 i=33 48
38 i=34 48
39 i=35 48
40 i=36 48
41 i=37 48
42 i=38 48
43 i=39 48
44 i=40 48
45 i=41 64
46 i=42 64
47 i=43 64
48 i=44 64
49 i=45 64
50 i=46 64
51 i=47 64
52 i=48 64
53 i=49 64
54 i=50 64
55 i=51 64
56 i=52 64
57 i=53 64
58 i=54 64
59 i=55 64
60 i=56 64
61 i=57 80
62 i=58 80
63 i=59 80
64 i=60 80
65 i=61 80
66 i=62 80
67 i=63 80
68 i=64 80
69 i=65 80
70 i=66 80
71 i=67 80
72 i=68 80
73 i=69 80
74 i=70 80
75 i=71 80
76 i=72 80
77 i=73 96
78 i=74 96
79 i=75 96
80 i=76 96
81 i=77 96
82 i=78 96
83 i=79 96
84 i=80 96
85 i=81 96
86 i=82 96
87 i=83 96
88 i=84 96
89 i=85 96
90 i=86 96
91 i=87 96
92 i=88 96
93 i=89 112
94 i=90 112
95 i=91 112
96 i=92 112
97 i=93 112
98 i=94 112
99 i=95 112
100 i=96 112
101 i=97 112
102 i=98 112
103 i=99 112
了解了malloc的内存对其原理后,对于程序的内存占用的优化又有了有的放矢。我们可以根据内存对齐的原则来请求内存,来制作我们的高效内存池,从而避免隐形的资源浪费.
例如,目前STL的内存池是以8Byte为对齐单位,内存池free_list大小为
free_list[0] --------> 8 byte
free_list[1] --------> 16 byte
free_list[2] --------> 24 byte
free_list[3] --------> 32 byte
... ...
free_list[15] -------> 128 byte
我们可以将其优化为
32bit OS 16-4+n*8
64bit OS 32-8+n*16
n=(0,1,2,3.....max)
这样,占用同样大小的内存池,可用性会更高...
摘自 大熊 | Zealot Yin