问题:
我们在写程序的时候经常发现程序使用的内存往往比我们申请的多,为了优化程序的内存占用,搅尽脑汁想要优化内存占用,可是发现自己的代码也无从优化了,怎么办?现在我们把我们的焦点放到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
(对于上面计算的由来可以参见这篇文章的第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
STL内存池在发现某个规则的内存用完了时,会进行refill,在进行chunk_alloc
例如8Byte大小的空间没有了,调用refill,refill会将其空间准备20个,也就是20*8,当然refill做不了内存分配,他把20个8Byte的需求提交给chunk_alloc
chunk_alloc 能真正分配内存,但是它分配的时候会将内存空间*2,所以最终malloc的内存为8*20*2=320 ,32bit系统给malloc的内存为328,64bit系统给malloc的内存为336
在32位和64位操作系统分别浪费掉8Byte和16Byte,其实我们可以在chunk_alloc内部简单的计算一下系统的内存对齐,达到 chunk_alloc 级零浪费... 至于 allocate级别的浪费,我想是避免不了了,譬如,我需要一个6Byte的空间,STL内存池给我的确实8Byte
参考资料: