c++内存碎片
內存碎片的產生:
????????內存分配有靜態分配和動態分配兩種
?????? 靜態分配在程序編譯鏈接時分配的大小和使用壽命就已經確定,而應用上要求操作系統可以提供給進程運行時申請和釋放任意大小內存的功能,這就是內存的動態分配。
??????? 因此動態分配將不可避免會產生內存碎片的問題,那么什么是內存碎片?內存碎片即“碎片的內存”描述一個系統中所有不可用的空閑內存,這些碎片之所以不能被使用,是因為負責動態分配內存的分配算法使得這些空閑的內存無法使用,這一問題的發生,原因在于這些空閑內存以小且不連續方式出現在不同的位置。因此這個問題的或大或小取決于內存管理算法的實現上。
?????? 為什么會產生這些小且不連續的空閑內存碎片呢?
?????? 實際上這些空閑內存碎片存在的方式有兩種:a.內部碎片 b.外部碎片 。
????? ?內部碎片的產生:因為所有的內存分配必須起始于可被 4、8 或 16 整除(視處理器體系結構而定)的地址或者因為MMU的分頁機制的限制,決定內存分配算法僅能把預定大小的內存塊分配給客戶。假設當某個客戶請求一個 43 字節的內存塊時,因為沒有適合大小的內存,所以它可能會獲得 44字節、48字節等稍大一點的字節,因此由所需大小四舍五入而產生的多余空間就叫內部碎片。
????? 外部碎片的產生: 頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊夾雜在已分配的頁面中間,就會產生外部碎片。假設有一塊一共有100個單位的連續空閑內存空間,范圍是0~99。如果你從中申請一塊內存,如10個單位,那么申請出來的內存塊就為0~9區間。這時候你繼續申請一塊內存,比如說5個單位大,第二塊得到的內存塊就應該為10~14區間。如果你把第一塊內存塊釋放,然后再申請一塊大于10個單位的內存塊,比如說20個單位。因為剛被釋放的內存塊不能滿足新的請求,所以只能從15開始分配出20個單位的內存塊。現在整個內存空間的狀態是0~9空閑,10~14被占用,15~24被占用,25~99空閑。其中0~9就是一個內存碎片了。如果10~14一直被占用,而以后申請的空間都大于10個單位,那么0~9就永遠用不上了,變成外部碎片。
?
如何解決內存碎片:
?????? ?采用Slab Allocation機制:整理內存以便重復使用
?????? ?最近的memcached默認情況下采用了名為Slab Allocator的機制分配、管理內存。在該機制出現以前,內存的分配是通過對所有記錄簡單地進行malloc和free來進行的。但是,這種方式會導致內存碎片,加重操作系統內存管理器的負擔,最壞的情況下,會導致操作系統比memcached進程本身還慢。Slab Allocator就是為解決該問題而誕生的。
??????? 下面來看看Slab Allocator的原理。下面是memcached文檔中的slab allocator的目標:he primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issuestotally by using fixedsizememory chunks coming from a few predetermined size classes.
??????? 也就是說,Slab Allocator的基本原理是按照預先規定的大小,將分配的內存分割成特定長度的塊,以完全解決內存碎片問題。Slab Allocation的原理相當簡單。將分配的內存分割成各種尺寸的塊(chunk),并把尺寸相同的塊分成組(chunk的集合)(圖2.1)。
??slab allocator還有重復使用已分配的內存的目的。也就是說,分配到的內存不會釋放,而是重復利用
Slab Allocation的主要術語
? ? Page
?? ?分配給Slab的內存空間,默認是1MB。分配給Slab之后根據slab的大小切分成chunk。
??? Chunk
??? 用于緩存記錄的內存空間。
??? Slab Class
??? 特定大小的chunk的組。
在Slab中緩存記錄的原理
下面說明memcached如何針對客戶端發送的數據選擇slab并緩存到chunk中。memcached根據收到的數據的大小,選擇最適合數據大小的slab(圖2.2)。memcached中保存著slab內空閑chunk的列表,根據該列表選擇chunk,然后將數據緩存于其中。
?
圖2.2:選擇存儲記錄的組的方法
實際上,Slab Allocator也是有利也有弊。下面介紹一下它的缺點。
Slab Allocator的缺點
Slab Allocator解決了當初的內存碎片問題,但新的機制也給memcached帶來了新的問題。這個問題就是,由于分配的是特定長度的內存,因此無法有效利用分配的內存。例如,將100字節的數據緩存到128字節的chunk中,剩余的28字節就浪費了
?對于該問題目前還沒有完美的解決方案,但在文檔中記載了比較有效的解決方案。
The most efficient way to reduce the waste is to use a list of size classes that closely matches (if that's at all
possible) common sizes of objects that the clients of this particular installation of memcached are likely to
store.
?????? 就是說,如果預先知道客戶端發送的數據的公用大小,或者僅緩存大小相同的數據的情況下,只要使用適合數據大小的組的列表,就可以減少浪費。但是很遺憾,現在還不能進行任何調優,只能期待以后的版本了。但是,我們可以調節slab class的大小的差別
?
最佳適合與最差適合分配程序
最佳適合算法在功能上與最先適合算法類似,不同之處是,系統在分配一個內存塊時,要搜索整個自由表,尋找最接近請求存儲量的內存塊。這種搜索所花的時間要比最先適合算法長得多,但不存在分配大小內存塊所需時間的差異。最佳適合算法產生的內存碎片要比最先適合算法多,因為將小而不能使用的碎片放在自由表開頭部分的排序趨勢更為強烈。由于這一消極因素,最佳適合算法幾乎從來沒有人采用過。
最差適合算法也很少采用。最差適合算法的功能與最佳適合算法相同,不同之處是,當分配一個內存塊時,系統在整個自由表中搜索與請求存儲量不匹配的內存快。這種方法比最佳適合算法速度快,因為它產生微小而又不能使用的內存碎片的傾向較弱。始終選擇最大空閑內存塊,再將其分為小內存塊,這樣就能提高剩余部分大得足以供系統使用的概率。
伙伴(buddy)分配程序與本文描述的其它分配程序不同,它不能根據需要從被管理內存的開頭部分創建新內存。它有明確的共性,就是各個內存塊可分可合,但不是任意的分與合。每個塊都有個朋友,或叫“伙伴”,既可與之分開,又可與之結合。伙伴分配程序把內存塊存放在比鏈接表更先進的數據結構中。這些結構常常是桶型、樹型和堆型的組合或變種。一般來說,伙伴分配程序的工作方式是難以描述的,因為這種技術隨所選數據結構的不同而各異。由于有各種各樣的具有已知特性的數據結構可供使用,所以伙伴分配程序得到廣泛應用。有些伙伴分配程序甚至用在源碼中。伙伴分配程序編寫起來常常很復雜,其性能可能各不相同。伙伴分配程序通常在某種程度上限制內存碎片。
固定存儲量分配程序有點像最先空閑算法。通常有一個以上的自由表,而且更重要的是,同一自由表中的所有內存塊的存儲量都相同。至少有四個指針:MSTART指向被管理內存的起點,MEND 指向被管理內存的末端,MBREAK 指向 MSTART 與 MEND 之間已用內存的末端,而 PFREE[n]則是指向任何空閑內存塊的一排指針。在開始時,PFREE 為 NULL,MBREAK 指針為MSTART。當一個分配請求到來時,系統將請求的存儲量增加到可用存儲量之一。然后,系統檢查 PFREE[ 增大后的存儲量 ] 空閑內存塊。因為PFREE[ 增大后的存儲量 ] 為 NULL,一個具有該存儲量加上一個管理標題的內存塊就脫離 MBREAK,MBREAK 被更新。
這些步驟反復進行,直至系統使一個內存塊空閑為止,此時管理標題包含有該內存塊的存儲量。當有一內存塊空閑時,PFREE[ 相應存儲量 ]通過標題的鏈接表插入項更新為指向該內存塊,而該內存塊本身則用一個指向 PFREE[ 相應存儲量 ]以前內容的指針來更新,以建立一個鏈接表。下一次分配請求到來時,系統將 PFREE[ 增大的請求存儲量 ]鏈接表的第一個內存塊送給系統。沒有理由搜索鏈接表,因為所有鏈接的內存塊的存儲量都是相同的。
固定存儲量分配程序很容易實現,而且便于計算內存碎片,至少在塊存儲量的數量較少時是這樣。但這種分配程序的局限性在于要有一個它可以分配的最大存儲量。固定存儲量分配程序速度快,并可在任何狀況下保持速度。這些分配程序可能會產生大量的內部內存碎片,但對某些系統而言,它們的優點會超過缺點。
減少內存碎片
內存碎片是因為在分配一個內存塊后,使之空閑,但不將空閑內存歸還給最大內存塊而產生的。最后這一步很關鍵。如果內存分配程序是有效的,就不能阻止系統分配內存塊并使之空閑。即使一個內存分配程序不能保證返回的內存能與最大內存塊相連接(這種方法可以徹底避免內存碎片問題),但你可以設法控制并限制內存碎片。所有這些作法涉及到內存塊的分割。每當系統減少被分割內存塊的數量,確保被分割內存塊盡可能大時,你就會有所改進。
這樣做的目的是盡可能多次反復使用內存塊,而不要每次都對內存塊進行分割,以正好符合請求的存儲量。分割內存塊會產生大量的小內存碎片,猶如一堆散沙。以后很難把這些散沙與其余內存結合起來。比較好的辦法是讓每個內存塊中都留有一些未用的字節。留有多少字節應看系統要在多大
程度上避免內存碎片。對小型系統來說,增加幾個字節的內部碎片是朝正確方向邁出的一步。當系統請求1字節內存時,你分配的存儲量取決于系統的工作狀態。
如果系統分配的內存存儲量的主要部分是 1 ~ 16 字節,則為小內存也分配 16字節是明智的。只要限制可以分配的最大內存塊,你就能夠獲得較大的節約效果。但是,這種方法的缺點是,系統會不斷地嘗試分配大于極限的內存塊,這使系統可能會停止工作。減少最大和最小內存塊存儲量之間內存存儲量的數量也是有用的。采用按對數增大的內存塊存儲量可以避免大量的碎片。例如,每個存儲量可能都比前一個存儲量大20%。在嵌入式系統中采用“一種存儲量符合所有需要”對于嵌入式系統中的內存分配程序來說可能是不切實際的。這種方法從內部碎片來看是代價極高的,但系統可以徹底避免外部碎片,達到支持的最大存儲量。
將相鄰空閑內存塊連接起來是一種可以顯著減少內存碎片的技術。如果沒有這一方法,某些分配算法(如最先適合算法)將根本無法工作。然而,效果是有限的,將鄰近內存塊連接起來只能緩解由于分配算法引起的問題,而無法解決根本問題。而且,當內存塊存儲量有限時,相鄰內存塊連接可能很難實現。
有些內存分配器很先進,可以在運行時收集有關某個系統的分配習慣的統計數據,然后,按存儲量將所有的內存分配進行分類,例如分為小、中和大三類。系統將每次分配指向被管理內存的一個區域,因為該區域包括這樣的內存塊存儲量。較小存儲量是根據較大存儲量分配的。這種方案是最先適合算法和一組有限的固定存儲量算法的一種有趣的混合,但不是實時的。
有效地利用暫時的局限性通常是很困難的,但值得一提的是,在內存中暫時擴展共處一地的分配程序更容易產生內存碎片。盡管其它技術可以減輕這一問題,但限制不同存儲量內存塊的數目仍是減少內存碎片的主要方法。
現代軟件環境業已實現各種避免內存碎片的工具。例如,專為分布式高可用性容錯系統開發的 OSE 實時操作系統可提供三種運行時內存分配程序:內核alloc(),它根據系統或內存塊池來分配;堆 malloc(),根據程序堆來分配; OSE 內存管理程序alloc_region,它根據內存管理程序內存來分配。
從許多方面來看,Alloc就是終極內存分配程序。它產生的內存碎片很少,速度很快,并有判定功能。你可以調整甚至去掉內存碎片。只是在分配一個存儲量后,使之空閑,但不再分配時,才會產生外部碎片。內部碎片會不斷產生,但對某個給定的系統和八種存儲量來說是恒定不變的。
Alloc是一種有八個自由表的固定存儲量內存分配程序的實現方法。系統程序員可以對每一種存儲量進行配置,并可決定采用更少的存儲量來進一步減少碎片。除開始時以外,分配內存塊和使內存塊空閑都是恒定時間操作。首先,系統必須對請求的存儲量四舍五入到下一個可用存儲量。就八種存儲量而言,這一目標可用三個 如果語句來實現。其次,系統總是在八個自由表的表頭插入或刪除內存塊。開始時,分配未使用的內存要多花幾個周期的時間,但速度仍然極快,而且所花時間恒定不變。
堆 malloc() 的內存開銷(8 ~ 16 字節/分配)比 alloc小,所以你可以停用內存的專用權。malloc()分配程序平均來講是相當快的。它的內部碎片比alloc()少,但外部碎片則比alloc()多。它有一個最大分配存儲量,但對大多數系統來說,這一極限值足夠大。可選的共享所有權與低開銷使 malloc() 適用于有許多小型對象和共享對象的 C++應用程序。堆是一種具有內部堆數據結構的伙伴系統的實現方法。在 OSE 中,有 28 個不同的存儲量可供使用,每種存儲量都是前兩種存儲量之和,于是形成一個斐波那契(Fibonacci)序列。實際內存塊存儲量為序列數乘以 16 字節,其中包括分配程序開銷或者 8 字節/分配(在文件和行信息啟用的情況下為 16 字節)。
當你很少需要大塊內存時,則OSE內存管理程序最適用。典型的系統要把存儲空間分配給整個系統、堆或庫。在有 MMU 的系統中,有些實現方法使用 MMU 的轉換功能來顯著降低甚至消除內存碎片。在其他情況下,OSE 內存管理程序會產生非常多的碎片。它沒有最大分配存儲量,而且是一種最先適合內存分配程序的實現方法。內存分配被四舍五入到頁面的偶數——典型值是 4 k 字節。
本文來自:我愛研發網(52RD.com) - R&D大本營
總結
- 上一篇: 深度学习的三种硬件方案:ASIC,FPG
- 下一篇: 你需要了解的 C++ 17 Top 19