伙伴分配器的一個極簡實現

提起buddy system相信很多人不會陌生,它是一種經典的內存分配算法,大名鼎鼎的Linux底層的內存管理用的就是它。這里不探討內核這么復雜實現,而僅僅是將該算法抽象提取出來,同時給出一份及其簡潔的源碼實現,以便定制擴展。

伙伴分配的實質就是一種特殊的“分離適配”,即將內存按2的冪進行劃分,相當于分離出若干個塊大小一致的空閑鏈 表,搜索該鏈表并給出同需求最佳匹配的大小。其優點是快速搜索合并(O(logN)時間復雜度)以及低外部碎片(最佳適配best-fit);其缺點是內 部碎片,因為按2的冪劃分塊,如果碰上66單位大小,那么必須劃分128單位大小的塊。但若需求本身就按2的冪分配,比如可以先分配若干個內存池,在其基 礎上進一步細分就很有吸引力了。

可以在維基百科上找到該算法的描述,大體如是:

分配內存:

1.尋找大小合適的內存塊(大于等于所需大小并且最接近2的冪,比如需要27,實際分配32)

  1. .如果找到了,分配給應用程序。
  2. 如果沒找到,分出合適的內存塊。
    1. .對半分離出高于所需大小的空閑內存塊
    2. .如果分到最低限度,分配這個大小。
    3. 回溯到步驟1(尋找合適大小的塊)
    4. .重復該步驟直到一個合適的塊

?

釋放內存:

1.釋放該內存塊

  1. 尋找相鄰的塊,看其是否釋放了。
  2. 如果相鄰塊也釋放了,合并這兩個塊,重復上述步驟直到遇上未釋放的相鄰塊,或者達到最高上限(即所有內存都釋放了)。

上面這段文字對你來說可能看起來很費勁,沒事,我們看個內存分配和釋放的示意圖你就知道了:

上圖中,首先我們假設我們一個內存塊有1024K,當我們需要給A分配70K內存的時候,

  1. 我們發現1024K的一半大于70K,然后我們就把1024K的內存分成兩半,一半512K。
  2. 然后我們發現512K的一半仍然大于70K,于是我們再把512K的內存再分成兩半,一半是128K。
  3. 此時,我們發現128K的一半小于70K,于是我們就分配為A分配128K的內存。

后面的,B,C,D都這樣,而釋放內存時,則會把相鄰的塊一步一步地合并起來(合并也必需按分裂的逆操作進行合并)。

我們可以看見,這樣的算法,用二叉樹這個數據結構來實現再合適不過了。

我在網上分別找到cloudwu和wuwenbin寫的兩份開源實現和測試用例。實際上后一份是對前一份的精簡和優化,本文打算從后一份入手講解,因為這份實現真正體現了“極簡”二字,追求突破常規的,極致簡單的設計。網友對其評價甚高,甚至可用作教科書標準實現,看完之后回過頭來看cloudwu的代碼就容易理解了。

分配器的整體思想是,通過一個數組形式的完全二叉樹來監控管理內存,二叉樹的節點用于標記相應內存塊的使用狀態,高層節點對應大的塊,低層節點對應 小的塊,在分配和釋放中我們就通過這些節點的標記屬性來進行塊的分離合并。如圖所示,假設總大小為16單位的內存,我們就建立一個深度為5的滿二叉樹,根 節點從數組下標[0]開始,監控大小16的塊;它的左右孩子節點下標[1~2],監控大小8的塊;第三層節點下標[3~6]監控大小4的塊……依此類推。

在分配階段,首先要搜索大小適配的塊,假設第一次分配3,轉換成2的冪是4,我們先要對整個內存進行對半切割,從16切割到4需要兩步,那么從下標 [0]節點開始深度搜索到下標[3]的節點并將其標記為已分配。第二次再分配3那么就標記下標[4]的節點。第三次分配6,即大小為8,那么搜索下標 [2]的節點,因為下標[1]所對應的塊被下標[3~4]占用了。

在釋放階段,我們依次釋放上述第一次和第二次分配的塊,即先釋放[3]再釋放[4],當釋放下標[4]節點后,我們發現之前釋放的[3]是相鄰的, 于是我們立馬將這兩個節點進行合并,這樣一來下次分配大小8的時候,我們就可以搜索到下標[1]適配了。若進一步釋放下標[2],同[1]合并后整個內存 就回歸到初始狀態。

還是看一下源碼實現吧,首先是伙伴分配器的數據結構:

  1. struct?buddy2?{?
  2. ??unsigned?size;?
  3. ??unsigned?longest[1];?
  4. };?

這里的成員size表明管理內存的總單元數目(測試用例中是32),成員longest就是二叉樹的節點標記,表明所對應的內存塊的空閑單位,在下文中會分析這是整個算法中最精妙的設計。此處數組大小為1表明這是可以向后擴展的(注:在GCC環境下你可以寫成longest[0],不占用空間,這里是出于可移植性考慮),我們在分配器初始化的buddy2_new可以看到這種用法。

  1. truct?buddy2*?buddy2_new(?int?size?)?{?
  2. ??struct?buddy2*?self;?
  3. ??unsigned?node_size;?
  4. ??int?i;?
  5. ??
  6. ??if?(size?<?1?||?!IS_POWER_OF_2(size))?
  7. ????return?NULL;?
  8. ??
  9. ??self?=?(struct?buddy2*)ALLOC(?2?*?size?*?sizeof(unsigned));?
  10. ??self->size?=?size;?
  11. ??node_size?=?size?*?2;?
  12. ??
  13. ??for?(i?=?0;?i?<?2?*?size?-?1;?++i)?{?
  14. ????if?(IS_POWER_OF_2(i+1))?
  15. ??????node_size?/=?2;?
  16. ????self->longest[i]?=?node_size;?
  17. ??}?
  18. ??return?self;?
  19. }?

整個分配器的大小就是滿二叉樹節點數目,即所需管理內存單元數目的2倍。一個節點對應4個字節,longest記錄了節點所對應的的內存塊大小。

內存分配的alloc中,入參是分配器指針和需要分配的大小,返回值是內存塊索引。alloc函數首先將size調整到2的冪大小,并檢查是否超過最大限度。然后進行適配搜索,深度優先遍歷,當找到對應節點后,將其longest標記為0,即分離適配的塊出來,并轉換為內存塊索引offset返回,依據二叉樹排列序號,比如內存總體大小32,我們找到節點下標[8],內存塊對應大小是4,則offset = (8+1)*4-32 = 4,那么分配內存塊就從索引4開始往后4個單位。

  1. int?buddy2_alloc(struct?buddy2*?self,?int?size)?{?
  2. ??unsigned?index?=?0;?
  3. ??unsigned?node_size;?
  4. ??unsigned?offset?=?0;?
  5. ??
  6. ??if?(self==NULL)?
  7. ????return?-1;?
  8. ??
  9. ??if?(size?<=?0)?
  10. ????size?=?1;?
  11. ??else?if?(!IS_POWER_OF_2(size))?
  12. ????size?=?fixsize(size);?
  13. ??
  14. ??if?(self->longest[index]?<?size)?
  15. ????return?-1;?
  16. ??
  17. ??for(node_size?=?self->size;?node_size?!=?size;?node_size?/=?2?)?{?
  18. ????if?(self->longest[LEFT_LEAF(index)]?>=?size)?
  19. ??????index?=?LEFT_LEAF(index);?
  20. ????else?
  21. ??????index?=?RIGHT_LEAF(index);?
  22. ??}?
  23. ??
  24. ??self->longest[index]?=?0;?
  25. ??offset?=?(index?+?1)?*?node_size?-?self->size;?
  26. ??
  27. ??while?(index)?{?
  28. ????index?=?PARENT(index);?
  29. ????self->longest[index]?=?
  30. ??????MAX(self->longest[LEFT_LEAF(index)],?self->longest[RIGHT_LEAF(index)]);?
  31. ??}?
  32. ??
  33. ??return?offset;?
  34. }?

在函數返回之前需要回溯,因為小塊內存被占用,大塊就不能分配了,比如下標[8]標記為0分離出來,那么其父節點下標[0]、[1]、[3]也需要相應大小的分離。將它們的longest進行折扣計算,取左右子樹較大值,下標[3]取4,下標[1]取8,下標[0]取16,表明其對應的最大空閑值。

在內存釋放的free接口,我們只要傳入之前分配的內存地址索引,并確保它是有效值。之后就跟alloc做反向回溯,從最后的節點開始一直往上找到longest為0的節點,即當初分配塊所適配的大小和位置。我們將longest恢復到原來滿狀態的值。繼續向上回溯,檢查是否存在合并的塊,依據就是左右子樹longest的值相加是否等于原空閑塊滿狀態的大小,如果能夠合并,就將父節點longest標記為相加的和(多么簡單!)。

  1. void?buddy2_free(struct?buddy2*?self,?int?offset)?{?
  2. ??unsigned?node_size,?index?=?0;?
  3. ??unsigned?left_longest,?right_longest;?
  4. ??
  5. ??assert(self?&&?offset?>=?0?&&?offset?<?size);?
  6. ??
  7. ??node_size?=?1;?
  8. ??index?=?offset?+?self->size?-?1;?
  9. ??
  10. ??for?(;?self->longest[index]?;?index?=?PARENT(index))?{?
  11. ????node_size?*=?2;?
  12. ????if?(index?==?0)?
  13. ??????return;?
  14. ??}?
  15. ??
  16. ??self->longest[index]?=?node_size;?
  17. ??
  18. ??while?(index)?{?
  19. ????index?=?PARENT(index);?
  20. ????node_size?*=?2;?
  21. ??
  22. ????left_longest?=?self->longest[LEFT_LEAF(index)];?
  23. ????right_longest?=?self->longest[RIGHT_LEAF(index)];?
  24. ??
  25. ????if?(left_longest?+?right_longest?==?node_size)?
  26. ??????self->longest[index]?=?node_size;?
  27. ????else?
  28. ??????self->longest[index]?=?MAX(left_longest,?right_longest);?
  29. ??}?
  30. }?

上面兩個成對alloc/free接口的時間復雜度都是O(logN),保證了程序運行性能。然而這段程序設計的獨特之處就在于使用加權來標記內存空閑狀態,而不是一般的有限狀態機,實際上longest既可以表示權重又可以表示狀態,狀態機就毫無必要了,所謂“少即是多”嘛!反 觀cloudwu的實現,將節點標記為UNUSED/USED/SPLIT/FULL四個狀態機,反而會帶來額外的條件判斷和管理實現,而且還不如數值那 樣精確。從邏輯流程上看,wuwenbin的實現簡潔明了如同教科書一般,特別是左右子樹的走向,內存塊的分離合并,塊索引到節點下標的轉換都是一步到 位,不像cloudwu充斥了大量二叉樹的深度和長度的間接計算,讓代碼變得晦澀難讀,這些都是longest的功勞。一個“極簡”的設計往往在于你想不到的突破常規思維的地方。

這份代碼唯一的缺陷就是longest的大小是4字節,內存消耗大。但cloudwu的博客上有人提議用logN來保存值,這樣就能實現uint8_t大小了,看,又是一個“極簡”的設計!

說實話,很難在網上找到比這更簡約更優雅的buddy system實現了——至少在Google上如此。

原文鏈接:

譯文鏈接:

轉載于:https://www.cnblogs.com/rinack/p/3368352.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/377718.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/377718.shtml
英文地址,請注明出處:http://en.pswp.cn/news/377718.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[USACO3.2.3 Spinning Wheels]

[關鍵字]&#xff1a;模擬 枚舉 [題目大意]&#xff1a;有5個輪子&#xff0c;每個輪子優r個缺口并且會按一定速度不停轉動&#xff0c;問什么時候可以使一條光線射過所有輪子。 // [分析]&#xff1a;從0到1000&#xff08;或其他的&#xff09;枚舉分鐘然后判斷&#xff0c;當…

一、SQLServer2008安裝(帶密碼)、創建數據庫、C#窗體項目測試

一、下載和安裝SQLServer2008 東西太大了&#xff0c;沒法上傳到資源里面&#xff0c;官網其他公眾號都下載可以。 右擊管理員身份 運行setup.exe 這個密鑰不能用的話&#xff0c;也可以去百度其他密鑰 JD8Y6-HQG69-P9H84-XDTPG-34MBB 建議改一下路徑&#xff0c;我這邊修…

python獲取當前日期_Python程序獲取當前日期

python獲取當前日期In the below example – we are implementing a python program to get the current date. 在下面的示例中-我們正在實現一個python程序來獲取當前日期 。 Steps: 腳步&#xff1a; Import the date class from datetime module. 從datetime模塊導入日期類…

【C++grammar】多態、聯編、虛函數

目錄1、多態概念1.多態性有兩種表現的方式2、聯編&#xff08;實現多態&#xff09;1.靜態聯編2.動態聯編3、實現運行時多態1.為何要使用運行時多態&#xff1f;2.如何實現運行時多態3.多態的例子1.調用哪個同名虛函數&#xff1f;2. 用途&#xff1a;可以用父類指針訪問子類對…

一 MVC - HtmlHelper

HtmlHelper類位于System.Web.Mvc.Html之中主要有七個靜態類組成&#xff1a; FormExtensions - BeginForm, BeginRouteForm, EndForm InputExtensions - CheckBox, CheckBoxFor, Hidden, HiddenFor, Password, PasswordFor, RadioButton, RadioButtonFor, TextBox, TextBoxFor …

HDOJ 400題紀念。

剛剛交了1506&#xff0c;無意間瞟到左邊的隨筆數&#xff0c;發現已經401題了&#xff0c;這么說前幾天就400題了啊囧。 昨天還想交到400題就先放放&#xff0c;背單詞的&#xff0c;沒想到那么快。等把USACO那個八皇后寫完吧。人生總是有許多不想做又不得不做的事情。。。 還…

二、用戶登錄和注冊

一、頁面設計 一共四個頁面 主頁面Form1&#xff0c;登錄頁面login&#xff0c;注冊頁面resister&#xff0c;主菜單頁面main_page 系統運行進入Form1&#xff0c;單擊登錄按鈕跳轉到login&#xff0c;數據庫中得存在數據信息且輸入正確才可登錄成功&#xff0c;跳轉到main_pa…

readdir函數_PHP readdir()函數與示例

readdir函數PHP readdir()函數 (PHP readdir() function) The full form of readdir is "Read Directory", the function readdir() is used to read the directory i.e. read the name of the next entry in the directory. readdir的完整形式為“ Read Directory”…

【C++grammar】訪問控制與抽象類與純虛函數

目錄一、訪問控制 (可見性控制)1.private、public、protected關鍵字2.關鍵字示例1、關鍵字對類數據成員訪問的限制3. 公有繼承4. 私有繼承5. 保護繼承6. 私有繼承和保護繼承的區別二、抽象類與純虛函數1.什么是抽象類2.抽象函數/純虛函數3.抽象類示例一、訪問控制 (可見性控制)…

mongodb 如何刪除 字段值為 json對象中的某個字段值

例如&#xff1a; { attributes: { birthday:1988-01-01, name: aq } } birthday是attributes字段的value的一個字段&#xff0c; 我要刪除birthday 用這句話&#xff1a; db.User.update({email:adminlinkris.com},{$unset:{attributes.birthday:}})轉載于:https://www.cnblog…

使用 Spring 的 Web 服務模擬器框架解決方案

http://www.ibm.com/developerworks/cn/web/wa-aj-simulator/index.html轉載于:https://www.cnblogs.com/diyunpeng/archive/2012/02/28/2371390.html

三、上傳織物圖片至SQL Server并提供name進行展示織物照片

一、數據庫的建立 還是在fiber_yy數據庫下創建images表 images表設計如下 二、頁面完善設計 main_page頁面進行功能完善 入庫管理系統 warehousing頁面 庫存查詢系統 query頁面 登錄注冊頁面前面幾個博文已經實現過了&#xff0c;這里就再贅述了&#xff0c;仍是沿用前…

gettype_PHP gettype()函數與示例

gettypePHP gettype()函數 (PHP gettype() function) In PHP, we have a library function gettype() to identify the type of data. The function is primarily used to sanity check the type of data being input in a variable. The function can identify the data into …

ARM MMU工作原理剖析[轉]

一、MMU的產生 許多年以前&#xff0c;當人們還在使用DOS或是更古老的操作系統的時候&#xff0c;計算機的內存還非常小&#xff0c;一般都是以K為單位進行計算&#xff0c;相應的&#xff0c;當時的程序規模也不大&#xff0c;所以內存容量雖然小&#xff0c;但還是可以容納當…

棧與隊列在SGI STL的底層實現

棧 棧提供push和pop等接口&#xff0c;不提供走訪功能&#xff0c;也不提供迭代器。 STL中棧不被歸類為容器&#xff0c;而被歸類為container adapter(容器適配器)&#xff0c;這是因為棧是以底層容器完成其所有的工作&#xff0c;對外提供統一的接口&#xff0c;底層容器是可…

【原創】SharePoint Document library List Check out 文檔時碰到的問題解決

環境&#xff1a;TFS(Team Foundation Server)集成的WSS 3.0&#xff08;SharePoint Service 3.0&#xff09; 問題&#xff1a;如題&#xff0c;祥見下圖 解決&#xff1a;一般碰到沒有經驗的問題&#xff0c;大家當然是外事不決問谷歌了&#xff0c;于是谷歌搜到了這篇博客 h…

getdate函數_PHP getdate()函數與示例

getdate函數PHP getdate()函數 (PHP getdate() function) getdate() function is used to get the local date/time (or it is also used to get the date/time based on the given timestamp. getdate()函數用于獲取本地日期/時間(或也用于根據給定的時間戳獲取日期/時間。 S…

四、入庫管理功能的完善

一、數據庫的創建 在fiber_yy數據庫下創建yy_textile表 先隨便添加幾條數據 二、頁面的完善 登錄注冊頁面我就不演示了&#xff0c;前幾篇博文也都有介紹 warehousing入庫頁面 main_page頁面進行功能完善 三、代碼實現 warehousing頁面 using System; using System.…

leetcode 232. 用棧實現隊列 思考分析

題目 請你僅使用兩個棧實現先入先出隊列。隊列應當支持一般隊列的支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 實現 MyQueue 類&#xff1a; void push(int x) 將元素 x 推到隊列的末尾 int pop() 從隊列的開頭移除并返回元素 int peek() 返…

YCSB初步介紹

隨著大數據時代的到來和云計算的不斷發展&#xff0c;作為云計算最基礎的設施存儲產品也越來越多&#xff0c;開源分布式存儲系統有BigTable-like系統HBase&#xff0c;dynamo-like系統Cassandra&#xff0c;voldemort&#xff0c;Riak&#xff0c;淘寶開源的OceanBase等。當然…