在當前的軟件開發環境下,主要分爲兩大類:客戶端和服務端。軟件部署在客戶端的狀況逐漸被Web應用和服務端的網絡應用所替代(遊戲客戶端例外),而且隨 着硬件的不斷升級和成本的下降,各類計算資源和存儲資源被程序隨意使用,基本不用考慮一個進程多佔了幾個Byte,多消耗了CPU幾個毫秒。不過,某些場 合下,好比嵌入式環境和大型服務器(尤爲是分佈式和雲計算平臺下的大規模數據和海量計算),對資源的使用仍舊須要在時間和空間上進行優化。因此,做者認 爲,經過技巧和算法來優化程序仍是頗有必要的。 mysql
本文主要介紹一些內存的使用技巧,系統場景爲Linux,Windows/Mac也可做爲參考。 linux
一、內存的基本操做 : 算法
C語言中若是要申請堆內存,能夠經過malloc/calloc/realloc來得到,參數就是內存大小和從新分配的大小。但須要注意的是malloc 出來分配的內存是未通過初始化的,不能直接使用,因此要bzero或memset(0),能夠用calloc(1,SIZE)一行來代替代替。這裏用到的 sizeof要注意,獲取一個struct或者class的大小必定要sizeof,不然會發生莫名的錯誤(後面會講到字節對齊)。 sql
在C++裏經過new來分配堆內存,new除了像malloc同樣分配一塊內存,還有一個功能就是會調用該類的構造函數,對此對象進行初始化操做,因此如 果是C++裏,不是申請固定大小的內存本身規劃用的只是爲對象分配空間的話,就儘可能用new。新的C++標準規定,當內存不夠,new不成功的時候,不是 像之前同樣返回NULL,而是拋出一個異常 -- std::bad_alloc,不少項目裏,包括Google的開源項目,都不建議使用異常(由於沒有finally,而且速度很慢),因此new的時候 應該加上std::nothrow。 數據庫
若是不使用了則用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信號)。
若是有一個struct/class在內存中可能有10萬、或者100萬個instanse(大規模服務器常常的狀況),能夠考慮對它經過字節對齊進行壓縮:
六、內存讀取訪問最好按4的整倍數步進:
能夠看看memset的代碼實現,並非一個for循環而後set每個字節,這樣cpu效率低。memset的實現是按4個字節set一次進行步進,這 樣效率高些,相對循環次數也多,而後針對剩下的1~3次可用1~3行冗餘代碼搞定(相似上篇文章介紹的冗餘代碼的一些好處)。
七、offsetof()函數能夠得到某個字段在struct中的偏移量 :
offsetof()在32位系統下的實現相似:
八、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了也沒用,池子並不會服用。可是這塊大內存的生命週期不會很長,因此通常場景下不影響。下一小節會介紹"高級"一點的實現的方式,經過動態的拆分和合並來管理不一樣大小的內存。
廢話很少說,直接上代碼:
下面是ut單側的代碼,分配完了就destroy並退出 :
http://blog.csdn.net/gugemichael/article/details/7547143