大家生活中肯定都有這樣的經驗,那就是大眾化的產品都比較便宜,但便宜的大眾產品就是一個詞,普通;而可以定制的產品一般都價位不凡,這種定制的產品注定不會在大眾中普及,因此定制產品就是一個詞,獨特。
有的同學可能會有疑問,你不是要聊技術嗎?怎么又說起消費了?
原來技術也有大眾貨以及定制品。
通用 VS 定制
作為程序員(C/C++)我們知道申請內存使用的是malloc,malloc其實就是一個通用的大眾貨,什么場景下都可以用,但是什么場景下都可以用就意味著什么場景下都不會有很高的性能。
malloc性能不高的原因一在于其沒有為特定場景做優化,除此之外還在于malloc看似簡單,但是其調用過程是很復雜的,一次malloc的調用過程可能需要經過操作系統的配合才能完成。 那么調用malloc時底層都發生了什么呢?簡單來說會有這樣典型的幾個步驟:
malloc開始搜索空閑內存塊,如果能找到一塊大小合適的就分配出去
如果malloc找不到一塊合適的空閑內存,那么調用brk等系統調用擴大堆區從而獲得更多的空閑內存
malloc調用brk后開始轉入內核態,此時操作系統中的虛擬內存系統開始工作,擴大進程的堆區,注意額外擴大的這一部分內存僅僅是虛擬內存,操作系統并沒有為此分配真正的物理內存
brk執行結束后返回到malloc,從內核態切換到用戶態,malloc找到一塊合適的空閑內存后返回
以上就是一次內存申請的完整過程,我們可以看到,一次內存申請過程其實是非常復雜的,關于這個問題的詳細討論你可以參考這里。 既然每次分配內存都要經過這么復雜的過程,那么如果程序大量使用malloc申請內存那么該程序注定無法獲得高性能。 幸好,除了大眾貨的malloc,我們還可以私人定制,也就是針對特定場景自己來維護內存申請和分配,這就是高性能高并發必備的內存池技術。
內存池技術有什么特殊的嗎? 有的同學可能會說,等等,那malloc和這里提到的內存池技術有什么區別呢? 第一個區別在于我們所說的malloc其實是標準庫的一部分,位于標準庫這一層;而內存池是應用程序的一部分。
其次在于定位,我們自己實現的malloc其實也是定位通用性的,通用性的內存分配器設計實現往往比較復雜,但是內存池技術就不一樣了,內存池技術專用于某個特定場景,以此優化程序性能,但內存池技術的通用性是很差的,在一種場景下有很高性能的內存池基本上沒有辦法在其它場景也能獲得高性能,甚至根本就不能用于其它場景,這就是內存池這種技術的定位。
那么內存池技術是怎樣優化性能的呢?
內存池技術原理 簡單來說,內存池技術一次性獲取到大塊內存,然后在其之上自己管理內存的申請和釋放,這樣就繞過了標準庫以及操作系統:
也就是說,通過內存池,一次內存的申請再也不用去繞一大圈了。 除此之外,我們可以根據特定的使用模式來進一步優化,比如在服務器端,每次用戶請求需要創建的對象可能就那幾種,那么這時我們就可以在自己的內存池上提前創建出這些對象,當業務邏輯需要時就從內存池中申請已經創建好的對象,使用完畢后還回內存池。 因此我們可以看到,這種為某些應用場景定制的內存池相比通用的比如malloc內存分配器會有大的優勢。 接下來我們就著手實現一個。
實現內存池的考慮 值得注意的是,內存池實際上有很多的實現方法,在這里我們還是以服務器端編程為例來說明。 假設你的服務器程序非常簡單,處理用戶請求時只使用一種對象(數據結構),那么最簡單的就是我們提前申請出一堆來,使用的時候拿出一個,使用完后還回去:
怎么樣,足夠簡單吧!這樣的內存池只能分配特定對象(數據結構),當然這樣的內存池需要自己維護哪些對象是已經被分配出去的,哪些是還沒有被使用的。 但是,在這里我們可以實現一個稍微復雜一些的,那就是可以申請不同大小的內存,而且由于是服務器端編程,那么一次用戶請求過程中我們只申請內存,只有當用戶請求處理完畢后一次性釋放所有內存,從而將內存申請釋放的開銷降低到最小。 因此,你可以看到,內存池的設計都是針對特定場景的。 現在,有了初步的設計,接下來就是細節了。
數據結構 為了能夠分配大小可變的對象,顯然我們需要管理空閑內存塊,我們可以用一個鏈表把所有內存塊鏈接起來,然后使用一個指針來記錄當前空閑內存塊的位置,如圖所示:
從圖中我們可以看到,有兩個空閑內存塊,空閑內存之間使用鏈表鏈接起來,每個內存塊都是前一個的2倍,也就是說,當內存池中的空閑內存不足以分配時我們就向malloc申請內存,只不過其大小是前一個的2倍:
其次,我們有一個指針free_ptr,指向接下來的空閑內存塊起始位置,當向內存池分配內存時找到free_ptr并判斷當前內存池剩余空閑是否足夠就可以了,有就分配出去并修改free_ptr,否則向malloc再次成倍申請內存。 從這里的設計可以看出,我們的內存池其實是不會提供類似free這樣的內存釋放函數的,如果要釋放內存,那么會一次性將整個內存池釋放掉,這一點和通用的內存分配器是不一樣。 現在,我們可以分配內存了,還有一個問題是所有內存池設計不得不考慮的,那就是線程安全,這個話題你可以參考這里。
線程安全 顯然,內存池不應該局限在單線程場景,那我們的內存池要怎樣實現線程安全呢? 有的同學可能會說這還不簡單,直接給內存池一把鎖保護就可以了。
這種方法是不是可行呢?還是那句話,It depends,要看情況。 如果你的程序有大量線程申請釋放內存,那么這種方案下鎖的競爭將會非常激烈,線程這樣的場景下使用該方案不會有很好的性能。 那么還有沒有一種更好的辦法嗎?答案是肯定的。
線程局部存儲 既然多線程使用線程池存在競爭問題,那么干脆我們為每個線程維護一個內存池就好了,這樣多線程間就不存在競爭問題了。 那么我們該怎樣為每個線程維護一個內存池呢? 線程局部存儲,Thread Local Storage正是用于解決這一類問題的,什么是線程局部存儲呢? 簡單說就是,我們可以創建一個全局變量,因此所有線程都可以使用該全局變量,但與此同時,我們將該全局變量聲明為線程私有存儲,那么這時雖然所有線程依然看似使用同一個全局變量,但該全局變量在每個線程中都有自己的副本,變量指向的值是線程私有的,相互之間不會干擾。
關于線程局部存儲,可以參考這里。 假設這個全局變量是一個整數,變量名字為global_value,初始值為100,那么當線程A將global_value修改為200時,線程B看到的global_value的值依然為100,只有線程A看到的global_value為200,這就是線程局部存儲的作用。
線程局部存儲+內存池 有了線程局部存儲問題就簡單了,我們可以將內存池聲明為線程局部存儲,這樣每個線程都只會操作屬于自己的內存池,這樣就再也不會有鎖競爭問題了。
注意,雖然這里給出了線程局部存儲的設計,但并不是說加鎖的方案就比不上線程局部存儲方案,還是那句話,一切要看使用場景,如果加鎖的方案夠用,那么我們就沒有必要絞盡腦汁的去用其它方案,因為加鎖的方案更簡單,代碼也更容易維護。 還需要提醒的是,這里只是給出了內存池的一種實現方法,并不是說所有內存池都要這么設計,內存池可以簡單也可復雜,一切要看實際場景,這一點也需要注意。
其它內存池形式 到目前為止我們給出了兩種內存池的設計方法,第一種是提前創建出一堆需要的對象(數據結構),自己維護好哪些對象(數據結構)可用哪些已被分配;第二種可以申請任意大小的內存空間,使用過程中只申請不釋放,最后一次性釋放。這兩種內存池天然適用于服務器端編程。 最后我們再來介紹一種內存池實現技術,這種內存池會提前申請出一大段內存,然后將這一大段內存切分為大小相同的小內存塊:
然后我們自己來維護這些被切分出來的小內存塊哪些是空閑的哪些是已經被分配的,比如我們可以使用棧這種數據結構,最初把所有空閑內存塊地址push到棧中,分配內存是就pop出來一個,用戶使用完畢后再push回棧里。
從這里的設計我們可以看出,這種內存池有一個限制,這個限制就是說程序申請的最大內存不能超過這里內存塊的大小,否則不足以裝下用戶數據,這需要我們對程序所涉及的業務非常了解才可以。 用戶申請到內存后根據需要將其塑造成特定對象(數據結構)。 關于線程安全的問題,可以同樣采用線程局部存儲的方式來實現:
一個有趣的問題 除了線程安全,這里還有一個非常有趣的問題,那就是如果線程A申請的對象被線程B拿去釋放,我們的內存池該怎么處理呢? 這個問題之所以有趣是因為我們必須知道該內存屬于哪個線程的局部存儲,但申請的內存本身并不能告訴你這樣的信息。 有的同學可能會說這還不簡單,不就是一個指針到另一個指針的映射嗎,直接用map之類存起來就好了,但問題并沒有這么簡單,原因就在于如果我們切分的內存塊很小,那么會存在大量內存塊,這就需要存儲大量的映射關系,有沒有辦法改進呢? 改進方法是這樣的,一般來說,我們申請到的大段內存其實是會按照特定大小進行內存對齊,我們假設總是按照4K字節對齊,那么該大段內存的起始地址后12個bit(4K = 2^12)為總是0,比如地址0x9abcd000,同時我們也假設申請到的大段內存大小也是4K:
那么我們就能知道該大段內存中的各個小內存塊起始地址除了后12個bit位外都是一樣的:
這樣拿到任意一個內存的地址我們就能知道對應的大段內存的起始地址,只需要簡單的將后12個bit置為0即可,有了大段內存的起始地址剩下的就簡單了,我們可以在大段內存中的最后保存對應的線程局部存儲信息:
這樣我們對任意一個內存塊地址進行簡單的位運算就可以得到對應的線程局部存儲信息,大大減少了維護映射信息對內存的占用。
總結 內存池是高性能服務器中常見的一種優化技術,在這里我們介紹了三種實現方法,值得注意的是,內存池實現沒有統一標準,一切都要根據具體場景定制,因此我們可以看到內存池設計是有針對性的,當然其反面就是不具備通用性。
編輯:hfy
-
內存
+關注
關注
8文章
3052瀏覽量
74215 -
局部存儲
+關注
關注
0文章
2瀏覽量
5469 -
malloc
+關注
關注
0文章
53瀏覽量
75
發布評論請先 登錄
相關推薦
評論