內存使用技巧及內存池實現

本文只是展現了一些基本的內存管理技巧,處於篇幅沒有更深刻的講解,有興趣可回覆一塊兒探討^_^

        在當前的軟件開發環境下,主要分爲兩大類:客戶端和服務端。軟件部署在客戶端的狀況逐漸被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

相關文章
相關標籤/搜索