動態記憶體配置 (Dynamic Memory Allocation)

Overview Table

小節 主題 核心重點
9.9 動態配置器概觀 (p.875) heap 由 allocator 管理;explicit(malloc/free)vs implicit(GC)兩種風格
9.9.1 malloc / free / sbrk (p.876) malloc 回傳對齊的至少 size bytes;free 參數必須來自 malloc/calloc/realloc
9.9.2 為何要動態配置 (p.879) 資料結構大小 run time 才知道;硬編上限是維護惡夢
9.9.3 需求與目標 (p.880) 立即回應、只用 heap、對齊、不可搬動已配置區塊;最大化 throughputpeak utilization Uk
9.9.4 碎片化 (p.882) internal(block > payload)vs external(總量夠但無單一夠大 free block)
9.9.5 實作議題 (p.882) free block 組織、placement、splitting、coalescing 四大問題
9.9.6 Implicit free list (p.883) header 編碼 size+allocated bit;靠 size 欄位「隱式」串起所有 block
9.9.7 Placement (p.885) first fit / next fit / best fit 的取捨
9.9.8 Splitting (p.885) 整塊用掉(內部碎片)或切割成 allocated + 新 free block
9.9.9 擴充 heap (p.886) 先 coalesce,不夠再 sbrk 要新記憶體
9.9.10 Coalescing (p.886) 對抗 false fragmentation;immediate vs deferred
9.9.11 Boundary tags (p.887) footer = header 複本 → 常數時間合併前一塊;四種 case
9.9.12 簡單配置器實作 (p.890) implicit list + prologue/epilogue + 立即 boundary-tag coalescing
9.9.13 Explicit free list (p.898) free block 內藏 pred/succ 指標;LIFO vs address order
9.9.14 Segregated free lists (p.899) size class 陣列;simple segregated storage、segregated fits、buddy system

9.9 動態記憶體配置器與 Heap (p.875)

雖然可直接用 mmap/munmap 建立虛擬記憶體區域,C 程式設計者通常用動態記憶體配置器 (dynamic memory allocator) 取得 run-time 記憶體,較方便也較可攜。配置器維護稱為 heap 的虛擬記憶體區域:demand-zero、緊接在 .bss 之後、向高位址成長;kernel 為每個 process 維護指向 heap 頂端的 brk 指標。

高位址
+---------------------------+
|        User stack         |  向下成長
+---------------------------+
|            ...            |
+---------------------------+
| Memory-mapped region      |
|  (shared libraries)       |
+---------------------------+
|            ...            |
+---------------------------+ <-- brk (top of the heap)
|           Heap            |  向上成長
+---------------------------+
| Uninitialized data (.bss) |
+---------------------------+
| Initialized data (.data)  |
+---------------------------+
|       Code (.text)        |
+---------------------------+
低位址 (0)
Tip

記憶體配置是通用概念:圖形演算法等應用常先向標準 allocator 要一大塊,再用 application-specific allocator 自行管理其中的節點 (p.876)。

9.9.1 malloc 與 free (p.876)

#include <stdlib.h>
void *malloc(size_t size);   // 成功回傳 block 指標,失敗回傳 NULL 並設 errno
void free(void *ptr);        // 無回傳值
#include <unistd.h>
void *sbrk(intptr_t incr);   // 成功回傳舊 brk,失敗回傳 -1 (errno=ENOMEM)

Figure 9.34 (p.878) 以 16-word 小 heap 示範:malloc 從 free block 前端切塊;請求 5 words 時 malloc 實際配 6 words(padding 一個 word 維持 double-word 對齊);free 後指標 p2 仍指向舊處——應用程式有責任不再使用它

9.9.2 為何需要動態配置 (p.879)

程式常在 run time 才知道資料結構大小。用 #define MAXN 15213 硬編陣列上限:上限與實際可用虛擬記憶體無關、超過就得重新編譯,在百萬行等級的軟體中是維護惡夢。改用 array = (int *)Malloc(n * sizeof(int)) 在讀到 n 之後才配置,上限僅受可用虛擬記憶體限制。

9.9.3 配置器的限制與目標 (p.880)

Explicit allocator 的硬性限制:

限制 意義
處理任意 request 序列 不可假設 allocate/free 成對或巢狀;free 必須對應先前配置且目前 allocated 的 block
立即回應 不可為了效能而重排或緩衝 requests
只使用 heap 配置器自身的非 scalar 資料結構也必須存在 heap 內
對齊 blocks block 必須能容納任何型別的資料物件
不修改已配置 blocks 只能操作 free blocks;不允許 compaction(搬動已配置 block)

兩個互相衝突的目標 (p.881):

Uk=maxikPiHk

目標是最大化整個序列的 Un1

Warning

Hk 單調不減是簡化假設;p.882 Aside 指出可放寬為「前 k+1 個 requests 的 high-water mark」,允許 heap 伸縮。追求極端 throughput(如下述 naive allocator)會犧牲 utilization,設計的挑戰即在兩者間取得平衡。

9.9.4 碎片化 (Fragmentation) (p.882)

Fragmentation = 未使用的記憶體無法滿足 allocate requests,是 heap 利用率差的主因。

Internal fragmentation External fragmentation
定義 allocated block 比 payload 大 總 free 記憶體足夠,但無單一 free block 夠大
成因 最小 block size 限制、對齊 padding free 空間被切碎散佈在多個 blocks
量化 容易:(block sizepayload),只取決於過去的 requests 與實作 困難:還取決於未來 requests 的模式,無法預測
對策 好的 placement / 精簡 block 格式 配置器以 heuristics 維持「少量大 free blocks」而非「大量小 free blocks」

9.9.5 實作議題 (p.882)

Naive allocator:heap 當大 byte 陣列,malloc 就遞增指標 p,free 什麼都不做——throughput 極佳但從不重用 blocks,utilization 極差。實用配置器必須處理四個議題:

  1. Free block organization:如何追蹤 free blocks?
  2. Placement:選哪個 free block 放新配置的 block?
  3. Splitting:放進去之後,剩餘部分怎麼辦?
  4. Coalescing:block 剛被 free 之後怎麼辦?

9.9.6 Implicit Free List (p.883)

block 格式:1-word header + payload + 選擇性 padding。header 編碼 block size(含 header 與 padding)與 allocated bit。double-word 對齊下 size 是 8 的倍數 → 低 3 bits 恆為 0,只需存高 29 bits,低 3 bits 可編碼其他資訊(最低位 = allocated bit,a=1 allocated、a=0 free):

 31                      3 2 1 0
+------------------------+-----+
|      Block size        |0 0 a|  Header (1 word)
+------------------------+-----+
|          Payload             |  malloc 回傳指向此處的指標
|    (allocated block only)    |
+------------------------------+
|      Padding (optional)      |
+------------------------------+

header 編碼公式:header = size | alloc_bit

heap 組織成連續的 allocated/free blocks 序列,free blocks 由 header 的 size 欄位隱式串連——走訪所有 blocks 即可間接走訪所有 free blocks:

Start                                                     終止 header
 of   | unused | 8/0 | 16/1 | 32/0 | 16/1 | 0/1 |
heap    padding  free  alloc  free   alloc  epilogue(size 0, a=1)
        (標記為 size/allocated bit)
Important

Practice 9.6 (p.885) 模式:size 向上取整公式(此格式,header 1 word、無 footer)= round_up(payload + 4, 8)。例:malloc(2)→8 (0x9)、malloc(9)→16 (0x11)、malloc(15)→24 (0x19)、malloc(20)→24 (0x19)。

9.9.7 放置策略 (Placement) (p.885)

應用程式要求 k bytes 時,配置器依 placement policy 搜尋夠大的 free block:

策略 作法 優點 缺點
First fit 從 list 開頭找第一個能容納者 傾向把大 free blocks 留在 list 尾端 開頭累積小碎片「splinters」,拉長大 block 的搜尋時間
Next fit (Knuth 提出) 從上次搜尋停止處繼續 可比 first fit 快很多(尤其開頭多 splinters 時) 研究顯示 utilization 比 first fit 差
Best fit 檢查每個 free block,選最小的能容納者 utilization 通常最佳 在 implicit list 等簡單組織上需徹底搜尋 heap

Segregated free list(9.9.14)可不需徹底搜尋就近似 best fit

9.9.8 分割 Free Blocks (Splitting) (p.885)

找到 fit 之後的政策決定:

9.9.9 取得額外 Heap 記憶體 (p.886)

找不到 fit 時的流程:

找不到 fit
   |
   v
合併 (coalesce) 實體相鄰的 free blocks 造出更大 free block
   |
   +-- 仍不夠大 / 已最大程度合併?
   |
   v
呼叫 sbrk 向 kernel 要額外 heap 記憶體
   |
   v
把新記憶體轉成一個大 free block,插入 free list,
再把 requested block 放進這個新 free block

9.9.10 合併 Free Blocks (Coalescing) (p.886)

free 一個 block 後,相鄰可能已有其他 free blocks,造成 false fragmentation:可用記憶體被切成許多小而無用的 free blocks(如兩個相鄰 3-word-payload free blocks 無法滿足 4-word 請求,即使總量足夠)。實用配置器必須合併相鄰 free blocks

9.9.11 Boundary Tags (p.887)

合併下一個 block 容易:current header 走到 next header,檢查 allocated bit,若 free 就把 size 加進 current header,常數時間。合併前一個 block 在只有 header 的 implicit list 中得線性搜尋整個 list。

Knuth 的 boundary tags:在每個 block 尾端加 footer(header 的複本)。前一個 block 的 footer 永遠位於 current block 開頭往前一個 word 處 → 常數時間得知前一 block 的起點與狀態。

+------------------------+-----+
|      Block size        | a/f |  Header
+------------------------+-----+
|          Payload             |
+------------------------------+
|      Padding (optional)      |
+------------------------------+
|      Block size        | a/f |  Footer (boundary tag, header 複本)
+------------------------------+

free 當前 block(n)時的四種 case(皆常數時間):

Case prev next 動作
1 allocated allocated 不合併,只把 current 標成 free
2 allocated free 合併 current+next:更新 current 的 header 與 next 的 footer 為 n+m2
3 free allocated 合併 prev+current:更新 prev 的 header 與 current 的 footer 為 n+m1
4 free free 三塊全合併:更新 prev 的 header 與 next 的 footer 為 n+m1+m2
Tip

省 footer 的最佳化 (p.888):footer 的 size 欄位只在前一 block 是 free 時才需要。把「前一 block 的 allocated/free bit」存入 current header 多餘的低位 bit,則 allocated blocks 不需 footer(空間可作 payload);free blocks 仍需 footer。這在應用程式操作大量小 blocks 時很重要——否則 header+footer 可能吃掉小 block 的一半空間。

Practice 9.7 (p.888) 最小 block size(header/footer 各 4 bytes、payload 不可為 0):

對齊 Allocated block 格式 Free block 格式 最小 block size
Single word (4B) header + footer header + footer 12 bytes
Single word (4B) 只有 header header + footer 8 bytes
Double word (8B) header + footer header + footer 16 bytes
Double word (8B) 只有 header header + footer 16 bytes

9.9.12 實作簡單配置器 (p.890)

implicit free list + immediate boundary-tag coalescing 實作;最大 block size 232=4GB,程式碼 64-bit clean(gcc -m32/-m64 皆可)。

memlib.c 記憶體模型 (p.890):mem_init 用大陣列模擬 heap(避免干擾系統 malloc);mem_sbrk 介面/語意同 sbrk拒絕縮小 heap。配置器 (mm.c) 匯出三個函式:mm_init(成功回傳 0)、mm_mallocmm_free(語意同系統版本)。最小 block size = 16 bytes

Heap 不變形式 (invariant form) (p.891, Figure 9.42):

        Prologue    Regular    Regular        Regular   Epilogue
        block       block 1    block 2  ...   block n   block
+------+----+----+---------+----------+-----+---------+-----+
|unused| 8/1| 8/1|hdr .. ftr|hdr .. ftr| ... |hdr .. ftr| 0/1 |
+------+----+----+---------+----------+-----+---------+-----+
 pad     hdr  ftr  ^
        (prologue) heap_listp

常數與巨集 (Figure 9.43, p.893)——考試常考的指標運算:

#define WSIZE     4            /* word / header / footer 大小 */
#define DSIZE     8            /* double word */
#define CHUNKSIZE (1<<12)      /* 每次擴充 heap 的預設量 = 4 KB */

#define PACK(size, alloc)  ((size) | (alloc))
#define GET(p)        (*(unsigned int *)(p))
#define PUT(p, val)   (*(unsigned int *)(p) = (val))
#define GET_SIZE(p)   (GET(p) & ~0x7)
#define GET_ALLOC(p)  (GET(p) & 0x1)
#define HDRP(bp)      ((char *)(bp) - WSIZE)
#define FTRP(bp)      ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))

bp(block pointer)指向第一個 payload byte;巨集可組合,如 GET_SIZE(HDRP(NEXT_BLKP(bp))) 取得下一 block 的 size。

mm_init (p.894):mem_sbrk(4*WSIZE) 取 4 words → 依序寫入 padding、prologue header PACK(DSIZE,1)、prologue footer、epilogue header PACK(0,1);heap_listp += 2*WSIZE;再 extend_heap(CHUNKSIZE/WSIZE) 建立初始 free block。

extend_heap (p.894):在 (1) heap 初始化與 (2) mm_malloc 找不到 fit 時被呼叫。把 words 向上取整為偶數(size = (words%2) ? (words+1)*WSIZE : words*WSIZE)維持對齊;mem_sbrk 回傳的 chunk 緊接在舊 epilogue header 之後——舊 epilogue header 變成新 free block 的 header,chunk 最後一個 word 寫入新 epilogue header;最後呼叫 coalesce(bp)(舊 heap 很可能以 free block 結尾)。

mm_free 與 coalesce (p.896):mm_free 把 header/footer 的 alloc bit 清 0 後呼叫 coalesce;coalesceGET_ALLOC(FTRP(PREV_BLKP(bp)))GET_ALLOC(HDRP(NEXT_BLKP(bp))) 分四個 case 實作 Figure 9.40,case 3/4 最後 bp = PREV_BLKP(bp)

mm_malloc (p.897):

  1. size == 0 的 spurious request 回傳 NULL。
  2. 調整 block size(adjusted size,含 header/footer overhead 與對齊):
if (size <= DSIZE)
    asize = 2*DSIZE;                                   /* 最小 16 bytes */
else
    asize = DSIZE * ((size + (DSIZE) + (DSIZE-1)) / DSIZE);  /* 加 overhead 後向上取整到 8 的倍數 */
  1. find_fit(asize) 搜尋 free list;找到就 place(bp, asize)(放置並視情況分割)。
  2. 找不到則 extendsize = MAX(asize, CHUNKSIZE),extend_heap(extendsize/WSIZE) 後再 place。

Practice 9.8/9.9 (p.897):find_fit 為 implicit list 的 first-fit 搜尋;place 把 block 放在 free block 開頭,只有剩餘部分 ≥ 最小 block size (16 bytes) 才分割

9.9.13 Explicit Free Lists (p.898)

implicit list 的配置時間與總 block 數成線性,不適合通用配置器(僅適合 block 數已知很小的特殊用途)。改為把 free blocks 組成顯式資料結構:free block 的 body 程式用不到,可存放指標——如在每個 free block 內放 pred / succ 指標構成雙向鏈結 free list:

 Allocated block               Free block
+--------------+-----+       +--------------+-----+
|  Block size  | a/f |       |  Block size  | a/f |   Header
+--------------------+       +--------------------+
|                    |       | pred (predecessor) |
|     Payload        |       +--------------------+
|                    |       | succ (successor)   |
+--------------------+       +--------------------+
| Padding (optional) |       |   (old payload)    |
+--------------------+       +--------------------+
|  Block size  | a/f |       |  Block size  | a/f |   Footer
+--------------+-----+       +--------------+-----+
排序政策 free 時間 特性
LIFO(新 free 的插到 list 開頭) 常數時間(搭配 boundary tags 則 coalescing 也是常數) first fit 會先檢查最近使用過的 blocks
Address order(list 中位址遞增) 線性(需搜尋找 predecessor) address-ordered first fit 的 utilization 較佳,接近 best fit

9.9.14 Segregated Free Lists (p.899)

Segregated storage:維護多條 free lists,每條放大小相近的 blocks——把所有可能的 block size 分割成 size classes(等價類)。例如以 2 的冪分割:

{1},{2},{3,4},{58},,{10252048},{20494096},{4097}

或小 blocks 各自成類、大 blocks 以 2 的冪分割。配置器維護一個 free list 陣列(依 size 遞增排列);要 size n 的 block 時搜尋對應的 list,不夠就搜尋下一個更大的 class。

Simple Segregated Storage (p.900)

每個 size class 的 free list 只含同一大小的 blocks(= 該 class 的最大元素;如 class {17–32} 的 list 全是 32-byte blocks)。

Segregated Fits (p.900)

每條 free list 對應一個 size class,內含該 class 中各種大小的 blocks(explicit 或 implicit list)。

Buddy Systems (p.901)

segregated fits 的特例:每個 size class 都是 2 的冪。heap 為 2m words,為每個 2k(0km)維護 free list;請求大小向上取整到最近的 2 的冪。

allocate 2^k:
  找最小的可用 2^j (k <= j <= m)
  j == k ? 完成
         : 反覆對半分割直到 j == k,
           每次分出的另一半 (buddy) 放入對應 free list

free 2^k:
  與 free 的 buddy 持續合併,遇到 allocated buddy 即停止

關鍵性質:由 block 的位址與大小可直接算出 buddy 的位址——兩者恰好差一個 bit。例:size 32 bytes、位址 xxx...x00000 的 block,其 buddy 在 xxx...x10000


各種 Free List 組織比較

組織 allocate 時間 free 時間 utilization 備註
Implicit free list O(總 block 數) 常數(含 boundary-tag coalescing) 取決於 placement 最簡單;教學用
Explicit (LIFO) O(free block 數) 常數 較差 需 pred/succ → 最小 block 變大
Explicit (address order) O(free block 數) 線性 接近 best fit 找 predecessor 需搜尋
Simple segregated storage 常數 常數 差(不 split/coalesce) 最小 block = 1 word
Segregated fits 限定於部分 heap 的搜尋 常數級 佳(近似 best fit) GNU malloc 採用
Buddy system 快(buddy 合併) internal fragmentation 嚴重 size 皆為 2 的冪

Exam/Test Patterns

情境 / 關鍵字 答案
malloc 回傳位址的對齊 32-bit mode:8 的倍數;64-bit mode:16 的倍數
要「清零」的動態記憶體 / 改變 block 大小 calloc / realloc
sbrk(0) 回傳什麼 目前的 brk 值(heap 頂端)
求 header 值(size 24、allocated) 0x18 | 0x1 = 0x19(size 含 header/padding;低 3 bits 中最低位是 alloc bit)
從 header 取 size / alloc bit GET(p) & ~0x7 / GET(p) & 0x1
計算 peak utilization Uk=maxikPi/Hk;Pk = aggregate payload,Hk = heap 大小
「總 free 空間夠,單一 block 不夠」 external fragmentation(取決於未來 requests,難量化)
「block 比 payload 大」(對齊/最小 size) internal fragmentation(= block size − payload 總和)
為何 free 後相鄰小 free blocks 無法滿足請求 false fragmentation → 需要 coalescing
常數時間合併「前一個」block 的技術 Knuth 的 boundary tags(footer = header 複本)
allocated block 何時可省 footer 把前一 block 的 alloc bit 存入 current header 的多餘低位 bit;free block 仍需 footer
prologue/epilogue block 的目的 消除 coalescing 的邊界條件檢查(皆標記 allocated;epilogue size=0)
mm_malloc 的 asize 計算 size<=8 → 16;否則 8*((size+8+7)/8)(加 header/footer overhead 再向上取整到 8)
first fit vs next fit vs best fit first fit 留大塊在尾端但開頭生 splinters;next fit 較快但 utilization 較差;best fit utilization 最佳但需徹底搜尋
implicit vs explicit list 的 allocate 複雜度 總 block 數線性 → free block 數線性
LIFO vs address-ordered explicit list LIFO free 常數時間;address order free 線性但 utilization 接近 best fit
simple segregated storage 最小 block size 1 word(只需 free block 的 succ 指標;無 header/footer、不 split、不 coalesce)
GNU malloc 採用的組織 segregated fits(快且省記憶體;first fit 近似全 heap best fit)
buddy 位址計算 block 與 buddy 位址恰差一個 bit:size 2k、位址 x...x00000 → buddy x...x10000
free 一個非 malloc 回傳的指標 undefined behavior,且 free 不回報錯誤 → 詭異的 run-time errors(見 9.11)
Warning

常見誤區:(1) header 的 size 是整個 block(含 header、payload、padding),不是 payload;(2) free 之後指標仍指向原處(dangling pointer),不使用它是應用程式的責任;(3) allocator 不允許 compaction,所以 external fragmentation 無法靠搬動解決;(4) FTRPNEXT_BLKP 等巨集依賴 HDRP(bp) 中 size 已正確——coalesce 中更新順序錯誤會算錯位址。