内存使用技巧及内存池实现

本文只是展现了一些基本的内存管理技巧,处于篇幅没有更深刻的讲解,有兴趣可回复一块儿探讨^_^

        在当前的软件开发环境下,主要分为两大类:客户端和服务端。软件部署在客户端的状况逐渐被Web应用和服务端的网络应用所替代(游戏客户端例外),而且随 着硬件的不断升级和成本的下降,各类计算资源和存储资源被程序随意使用,基本不用考虑一个进程多占了几个Byte,多消耗了CPU几个毫秒。不过,某些场 合下,好比嵌入式环境和大型服务器(尤为是分布式和云计算平台下的大规模数据和海量计算),对资源的使用仍旧须要在时间和空间上进行优化。因此,做者认 为,经过技巧和算法来优化程序仍是颇有必要的。  mysql

         本文主要介绍一些内存的使用技巧,系统场景为Linux,Windows/Mac也可做为参考。 linux

         一、内存的基本操做 :  算法

          C语言中若是要申请堆内存,能够经过malloc/calloc/realloc来得到,参数就是内存大小和从新分配的大小。但须要注意的是malloc 出来分配的内存是未通过初始化的,不能直接使用,因此要bzero或memset(0),能够用calloc(1,SIZE)一行来代替代替。这里用到的 sizeof要注意,获取一个struct或者class的大小必定要sizeof,不然会发生莫名的错误(后面会讲到字节对齐)。 sql

  1. void foo()  
  2. {  
  3.     // use malloc  
  4.     int *p = (int*)malloc(sizeof(int));  
  5.     memset(p,0,sizeof(*p));  
  6.   
  7.     // use calloc  
  8.     int *c = (int*)calloc(1,sizeof(int));  
  9. }  

          在C++里经过new来分配堆内存,new除了像malloc同样分配一块内存,还有一个功能就是会调用该类的构造函数,对此对象进行初始化操做,因此如 果是C++里,不是申请固定大小的内存本身规划用的只是为对象分配空间的话,就尽可能用new。新的C++标准规定,当内存不够,new不成功的时候,不是 像之前同样返回NULL,而是抛出一个异常 -- std::bad_alloc,不少项目里,包括Google的开源项目,都不建议使用异常(由于没有finally,而且速度很慢),因此new的时候 应该加上std::nothrow。 数据库

  1. void foo()  
  2. {  
  3.     MySample *p = new (std::nothrow) MySamle();  
  4.     if (NULL == p)  
  5.         DealError();      // handle the error  
  6. }  

       若是不使用了则用free/delete来释放,而且必定要把原来的指针指NULL。(不指NULL不必定有问题,可是为保险起见)。 windows


       二、Linux进程内存区域分布: 数组

      各个区段的意义和存储的数据网上资料不少,就不在此一一说明了。补充一句,栈是由高地址向低地址延伸,堆反过来(记不住的话能够记住栈分配int空间时候 是eps指针-4)。还有堆栈中间不是彻底空闲的,最中间一段是mmap(内存映射)使用的。若是想查看大小,能够用size命令,好比: size ./myprogram 缓存


      三、malloc的具体平台实现 : 安全

        咱们都知道,malloc是libc的标准函数,是c语言的标准。因此windows、mac、linux才都会有这个函数。由于是标准,因此就不能特 化,不能特化就意味着某种程度上的速度慢(不是绝对,可是个规则)。malloc是用户态函数,要想从Linux系统中获得实际内存,是要调用linux 的brk系统调用的,这个brk就是移动堆顶指针的函数,是将进程的mm_stat机构中的brk值扩大以得到空间。 服务器

        malloc在分配内存的时候,参数size不是传入多大就分配多大,试想一个,malloc(1)若是直接分配一个字节,那么malloc的管理字节就 会比数据空间大不少(关于管理字节,能够认为存储的是已分配的空间的大小,好比free的时候,并不用传入指针指向空间的大小,由于有管理字节)。还 有,cpu经过内存控制器访问内存的时候,是按cpu"喜欢"的对齐方式访问,通常是按4的整数字节读取,若是分配的空间是4的整数倍,就会加快访问。而 且malloc会对传入的大小数字进行“归一化”,按照内核的递增序列分配内存(通常最低层次是8byte,按2的幂增加,最大1M,也就是说若是申请大 于1M,则要多少给多少)。

      malloc在分配了大量的内存以后,会变得愈来愈慢,由于malloc的分配过程是如今内存管理模块的"空闲链表"里找到一个合适大小的内存返回,若是空闲链表太长,势必影响速度。


      四、锁住物理内存,不被swap :

      有的应用,如memcached等缓存系统,或者实时性很高的系统,要求分配的内存要所有Hold在内存中,不被swap到磁盘上(Linux系统内存满 了才会swap,但须要考虑PageCache)。因此,可使用mlockall/mlock函数把已分配的内存,甚至之后malloc的内存都一直留 在磁盘里。(很差之处是内存满了malloc直接返回NULL,还会触发SIGSEGV信号)。

  1. void lock_mem(void* area, size_t len)  
  2. {  
  3.     // lock an area of pointer  
  4.     mlock(area,len);  
  5.   
  6.     // lock all memory has allocated , but no effect with future  
  7.     mlockall(MCL_CURRENT);  
  8.   
  9.     // lock all memory has allocated and future  
  10.     mlockall(MCL_CURRENT | MCL_FUTURE);  
  11. }  

      五、巧用struct/class字节对齐,压缩空间 :

      若是有一个struct/class在内存中可能有10万、或者100万个instanse(大规模服务器常常的状况),能够考虑对它经过字节对齐进行压缩:

  1. #include <stdio.h>  
  2.   
  3. struct NoAlign {  
  4.     int a;    
  5.     char b;   
  6.     int c;    
  7.     short d;  
  8.     int e;    
  9.     char f;   
  10. };  
  11.   
  12. struct Align {   
  13.     char b;   
  14.     char f;   
  15.     short d;  
  16.     int a;    
  17.     int c;    
  18.     int e;    
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     printf("%d|%d",sizeof(struct NoAlign),sizeof(struct Align));  
  24.     return 0;  
  25. }  
       程序会输出"24|16",个人是x64的系统,能够看出经过适当的调整字段的顺序,可进行字节对齐的压缩,最简单廉价的方法,何乐而不为呢!若是是数组,建议放在最后,这样offset会快些(意义不是很大)。

      六、内存读取访问最好按4的整倍数步进:

      能够看看memset的代码实现,并非一个for循环而后set每个字节,这样cpu效率低。memset的实现是按4个字节set一次进行步进,这 样效率高些,相对循环次数也多,而后针对剩下的1~3次可用1~3行冗余代码搞定(相似上篇文章介绍的冗余代码的一些好处)。


     七、offsetof()函数能够得到某个字段在struct中的偏移量 :

     offsetof()在32位系统下的实现相似:

  1. #define OFF_SET_OF(s,m) (size_t)&(((s *)0)->m)  
      就是将0x00000000转换为struct*指针,而后对齐求其m元素的偏移再转换成地址再转换成数字。


      八、malloc的替代品 : tcmalloc/jemalloc

      介绍了这么多内存管理细节和技巧,总结一下,其实malloc并非用于大量内存分配操做(容易产生碎片、速度有问题),而且在多线程环境下也不太适合(malloc是不可重入可是线程安全的函数),说他不适合是由于多线程状况下malloc容易泄漏资源。

      这里提出两个解决方式,第一个就是写一个内存池,本身托管内存的使用和分配释放,内存使用技巧及内存池实现(二)将进行详细介绍。还有一种就是使用改良的 类malloc分配器,使用google的tcmalloc和jemalloc,tcmalloc在效率上比malloc快了不少(malloc()一次 大概300ns,而tcmalloc()大约50ns)。主要是由于TCMalloc减小了多线程程序中的锁争用状况。对于小对象,几乎已经达到了零争用。对于大对象,TCMalloc尝试使用粒度较好和有效的自旋锁。Redis也该用jemalloc来解决内存碎片问题,而且jemalloc在realloc函数上也下了不少功夫,使得realloc原地更新分配,而不是另外开辟一段新空间。

       在编译mysql时候就能够指定tcmalloc,有些资料显示使用tcmalloc的程序有了很大的性能提高(本人未测试)。

       使用tcmalloc很简单,只须要加入脚本 :LD_PRELOAD="/usr/local/lib/libtcmalloc.so"便可。






本文全部内容包括源码均是做者原创,出于尊重,若是转载请代表出处 ^_^        

        上一章节,提到了内存池的使用。其实内存池的做用看名字也能猜到,"池"意味着资源是同一管理和建立释放的,就像数据库的链接池、系统的线程池。主要就是 为了不建立、销毁资源的代价。c标准的malloc/free会形成大量的内存碎片以致于影响效率,因此“内存池”的技术某种程度上避免了这种消耗和影 响。

        本人以为实现的内存池能够分为3级:

           一、初级的简单内存池实现:解决malloc小空间的碎片化,托管回收。适用于函数或类,不跨线程 

           二、高级的内存池:经过块链式的方式长期托管内存,能够半自动的释放内存,并能够动态规划内存的块存储(相似linux内核BuddySystem)。

           三、能够托管内存和相关资源(文件句柄、数据库链接)的池 : 将和该块内存相关联的内存、资源整合,统一托管!全局托管资源。

        本文会实现并讲解一、2中的内存池实现方式。第3种时间和技术有限,你们能够自行写一写,或者用C++的RAII技术和STL中的实现来用。


        简单的内存池实现,核心思想就是想申请一块大内存(mb级别,而且为512KB的整数倍,好处是和linux内存管理配套,数字根据应用不一样能够改),这 样作可能有必定的浪费,不过试想一下,若是只是申请几十个Byte,根本用不到内存池。用到了内存池确定空间不会过小。而后从这块大内存上用游标控制分 配,alloc一块内存指针就移动固定位数,最后统一释放。这样,在操做系统看来,就老是去申请很大一块内存,而且形成碎片的概率很低,速度也快。

        这种实现方式不会让用户去free,由于free了也没用,池子并不会服用。可是这块大内存的生命周期不会很长,因此通常场景下不影响。下一小节会介绍"高级"一点的实现的方式,经过动态的拆分和合并来管理不一样大小的内存。

        废话很少说,直接上代码:

  1. typedef struct _mempool simple_mempool;  
  2.   
  3. struct _mempool {  
  4.     uint32_t size;      // memory pool total size (512KB * n)  
  5.     void *raw_area;  
  6.     void *cursor;       // indicate the current position of pool  
  7. };  
        内存池结构体句柄,保存大小、游标,和原始malloc指针。


  1. /** 
  2.  * Don't allocate more than 512M space, because this mempool 
  3.  * just implement simple way of pool, it don't free anything 
  4.  * util call simple_mempool_destroy(), this feature is based  
  5.  * on JUST USE it in an funtion or in one class 
  6.  */  
  7. void* simple_mempool_create(uint32_t size)  
  8. {  
  9.     if (size==0 || size>=1024*512)  
  10.         return NULL;  
  11.   
  12.     // align of 4 byte  
  13.     // size += size % 4;  
  14.   
  15.     simple_mempool *pool = (simple_mempool*)calloc(1,sizeof(simple_mempool));  
  16.     pool->size = CHUNK_SIZE*size;  
  17.     pool->raw_area = calloc(1,1024*size);  
  18.     pool->cursor = pool->raw_area;  
  19.   
  20.     return pool;  
  21. }  
         建立内存池,若是大于512M就忽略,建议使用高级内存池。


  1. uint8_t simple_mempool_could_allocate(simple_mempool* pool, uint32_t n)  
  2. {  
  3.     // cursor will out of the end  
  4.     if ((pool->cursor-pool->raw_area)+n > pool->size)  
  5.         return 0;  
  6.     else      
  7.         return 1;  
  8. }  
  9.   
  10. void* simple_mempool_allocate(simple_mempool* pool, uint32_t n)  
  11. {  
  12.     // no space here  
  13.     if (NULL==pool || NULL==pool->raw_area || !simple_mempool_could_allocate(pool,n))  
  14.         return NULL;  
  15.   
  16.     void* ret = pool->cursor;  
  17.     // move the cursor  
  18.     pool->cursor = (void*)((char*)pool->cursor + n);  
  19.   
  20.     return ret;  
  21. }  
         实际分配函数,只是挪动了一下cursor指针而已。


  1. uint32_t simple_mempool_left(simple_mempool *pool)  
  2. {  
  3.     if (NULL == pool)  
  4.         return -1;  
  5.     else return pool->size - (pool->cursor - pool->raw_area);  
  6. }  
         查看剩余量,此处pool->size / pool->cursor - pool->raw_area等是无符号整型,虽然没作边界校验,可是allocate函数保证了cursor不会超过size。

  1. void simple_mempool_destroy(simple_mempool* pool)  
  2. {  
  3.     free(pool->raw_area);  
  4.     pool->cursor = pool->raw_area = NULL;     
  5.     pool->size = 0;  
  6. }  
         释放内存池,pool句柄可复用。


        下面是ut单侧的代码,分配完了就destroy并退出 :

  1. #define ut_main main  
  2. int ut_main()  
  3. {  
  4.   
  5.     simple_mempool *pool = simple_mempool_create(1);  
  6.   
  7.     while(1) {  
  8.         long long *tmp = simple_mempool_allocate(pool,sizeof(long long));   
  9.         if (NULL == tmp) {  
  10.             printf("no space in mempool , destroy it !!!");  
  11.             simple_mempool_destroy(pool);  
  12.             break;    
  13.         }         
  14.     }  
  15.   
  16.     return 0;  


http://blog.csdn.net/gugemichael/article/details/7547143

相关文章
相关标签/搜索