contributed by < WangHanChi
>
原本的程式碼 clone 下來進行編譯的時候會出現這個問題
後來詳細了解原因後發現是 purge_ratio
在 purge.h 定義,而 purge.h 又被 alloc.c 與 huge.c 所 include ,才導致出現變數重複的問題,因此稍微修改程式碼就可以正常編譯
修改的程式碼
這樣就可以正常編譯了
我們可以知道使用 mmap 系統呼叫的時候,所請求的尺寸會對齊 page 的大小,像是如果請求 4095 bytes 或是 30 bytes 都會返回一個 4096 bytes 的大小回來 (在我的設備上),但是我做了一個小小的實驗, 程式碼如下
主要就是配置一個 8192 bytes 的空間,接著用 int 型別填滿它,原本預期是只能填入 2048
個 int ,但是如程式碼一樣填入了 2100 個卻是可以正常執行的,不知道為何會這樣。
使用 mmap
系統呼叫分配虛擬記憶體之後,這塊虛擬記憶體已經映射到進程的虛擬地址空間中了。但是,這個虛擬記憶體可能只是一個空的映射區域,實際的實體記憶體頁面還未分配或者還未初始化。所以,如果你想在這個虛擬記憶體中存儲數據,就需要使用 memory_commit
系統呼叫,讓作業系統為這個虛擬記憶體分配實體記憶體頁面,並將其初始化為 0
或其他的數據。
在 Linux 中,可以使用 mmap
系統呼叫分配一個虛擬記憶體區域,然後使用 mprotect
系統呼叫將這個區域設置為可讀可寫,最後使用 memory_commit
函數為這個區域分配實體記憶體頁面
在使用 munmap
系統呼叫釋放已經分配的虛擬記憶體時,通常不需要先使用 memory_decommit
函數取消映射區域的實體記憶體頁面。因為 munmap
函式會自動釋放該映射區域的實體頁面,並且會清空所有已經映射的內容。
事實上,memory_decommit
函數是一個專門用於取消映射區域的實體記憶體頁面的系統呼叫。如果你使用 memory_decommit
函數取消了映射區域的實體記憶體頁面,再使用 munmap
函數釋放虛擬記憶體時,系統可能會拋出錯誤或者行為不可預測。因此,在正常情況下,不需要在 munmap
前使用 memory_decommit
。
在 Linux 64 位版本下,虛擬記憶體的大小是從 0x0000000000000000 到 0x7fffffffffff,也就是 128TB(2的47次方)。這個範圍被稱為「Canonic Virtual Address Space」(簡稱 CVA)。
可以透過以下命令來取得 HugePageSize 為 2 MB
並且透過以下命令來看到自己有沒有開始這個功能
可以看到 [ ]
在 madvise 的位置,代表者只在MADV_HUGEPAGE標記的 VMA 使用 THP
Huge pages 是從 Linux Kernel 2.6 後被引入的,目的是通過使用大頁的記憶體來取代傳統的 4kb 記憶體頁, 以適應越来越大的記憶體,讓作業系统可以支持现代硬體架構的大頁功能。
Huge pages 有两種格式大小: 2MB 和 1GB , 2MB 頁的大小適合用於 GB 大小的記憶體 (Ex. 我們目前所使用的電腦), 1GB 頁大小適合用於 TB 级别的記憶體; 2MB 是預設的大小。
是一種在 Linux 作業系統中的記憶體管理機制,主要用於提高大型應用程式的性能。這種機制允許 kernel 自動管理大型記憶體頁(稱為“大頁”),這些頁比常規頁大許多。大頁的大小通常為 2MB 或1GB,相較於 4KB 的標準頁大小,可以減少記憶體管理的開銷和提高記憶體讀取/寫入的效率。
透明大頁支持機制允許 kernel 自動將一些散裝的標準頁合併為大頁,從而減少了管理頁表的開銷。這種機制還允許 kernel 自動將散裝的小頁轉換為大頁,從而減少記憶體碎片化的問題,並且可以更有效地利用記憶體。
透明大頁支持機制還有一個重要的優點是,它幾乎不需要應用程式的修改即可實現。這意味著應用程式可以繼續使用標準的記憶體分配和管理技術,而無需對程式碼進行修改。這種透明性使得大多數應用程式可以輕鬆地利用透明大頁支持機制,而不會帶來任何性能或兼容性問題。
並且由於 Huge pages 很難手動管理,而且通常需要對程式碼進行重大的更改才能有效的使用,因此 RHEL 6 開始引入了 Transparent Huge Pages ( THP ), THP 是一個抽象層,能夠自動創建、管理和使用傳統大頁 (Huge pages)。
從 man page 看起
posix_madvise(3)
關於 advice 的引數有以下幾種
map_populate
是 mmap()
系統呼叫的一個 flag,用於指示 kerenl 應在映射期間立即將文件中的所有內容讀入到新創建的映射區域中。這樣可以提高後續對這些內容的存取速度,因為所有資料已經存在於頁面快取中,而不必等到實際存取這些頁面時才進行disk I/O。
使用 map_populate
標誌可以顯著提高文件映射的性能,但需要額外的記憶體空間和 I/O 運算。因此,只有在確定需要快速且頻繁地存取整個映射區域的情況下才應使用 map_populate
標誌。
map_populate
可以讓程式在第一次存取新映射的記憶體時,避免因 page fault 產生的頁錯誤中斷,進而提高程式的運行效率。因為當 mmap 設置 MAP_POPULATE 標誌時, kernel 會在進行映射時將文件的內容直接讀取到物理頁中,這樣就可以避免第一次存取時因 page fault 而造成的缺頁中斷。map_populate
可能會導致一定程度上的性能下降。這是因為 kernel 在 mmap 函數調用時,需要在文件系統和文件快取之間進行多次的頁面交換,這會導致額外的 I/O 操作和 CPU 資源的消耗。此外,對於非常大的文件或共享記憶體區域,kernel 可能無法在 mmap 調用期間完成整個映射區域的填充,這樣可能會導致一些頁面在第一次存取時仍然需要進行缺頁中斷。rb.h
首先先看到 rb.h ,可以發現它與 quiz4 的 rb.h 是差不多的,因此這裡就不過多進行介紹
詳細的筆記可以參考 wanghanchi / linux2023-quiz4 與 wanghanchi / linux2023-tree
test_small.c
這段程式碼測試了程式在多執行緒情況下,能否正確地分配及收回記憶體。特別是在這種分配的記憶體數量是比較小 (16 Bytes) 且大量 (10000000個) 的情況下。
使用了 alloc_so
來進行測試,發現可以正常執行並且檢查回傳值也是 0
test_large.c
在這段程式碼中,使用 malloc
來配置了一個大小為 4096 * 4 (16KB) 的記憶體區域,然後使用多次 realloc 來改變其大小。在每次 realloc 調用之後,我們都檢查返回的指標是否與原始指標相同,以確認是否發生了記憶體區域的移動。
可以從註解看到 // in-place shrink
與 // in-place expand
。如果需要重新分配的記憶體大小比先前的還要小的話,是會將已分配的記憶體空間縮小至指定的大小; 相對地,若是要重新分配的空間比原本的還要大的話,就會指向新分配的更大記憶體。
直接從 realloc 這個程式碼開始看!
首先先了解對於 small 與 large 的 size 定義
可以得知 MAX_SMALL 為 512 ,而 MAX_LARGE 為 ((4096 * 1024) - (32 + 32)) = 4194240
接著看到從 realloc 的其中一段程式碼看到原地縮小以及原地擴張的條件為 <= MAX_LARGE
還有 >= MAX_SMALL
,也就是說要 realloc 的大小在 512 ~ 4194240 之間就不會去改變記憶體的初始位置而只有改變大小
接著一樣測試看看程式
回傳值為 0
,代表沒有問題 !
test_huge.c
這段程式碼主要也是在測試 malloc 與 realloc 的一些行為,主要可以分成以下幾點
首先,上面已知道 CHUNK_SIZE
為 4096 * 1024 = 4194304 ,因此只要分配了一個 CHUNK_SZIE
就會直接超出 MAX_LARGE
的大小
接著看到 realloc 的程式碼,可以看到如果超過了 MAX_LARGE
的大小之後,就會使用 huge_realloc
再來看到 huge_realloc 的程式碼
可以看到如果 new_real_size < old_size
, 就會不改變原本這塊記憶體的初始位置,並且進行大小裁剪; 如果 new_real_size > old_size
的話就會先嘗試不改動初始位置進行配置,如果失敗的話才去重新分配一塊新的記憶體位置; 而至於位什麼不考慮 new_real_size == old_size
的情況是因為在 realloc 函式的一開始就有檢查兩個大小是否一樣,因此不會有這種情況發生。
接著繼續往下查看 huge_no_move_expand
其中 memory_commit
的用處是將之前使用 mmap 分配的虛擬記憶體 page 實際分配到實體記憶體中。在 Linux 中,mmap 分配的虛擬記憶體 page 不會直接分配到實體記憶體中,而是分配到虛擬記憶體區域(Virtual Memory Area,VMA)中,當應用程式實際使用該 page 時,VMA 才會將其分配到實體記憶體中,這個過程也被稱為 page fault。
memory_commit
函式會呼叫 mprotect 函式,將指定的位址和大小的記憶體區域的保護權限設定為可讀可寫。由於在 Linux 中只有具有寫權限的頁才會被分配到實體記憶體中,因此透過將記憶體區域的保護權限設定為可讀可寫,就可以將之前分配的虛擬記憶體頁分配到實體記憶體中。
接著回去看到 huge_realloc
這個函式,可以看到如果有進到 huge_move_expand
這個函式的話,就會如同註解所說的用到 MREMAP
這個系統呼叫,從 man page 可以看到它會重新映射一個已經存在的虛擬記憶體區域,並且可以改變這個記憶體的大小。
接著看回 test_huge.c ,可以看到註解一段寫著 // madvise purge
,從 man page 可以知道他是要告知作業系統這塊記憶體不會被用到,可以進行釋放
從 huge_move_expand
這個函式中的 memory_decommit
函式中可以發現這個系統呼叫並且註明了使用 MADV_DONTNEED
接著就進行程式測試
發現 main 的回傳值竟然不是 0
,於是開始尋找哪個部份回傳 1
的,最後發現是在第 54 行的時候 return 的
可以用個簡單的實驗,將 54 行的回傳值修改成 10 ,並重新測試
可以看到確實是這個部份進行回傳的,看來 p 跟 dest 並不會使用同一個記憶體地址
從 memory_remap_fixed
這個函式看到使用了 MREMAP_MAYMOVE|MREMAP_FIXED
這樣的 flags 給作業系統,代表可能在配置記憶體的過程中作業系統會幫我們尋找可配置的位置,並且不會與其他的重疊如果有找到的話就會進行移動,但是如果失敗就會留在原本的位置。
所以我認為這邊會進行 return 應該是正確的。
alloc.c
主要想要了解這個專案對於記憶體是如何進行管理的
可以看到從測試文件 test_small, test_large, test_huge 看到這個分配器應該是有針對三種尺寸的記憶體來進行分配的。
接著在用一個大的結構包住這三個記憶體管理器
接著這些的初始化定義會是在 malloc_init
與 malloc_init_slow
這幾個函式
malloc_init_slow
malloc_init
透過這四個初始化函式來進行初始化
再來看到 allocate_small
這個函式,其中會用到 slab_first_alloc
與 slab_allocate
以及 struct slab
首先先看到這兩個 struct
struct slot 的 data 使用 uint8_t
的陣列型態 ([]),這是因為 uint8_t 是一個佔用一個 byte 的無號整數型態,所以使用 uint8_t data[] 的方式可以讓 data 陣列的大小動態指定為所需的大小,同時也讓資料塊的對齊方式更加靈活
struct slab 中的 data 也使用了類似的方式,用 uint8_t data[] 來表示一塊大小可變的記憶體。這樣的設計可以減少記憶體碎片,因為在記憶體池中,所有的資料塊大小都是固定的,所以如果每個 slab 中的 data 都使用固定大小的陣列,就可能會產生很多無法被利用的空間
更多關於
uint8_t *data
與uint8_t data[]
的探討可以從 課堂問答筆記 week 11 中找到
先做一點前置設定
另外這邊的 test_small 有下修 N 的數量為 30000。
可以看到有時候會偵測不到 branch-misses ,但是有時候又可以,不知為什麼會這樣,但是這並不是目前的重點,所以先忽略不看。先把重點放在 page-faults 與 context-switches 還有 instructions 的數量上面
在 Design 這個篇章可以看到幾個重點
在 Thread Safety 這個篇章有幾個重點
PRE_POPULATE_PAGES
在 Makefile 全局 cache 中啟用時,將創建根本結構和區域 bitmap(但不包括保存用戶資料的頁面),指示 MAP_POPULATE
kernel 預填充 page table , 從而減少頁面錯誤並提高性能SMALLEST_CHUNK_SZ
(16 或是 64) 至 SMALL_SIZE_MAX
(65535) 的最小值和最大值USE_SPINLOCK
使用自旋鎖透過 ( atomic_flag ) 而不是 pthread 互斥鎖。IsoAlloc 的性能和負載測試表明自旋鎖比互斥鎖稍慢,因此它不是首選的默認選項。$ cat /proc/meminfo | grep Hugepagesize
來取得 huge size 的大小IsoAlloc 設計了一些重要的快取技術,這些技術顯著提高了分配/釋放 hot path 的性能,並保持了設計的簡單性。
next_sz_index
成員鏈接在一起,該成員告訴分配器在 _root->zones
陣列中可以找到持有相同大小 chunks 的下一個區域。此查找表可幫助我們在 O(1) 時間內找到持有特定大小的第一個區域。這是通過將一個區域的索引值放置在該區域大小索引的位置上實現的,例如 zone_lookup_table[zone->size] = zone->index
,然後我們只需要使用下一個區域的索引成員,像單向 list 一樣走訪以找到該大小的其他區域Mimalloc 目前僅支援類 Unix 的系統來作使用,像是 Ubuntu 就很適合。
其中支援了很多的記憶體配置器,像是
在 README.md 中還有更多的記憶體配置器介紹
可以分成兩種
其中壓力測試包括了
目前已完成
待完成
目前遇到問題
目前打算先移除掉 srbk / brk 這樣的系統呼叫,一律改用 mmap 系統呼叫。
目前小尺寸的分配採用 linked-list 來進行
並且引入了 a1091150/2023q1_Homeworl6_quiz5 的 memory pool ,經過修改後進行使用
可以看到這個 commit 版本的程式碼在距離 allociator 還差得很遠
以下列出 test_small 與 test_large 的 perf performance
可以看到目前的 test_large 的表現是還不錯的,但是還有一些需要改進的地方,像是記憶體的位置可能重新分配要盡量固定在原地等等的問題。
再來應該著手改善的是要進行小尺寸記憶體配置器的效能改善以及設計一個可以選擇要用小尺寸還是大尺寸的選擇器。
首先先用 perf graph 來看看究竟是哪個環節佔用了最多的時間
可以得到以下結果
可以看到大部份的時間都是在 pool_free
與 get_loc_to_free
這兩個函式之間,所以可能要再回去重新設計這兩個函式。
從 test_small.c
這個測試來看到它所測試的是 FIFO ,而我所進行的實作卻是 LIFO ,因此在面對到這樣的測試的時候,就會顯的特別的耗時
若是將 test_small.c
修改成從這個 array 的末端開始釋放的話,就可以看到他的性能明顯的提昇
但是這樣做並不是一個完美的解決方法,因此,可以將改為 tree 來進行,這樣就可以至少保證搜尋的時間會是 ,詳見 Red–black tree 。
或是使用
這幾種方式來改善效能。
注意用詞: "physical memory" 應翻譯為「實體記憶體」,以強調「虛擬記憶體」(virtual memory) 的相對。繁體中文措辭應當追求信達雅,避免受到躁進又在審查中閃躲的中國網路用語影響。