手把手教你實(shí)現(xiàn) C++ 高性能內(nèi)存池,相比 malloc 性能提升7倍!
你知道嗎?在高并發(fā)場景下,頻繁的malloc和free操作就像是程序的"阿喀琉斯之踵",輕則拖慢系統(tǒng)響應(yīng),重則直接把服務(wù)器拖垮。
最近我從0到1實(shí)現(xiàn)了一個高性能內(nèi)存池,經(jīng)過嚴(yán)格的壓測驗(yàn)證,在8B到2048B的分配釋放場景下,性能相比傳統(tǒng)的malloc/free平均快了4.5倍!今天就來給大家分享這個實(shí)現(xiàn)過程,相信看完后你也能寫出自己的高性能內(nèi)存池。
數(shù)據(jù)最有說服力,來看看實(shí)測結(jié)果:

看到了嗎?相比標(biāo)準(zhǔn)malloc/free,平均性能提升4.62倍,最高達(dá)到7.37倍!
1. 為什么需要內(nèi)存池?
在開始擼代碼之前,我們先來聊聊為什么要造這個輪子。
(1) 傳統(tǒng)內(nèi)存分配的痛點(diǎn)
你有沒有遇到過這些情況:
- 頻繁分配小對象:比如游戲服務(wù)器中每秒創(chuàng)建成千上萬個臨時對象
- 內(nèi)存碎片化:明明還有很多空閑內(nèi)存,但就是分配不出連續(xù)的大塊
- 性能瓶頸:高并發(fā)場景下malloc成為系統(tǒng)的性能瓶頸
- 內(nèi)存泄漏:忘記free導(dǎo)致的內(nèi)存泄漏,讓人頭疼不已
這些問題的根源在于:系統(tǒng)級的內(nèi)存分配器設(shè)計得太通用了。它要處理各種大小的內(nèi)存請求,要考慮各種邊界情況,這就導(dǎo)致了性能上的妥協(xié)。
(2) 內(nèi)存池的優(yōu)勢
內(nèi)存池就像是給程序開了個"專屬食堂":
- 速度快:預(yù)先分配好內(nèi)存,拿來就用,不用每次都找系統(tǒng)要
- 減少碎片:統(tǒng)一管理,按需切分,內(nèi)存利用率更高
- 避免泄漏:集中管理,程序結(jié)束時統(tǒng)一釋放
- 可控性強(qiáng):自己的地盤自己做主,可以根據(jù)業(yè)務(wù)特點(diǎn)優(yōu)化
2. 設(shè)計思路:三層架構(gòu)設(shè)計
經(jīng)過大量調(diào)研和思考,我采用了類似 TCMalloc 的三層架構(gòu):
┌─────────────────────────────────────────────────────────┐
│ 應(yīng)用程序 │
└─────────────────────┬───────────────────────────────────┘
│ ConcurrentAlloc() / ConcurrentFree()
┌─────────────────────▼───────────────────────────────────┐
│ ThreadCache (線程緩存) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 8B │ │16B │ │32B │ │... │ 每個線程獨(dú)享 │
│ │List │ │List │ │List │ │List │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────┬───────────────────────────────────┘
│ 批量獲取/歸還
┌─────────────────────▼───────────────────────────────────┐
│ CentralCache (中央緩存) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 8B Span │ │16B Span │ │32B Span │ 全局共享,桶鎖 │
│ │ List │ │ List │ │ List │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────┬───────────────────────────────────┘
│ 申請新Span
┌─────────────────────▼───────────────────────────────────┐
│ PageHeap (頁堆) │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 1頁 │ │ 2頁 │ │ 4頁 │ │ 8頁 │ 管理大塊內(nèi)存 │
│ │ Span │ │ Span │ │ Span │ │ Span │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
└─────────────────────┬───────────────────────────────────┘
│ 系統(tǒng)調(diào)用
┌─────────────────────▼───────────────────────────────────┐
│ 操作系統(tǒng) │
│ (mmap/VirtualAlloc) │
└─────────────────────────────────────────────────────────┘(1) 為什么是三層?
這個設(shè)計的精妙之處在于分層解耦:
- ThreadCache:每個線程都有自己的緩存,分配時無需加鎖,速度飛快
- CentralCache:當(dāng)ThreadCache沒有合適的內(nèi)存塊時,向CentralCache申請
- PageHeap:管理大塊內(nèi)存,當(dāng)CentralCache也沒有時,向系統(tǒng)申請內(nèi)存
這樣設(shè)計的好處是:大部分情況下分配操作都在ThreadCache完成,無鎖且極快;只有在必要時才會涉及鎖操作。
(2) 第一層:ThreadCache(線程本地緩存)
設(shè)計理念:每個線程擁有獨(dú)立的內(nèi)存緩存,消除鎖競爭。
class ThreadCache {
private:
FreeList free_lists_[NFREELISTS]; // 208個不同大小的自由鏈表
static thread_local ThreadCache* tls_thread_cache_;
public:
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
};核心優(yōu)化點(diǎn):
- 無鎖設(shè)計:線程本地存儲,天然線程安全
- 多級緩存:208個不同大小的自由鏈表
- 批量操作:與中心緩存批量交換,減少交互次數(shù)
(3) 第二層:CentralCache(中心分配器)
設(shè)計理念:所有線程共享的中心分配器,負(fù)責(zé)向ThreadCache提供內(nèi)存。
class CentralCache {
private:
SpanList span_lists_[NFREELISTS]; // Span鏈表數(shù)組
std::mutex mutexes_[NFREELISTS]; // 桶鎖數(shù)組
public:
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t size);
void ReleaseListToSpans(void* start, size_t size);
};核心優(yōu)化點(diǎn):
- 桶鎖設(shè)計:每個大小類別獨(dú)立鎖,減少鎖競爭
- Span管理:每個Span管理一組連續(xù)頁面
- 批量分配:一次分配多個對象給ThreadCache
(4) 第三層:PageHeap(頁堆管理器)
設(shè)計理念:管理大塊內(nèi)存頁面,是系統(tǒng)內(nèi)存和應(yīng)用的橋梁。
class PageHeap {
private:
SpanList span_lists_[POWER_SLOTS]; // 只管理2的冪次頁數(shù)
PageMap2<PAGE_MAP_BITS> page_map_; // 頁面到Span映射,采用基數(shù)樹來管理
public:
Span* AllocateSpan(size_t n);
void ReleaseSpanToPageHeap(Span* span);
};核心優(yōu)化點(diǎn):
- 2的冪次優(yōu)化:只分配1,2,4,8,16,32,64,128,256頁的Span
- 智能分裂:大Span智能分裂成小Span
- 零開銷釋放:釋放直接緩存,無需合并操作
3. 核心數(shù)據(jù)結(jié)構(gòu)設(shè)計
(1) 自由鏈表(FreeList)
這是內(nèi)存池的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),將空閑內(nèi)存塊串成鏈表:
class FreeList {
private:
void* head_; // 鏈表頭指針
size_t size_; // 當(dāng)前大小
size_t max_size_; // 慢啟動最大批量大小
public:
void Push(void* obj);
void* Pop();
void PushRange(void* start, void* end, size_t n);
size_t PopRange(void*& start, void*& end, size_t n);
};巧妙設(shè)計:利用空閑內(nèi)存塊本身存儲鏈表指針,零額外開銷!
static inline void*& NextObj(void* obj) {
return *(void**)obj; // 前8字節(jié)存儲下一個塊的地址
}(2) Span結(jié)構(gòu)
Span是管理連續(xù)頁面的核心結(jié)構(gòu):
struct Span {
PageID page_id_; // 起始頁號
size_t n_; // 頁數(shù)
Span* next_; // 雙向鏈表指針
Span* prev_;
size_t object_size_; // 切分的對象大小
size_t use_count_; // 已分配對象數(shù)
void* free_list_; // 切分后的自由鏈表
bool is_used_; // 是否使用中
};4. 關(guān)鍵算法實(shí)現(xiàn)
(1) 大小類別映射算法
將任意大小映射到固定的大小類別,這是性能的關(guān)鍵:
static inline size_t RoundUp(size_t size) {
if (size <= 128) {
return Align(size, 8); // 8字節(jié)對齊
} elseif (size <= 1024) {
return Align(size, 16); // 16字節(jié)對齊
} elseif (size <= 8 * 1024) {
return Align(size, 128); // 128字節(jié)對齊
} elseif (size <= 64 * 1024) {
return Align(size, 1024); // 1KB對齊
} elseif (size <= 256 * 1024) {
return Align(size, 8 * 1024); // 8KB對齊
}
}設(shè)計考量:小對象精細(xì)對齊,大對象粗粒度對齊,平衡內(nèi)存利用率和性能。
(2) 慢啟動批量分配
動態(tài)調(diào)整批量大小,平衡內(nèi)存使用和性能:
static size_t NumMoveSize(size_t size) {
size_t base_batch;
if (size <= 32) {
base_batch = 128; // 小對象大批量
} else if (size <= 128) {
base_batch = 64;
} else if (size <= 512) {
base_batch = 32;
} else {
base_batch = 16; // 大對象小批量
}
return base_batch * batch_multiplier;
}(3) 頁面映射優(yōu)化
采用二層基數(shù)樹,快速查找對象所屬的Span:
template<int BITS>
class PageMap2 {
private:
staticconstint LEAF_BITS = BITS / 2;
staticconstint ROOT_BITS = BITS - LEAF_BITS;
struct OptimizedLeaf {
SubLeaf* sub_leafs[SUB_LEAFS_PER_LEAF];
// 延遲初始化,減少內(nèi)存開銷
};
public:
inline void* get(size_t k) const;
inline void set(size_t k, void* v);
};上面展示的是部分核心設(shè)計思路的簡化代碼,實(shí)際實(shí)現(xiàn)中還包含了更多的邊界處理和優(yōu)化細(xì)節(jié)。
PS:說實(shí)話,能參考TCMalloc架構(gòu)手搓高性能內(nèi)存池的人應(yīng)該不多。我在研究階段看了網(wǎng)上的幾個版本,發(fā)現(xiàn)大部分還是基于32位系統(tǒng)設(shè)計的,在如今的64位環(huán)境下就顯得有些局限了。可能是早期教學(xué)項(xiàng)目的代碼被反復(fù)借鑒,缺少針對現(xiàn)代系統(tǒng)的深度優(yōu)化。
注意: 我這個版本從頭開始針對64位系統(tǒng)設(shè)計,不僅支持完整的虛擬地址空間,還考慮了現(xiàn)代CPU架構(gòu)的特性, 至少在設(shè)計思路上更貼近實(shí)際應(yīng)用場景。
5. 性能優(yōu)化技巧
(1) 分支預(yù)測優(yōu)化
// 利用__builtin_expect優(yōu)化分支預(yù)測
if (__builtin_expect(!list.Empty(), 1)) {
return list.Pop(); // 大概率走這個分支
}(2) 內(nèi)聯(lián)函數(shù)優(yōu)化
// 熱點(diǎn)函數(shù)全部內(nèi)聯(lián)
static inline size_t GetPageID(void* addr) {
return reinterpret_cast<PageID>(addr) >> PAGE_SHIFT;
}(3) 緩存友好的數(shù)據(jù)結(jié)構(gòu)
// 64字節(jié)對齊,匹配CPU緩存行
struct SimpleBatch {
void* ptrs[32];
uint8_t count = 0;
} __attribute__((aligned(64)));(4) 鎖優(yōu)化策略
// 桶鎖:每個大小類別獨(dú)立鎖
std::mutex mutexes_[NFREELISTS];
// 減少持鎖時間
{
std::lock_guard<std::mutex> lock(mutexes_[index]);
// 最少的臨界區(qū)代碼
}(5) 基于perf的性能分析優(yōu)化
在內(nèi)存池開發(fā)過程中,perf是我最重要的性能分析工具。下面分享三個實(shí)際優(yōu)化案例:
① 案例1:發(fā)現(xiàn)熱點(diǎn)函數(shù)并優(yōu)化
問題發(fā)現(xiàn):使用perf分析發(fā)現(xiàn)SizeClass::Index()函數(shù)占用了15%的CPU時間
# 性能分析命令
sudo perf record -g ./test_memory_pool
sudo perf report
# 發(fā)現(xiàn)熱點(diǎn)
15.23% test_memory_pool [.] SizeClass::Index(unsigned long)
8.94% test_memory_pool [.] ThreadCache::Allocate(unsigned long)優(yōu)化方案:針對最常用的小對象做特殊優(yōu)化
// 優(yōu)化前:每次都走復(fù)雜的Index計算
size_t index = SizeClass::Index(align_size);
// 優(yōu)化后:小對象直接計算,避免函數(shù)調(diào)用
size_t index;
if (__builtin_expect(align_size <= 128, 1)) {
index = (align_size >> 3) - 1; // 直接位運(yùn)算
} else {
index = SizeClass::Index(align_size); // 復(fù)雜情況才調(diào)用
}效果驗(yàn)證:再次perf分析,該函數(shù)CPU占用降到3.2%,整體性能提升12%
② 案例2:優(yōu)化Deallocate的批量處理
問題發(fā)現(xiàn):Deallocate函數(shù)中頻繁的Push操作CPU耗時較高
12.45% test_memory_pool [.] FreeList::Push(void*)
7.33% test_memory_pool [.] ThreadCache::Deallocate(void*, unsigned long)優(yōu)化方案:針對小對象使用批量釋放策略
// 優(yōu)化前:每次都要操作鏈表
void ThreadCache::Deallocate(void* ptr, size_t size) {
size_t index = GetIndex(size);
free_lists_[index].Push(ptr); // 每次都要修改鏈表頭
}
// 優(yōu)化后:使用批量緩沖區(qū)
SimpleBatch batches_[32]; // 只為熱點(diǎn)大小創(chuàng)建批量
void ThreadCache::Deallocate(void* ptr, size_t size) {
if (index < 32) {
SimpleBatch& batch = batches_[index];
batch.ptrs[batch.count++] = ptr;
if (batch.count >= 32) {
FlushSimpleBatch(index, size); // 批量刷新到鏈表
}
}
}③ 案例3:解決大量缺頁中斷問題
問題發(fā)現(xiàn):程序出現(xiàn)大量缺頁處理,perf顯示__memset_avx2_erms耗時嚴(yán)重
33.67% test_memory_pool [.] __memset_avx2_erms
11.22% test_memory_pool [.] PageMap2::set_range優(yōu)化方案:優(yōu)化PageMap二層基數(shù)樹,減少memset調(diào)用
// 優(yōu)化前:每次都要初始化大塊內(nèi)存
class PageMap2 {
void* values[HUGE_SIZE]; // 直接分配巨大數(shù)組,導(dǎo)致大量memset
};
// 優(yōu)化后:延遲初始化,按需分配
class PageMap2 {
struct SubLeaf {
void* values[1024]; // 只有8KB,memset很快
bool initialized = false;
};
void ensure_initialized() {
if (!initialized) {
memset(values, 0, sizeof(values)); // 只清零8KB
initialized = true;
}
}
};效果:memset調(diào)用減少95%,在高并發(fā)場景下性能提升顯著。由此可見在高并發(fā)場景下不能夠大量調(diào)用memset。
實(shí)際在優(yōu)化過程中還遇到了很多類似的性能瓶頸,這里只是舉了幾個例子。perf工具幫助我們精確定位問題,避免了盲目優(yōu)化,每一次改進(jìn)都有數(shù)據(jù)支撐。
6. 實(shí)戰(zhàn)測試與性能分析
(1) 測試環(huán)境
- 系統(tǒng):ubuntu20.04
- 編譯器:G++ -O3 優(yōu)化
- 線程數(shù):16
- 每線程操作:10000次分配釋放
(2) 性能提升分析
- 小對象優(yōu)勢明顯:8B-128B對象提升2-5倍
- 中等對象表現(xiàn)優(yōu)異:256B-1KB對象提升5-6倍
- 大對象依然領(lǐng)先:2KB以上對象提升7倍以上
(3) 為什么這么快?
- 減少系統(tǒng)調(diào)用:批量分配減少90%+的系統(tǒng)調(diào)用
- 消除鎖競爭:線程本地緩存 + 桶鎖設(shè)計
- 內(nèi)存局部性:連續(xù)內(nèi)存分配,緩存友好
- 算法優(yōu)化:O(1)分配釋放,無遍歷查找
7. 使用方法
接口設(shè)計簡潔,可以直接替換malloc/free:
// 基礎(chǔ)接口
void* ptr = ConcurrentAlloc(1024);
ConcurrentFree(ptr);8. 項(xiàng)目亮點(diǎn)總結(jié)
- 三層架構(gòu)設(shè)計:清晰的架構(gòu)層次,職責(zé)分離
- 多種優(yōu)化技術(shù):從算法到實(shí)現(xiàn)的全方位優(yōu)化
- 生產(chǎn)級質(zhì)量:完整的錯誤處理和邊界檢查
- 高可擴(kuò)展性:支持自定義配置和擴(kuò)展
- 實(shí)測性能卓越:平均4.62倍性能提升
9. 一些思考和收獲
這個內(nèi)存池項(xiàng)目從構(gòu)思到完成,前后花了我一個月的業(yè)余時間。
最初只是想解決項(xiàng)目中的性能瓶頸,沒想到越深入越發(fā)現(xiàn)內(nèi)存管理的復(fù)雜性。從最簡單的鏈表管理,到三層架構(gòu)設(shè)計,再到各種微觀優(yōu)化,每一步都讓我對系統(tǒng)底層有了新的認(rèn)識。
印象最深的是那次perf分析,發(fā)現(xiàn)15%的時間竟然消耗在一個看似簡單的Index計算上。這讓我意識到,真正的性能優(yōu)化往往隱藏在最不起眼的地方。
還有那次為了解決PageMap初始化性能問題,我不得不重新設(shè)計了整個二級頁表結(jié)構(gòu)。原本簡單粗暴的大數(shù)組分配導(dǎo)致了嚴(yán)重的缺頁中斷,perf顯示__memset_avx2_erms占用了23%的CPU時間。雖然推翻了之前的設(shè)計,改用延遲初始化的SubLeaf結(jié)構(gòu),但看到最終memset調(diào)用減少95%的數(shù)據(jù)時,一切都值得了。
這個項(xiàng)目最大的價值不是代碼本身,而是整個優(yōu)化思維的建立。
現(xiàn)在我看任何性能問題,都會先想:這是架構(gòu)問題還是實(shí)現(xiàn)問題?是算法復(fù)雜度的問題還是常數(shù)優(yōu)化的空間?數(shù)據(jù)訪問模式是否緩存友好?鎖的粒度是否合理?























