关于glibc的malloc内存对齐

来源:互联网 发布:sql server 自然连接 编辑:程序博客网 时间:2024/05/16 17:54

http://blog.csdn.net/elpmis/article/details/4500917

 最近需要写一个程序,这程序对内存比较敏感,如果采用最简单的分配策略至少要16G,而我们的服务器一般也就16G物理内存。因此需要采取各种策略减少内存的使用。后来采用的方法是一开始只分配少量内存,后面需要的时候再分配。再次分配的时候需要copy旧的数据到新的内存块中,然后把旧内存块释放掉。为了减少copy的次数,新内存块是旧内存块的2倍,也就是指数增长。这种设计类似于C++中vector的内存分配策略。为什么不直接用vector呢?因为vector本身需要一定的空间,而我的程序有很多的内存分配目标(256M个),如果每一个用一个vector来管理的话,那也会占用很多内存,在64位Linux中占24Bytes,在32位Linux中占12Bytes。而如果自己管理内存的话,每个内存分配目标用一个指针,在64位Linux中只占8Bytes,在32位Linux中只占4Bytes。
    按照这种设计方法,我估计程序在64位Linux中占用内存应该在8G到10G左右,但是跑了很多次,最后都因为内存不足(大于16G)出现了Bus error。为什么会比想像中多占用这么多内存呢? 最开始的想法就是内存泄漏,但检查了很多遍程序,又用valgrind查看了好多遍,都没有发现有内存泄漏。后来想是不是申请内存的时候系统会分配额外的内存。因为我用的是glibc,然后就打算看一下glibc中的malloc是如何实现的,虽然我用C++中的new来申请内存,但new底层其实也还是调用malloc。
    我从GNU网站中把glibc2.5整个down了下来,然后看其中的malloc.c文件。从malloc.c文件中看到了一个memory alignment的概念,中文译过来可以说是内存对齐,它与一般的字节对齐意思不同,这里对齐是指在动态分配内存的时候,最终的分配的内存是某个2次幂的倍数。因此,分配内存会有一个最小分配单元,不管你申请多少,都不会少于某个值。在malloc.c中,有如下宏定义:

  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  

    其中MALLOC_ALIGNMENT这个宏就是对齐的单位,在32位系统中size_t一般4个字节,在64位系统中一般8个字节,那样在32位和64位的对齐单位分别为8字节和16字节。那是不是最小分配内存单位也是MALLOC_ALIGNMENT的大小呢?这里就要牵涉到glibc分配内存的管理机制。在glibc中空闲的内存块会有双链表连起来,每个链表结点就是一个管理单元,它的结构定义如下:

  1. struct malloc_chunk {  
  2.   INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */  
  3.   INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */  
  4.   struct malloc_chunk* fd;         /* double links -- used only if free. */  
  5.   struct malloc_chunk* bk;  
  6. };  
  7.     An allocated chunk looks like this:  
  8.     chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
  9.             |             Size of previous chunk, if allocated            | |  
  10.             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
  11.             |             Size of chunk, in bytes                       |M|P|  
  12.       mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
  13.             |             User data starts here...                          .  
  14.             .                                                               .  
  15.             .             (malloc_usable_size() bytes)                      .  
  16.             .                                                               |  
  17. nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
  18.             |             Size of chunk                                     |  
  19.             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  

    当一块内存分配出去后,该内存块的信息所占用内存也会随着分配出去,但用户是用不到这些内存的,如图中如示 mem-> 后面才是用户可用内存的起点。到这里,或许你已经想到,使用malloc时消耗系统内存的最小单位就是sizeof(struct malloc_chunk)。实际上,差不多就是这样,只是还需要将sizeof(struct malloc_chunk)作内存对齐。
malloc.c中有如下代码:

  1. #define MALLOC_ALIGN_MASK      (MALLOC_ALIGNMENT - 1)  
  2. #define MIN_CHUNK_SIZE        (sizeof(struct malloc_chunk))  
  3. #define MINSIZE  /  
  4.   (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))  
  5. /* pad request bytes into a usable size -- internal version */  
  6. #define request2size(req)                                         /  
  7.   (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?             /  
  8.    MINSIZE :                                                      /  
  9.    ((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字节。
    为了证实这个事实,我在64位linux上运行以下程序:

  1. //test.cpp  
  2. //g++ -Wall -o test test.cpp  
  3. #include <vector>  
  4. using namespace std;  
  5. int main(int argc, char *argv[])  
  6. {  
  7.     int malloc_size = atoi(argv[1]);  
  8.     vector<char *> malloc_vec(1 * 1024 * 1024);  
  9.     for (size_t i = 0; i < malloc_vec.size(); ++i) {  
  10.         malloc_vec[i] = new char[malloc_size];  
  11.     }  
  12.     while (1) {}  
  13.     return 0;  
  14. }  

分别运行“./test 1","./test 24“和“./test 25",运行后查看程序占用内存可知,前两者占用内存为40M,后者占用56M,这就验证了前面的推断。首先,malloc_vec会占用8*1M,如果./test的输入参数<=24,程序在for循环中增加了32M内存,每次增加32字节,加起来刚好40M;如果./test的输入参数为25,在for循环中增加了48M内存,每次增加48字节,加起来56M。
    上述程序在32位系统中的结果如何,你可以自己试一下?
    现在,我终于明白了为什么之前的程序会多占用那么多内存。我可以针对内存对齐对程序进行优化。一开始我想,那将程序编译成32位目标代码(g++的-m32参数)不就可以节省很多不必要的内存了吗?不过试了后才想起32位程序的硬伤-4GB memory limit,这4G内存用完了还是不够用。后来我只能尽量使得申请内存的大小尽量靠近24,40,56...这些点(发生阶跃的点),这样可以获得尽量多的可用内存,优化后的程序基本上可以满足需求。
    关于gblic的malloc实现还有很多其它的内容,自己也只是看了一小部分, 关于malloc分配策略大家可以看这篇(http://blog.chinaunix.net/u3/94916/showart_1908306.html ) ,那个作者去看malloc.c的动机和我差不多,呵呵。


原创粉丝点击