
前言內存池memory pool要解決的問題很具體通用分配器general-purpose allocator也就是malloc/free或operator new/operator delete為了應付任意大小、任意生命周期的請求 內部必須維護元數(shù)據(jù)、分箱bin、合并空閑塊coalescing并且為了線程安全通常要加鎖。 如果你的程序是反復申請、釋放大量同尺寸、短生命周期的小對象這些都是純開銷。一個常見誤解是內存池就是自己寫個operator new。不是。重載operator new只是換了入口 真正決定性能的是池的分配策略塊大小如何取整、空閑塊如何組織、何時向系統(tǒng)批量取內存。 另一個誤解是內存池一定更快——單線程下它確實通常更快但多線程共享同一個池時 一把互斥量帶來的競爭可能比malloc的線程緩存還糟。本文分三層講先講定長池fixed-size pool的核心結構侵入式空閑鏈表 再給出一個完整可編譯的實現(xiàn)最后介紹 C17 標準庫里已經(jīng)提供的池設施std::pmr。一、核心思想侵入式空閑鏈表定長池只需要三樣東西一整塊從系統(tǒng)申請來的連續(xù)內存按固定塊大小切成 N 份一條空閑塊鏈表把所有未分配的塊串起來一個下一個空閑塊的頭指針。關鍵技巧是侵入式intrusivenext指針直接存放在空閑塊自身的字節(jié)里 而不是額外開一個數(shù)組。塊被分配出去時那塊內存里的next就被用戶的對象覆蓋 塊被歸還時再在原地重建next。這樣每個塊的開銷是零——沒有每塊的元數(shù)據(jù) 也沒有額外的鏈表節(jié)點。這套結構決定了池的復雜度都是 O(1)分配是從鏈表頭摘一個釋放是往鏈表頭插一個 沒有任何查找或合并。代價是塊大小固定只能服務一種尺寸或一個尺寸檔位的請求塊大小的下限是sizeof(void*)否則裝不下next指針池本身不保證線程安全多線程共享時必須自己加鎖。對比項operator new/delete定長內存池單次分配復雜度實現(xiàn)相關需要在空閑鏈/分箱里查找O(1) 摘頭每塊元數(shù)據(jù)有大小、邊界標記等實現(xiàn)定義無合并空閑塊支持不需要塊等大內存碎片會產生外部碎片池內的塊等大無外部碎片線程安全標準要求可多線程使用默認不安全需自行加鎖歸還給系統(tǒng)的時機視實現(xiàn)而定通常等池析構/release()二、對齊與塊尺寸內存池最容易寫錯的地方是對齊。placement newnew (p) T(...)要求p滿足alignof(T) 如果塊大小取成 9 字節(jié)這種不是對齊數(shù)倍數(shù)的值第 2 塊開始的地址就不再是 8 的倍數(shù) 在上面構造double或指針成員就是未定義行為UB。正確的做法是把塊大小向上取整到alignof(std::max_align_t)的倍數(shù) std::max_align_t在cstddef中。alignof的結果一定是 2 的冪 所以可以用位運算做取整constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n align - 1) ~(align - 1); }C17 的::operator new(size)返回的內存至少滿足alignof(std::max_align_t) 所以按這個對齊取整池內任意塊都可以安全地承載基本對齊的類型。 如果類型是過度對齊over-aligned的比如帶alignas(64) 就需要 C17 引入的對齊版本::operator new(size, std::align_val_t(align))和對應的::operator delete(ptr, std::align_val_t(align))都在new里。三、完整實現(xiàn)下面是一個可直接編譯的定長池以及疊在它上面的類型安全包裝。// 適用C17GCC 13 / Clang 17 / MSVC 19.3x 均可編譯 // 編譯g -stdc17 -Wall -Wextra -pedantic pool.cpp -o pool #include cstddef #include iostream #include new #include utility namespace { constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n align - 1) ~(align - 1); } // 空閑塊就地存放的結構一個指針指向下一個空閑塊 struct FreeNode { FreeNode* next; }; } // namespace class FixedPool { public: FixedPool(std::size_t block_size, std::size_t block_count) : block_size_(align_up(block_size sizeof(FreeNode) ? sizeof(FreeNode) : block_size, alignof(std::max_align_t))), block_count_(block_count), free_head_(nullptr), raw_(nullptr) { if (block_count_ 0) { return; } // 一次性向系統(tǒng)要一整塊內存之后不再和分配器打交道 raw_ static_castunsigned char*(::operator new(block_size_ * block_count_)); for (std::size_t i 0; i block_count_; i) { void* block raw_ i * block_size_; ::new (block) FreeNode{free_head_}; // 在空閑塊上建立 FreeNode 對象 free_head_ static_castFreeNode*(block); } } ~FixedPool() { ::operator delete(raw_); } // 傳 nullptr 是合法的空操作 FixedPool(const FixedPool) delete; FixedPool operator(const FixedPool) delete; // 池耗盡時返回 nullptr而不是拋異常 void* allocate() noexcept { if (free_head_ nullptr) { return nullptr; } FreeNode* node free_head_; free_head_ node-next; return node; } void deallocate(void* p) noexcept { if (p nullptr) { return; } ::new (p) FreeNode{free_head_}; // 重新建立 FreeNode結束被歸還對象的生命周期 free_head_ static_castFreeNode*(p); } std::size_t block_size() const noexcept { return block_size_; } std::size_t block_count() const noexcept { return block_count_; } std::size_t available() const noexcept { // O(n)僅供調試/統(tǒng)計 std::size_t n 0; for (const FreeNode* p free_head_; p ! nullptr; p p-next) { n; } return n; } private: std::size_t block_size_; // 已按 max_align_t 取整 std::size_t block_count_; FreeNode* free_head_; // 空閑鏈表頭 unsigned char* raw_; // 整塊原始內存 }; // 類型安全包裝負責構造與析構FixedPool 只負責字節(jié) template class T class ObjectPool { public: static_assert(alignof(T) alignof(std::max_align_t), 本示例只支持基本對齊的類型過度對齊類型請改用 C17 的 aligned operator new); explicit ObjectPool(std::size_t count) : pool_(sizeof(T), count) {} template class... Args T* create(Args... args) { void* p pool_.allocate(); if (p nullptr) { return nullptr; } try { return new (p) T(std::forwardArgs(args)...); } catch (...) { pool_.deallocate(p); // 構造函數(shù)拋異常時把塊還回去否則這塊內存永久泄露 throw; } } void destroy(T* obj) noexcept { if (obj nullptr) { return; } obj-~T(); // 顯式調用析構函數(shù) pool_.deallocate(static_castvoid*(obj)); } std::size_t available() const noexcept { return pool_.available(); } private: FixedPool pool_; }; struct Vec2 { double x; double y; Vec2(double x_, double y_) noexcept : x(x_), y(y_) {} }; int main() { ObjectPoolVec2 pool(4); std::cout 塊大小 sizeof(Vec2) 初始可用 pool.available() \n; Vec2* a pool.create(1.0, 2.0); Vec2* b pool.create(3.0, 4.0); if (a ! nullptr b ! nullptr) { std::cout a-x b-y \n; // 1.0 4.0 } pool.destroy(a); pool.destroy(b); std::cout 釋放后可用 pool.available() \n; return 0; }逐行核對幾個點::new (block) FreeNode{free_head_}是布局 newplacement new它不分配內存只在給定地址上構造對象FreeNode是聚合類型用花括號初始化列表。block_size_ sizeof(FreeNode) ? sizeof(FreeNode) : block_size保證塊至少裝得下一個指針。四個成員按block_size_、block_count_、free_head_、raw_的順序聲明構造函數(shù)的初始化列表順序與之一致避免-Wreorder警告成員初始化順序永遠按聲明順序不按列表順序。ObjectPool::create的try/catch(...)里throw;是重新拋出當前異常類型不變。四、標準庫里已有的池std::pmrC17 在memory_resource里提供了現(xiàn)成的池設施不必都自己寫std::pmr::memory_resource抽象基類公開allocate/deallocate/is_equal子類實現(xiàn)do_allocate/do_deallocate/do_is_equal。std::pmr::unsynchronized_pool_resource單線程用無鎖內部按尺寸檔位管理多個小塊池。std::pmr::synchronized_pool_resource多線程用需要同步開銷。std::pmr::pool_options可選項有max_blocks_per_chunk和largest_required_pool_block兩個std::size_t成員具體取值由實現(xiàn)決定。// 需要 C17 及以上 #include cstddef #include memory_resource #include vector int main() { std::pmr::unsynchronized_pool_resource pool; // 單線程別跨線程用 std::pmr::polymorphic_allocatorint alloc{pool}; std::pmr::vectorint v{alloc}; for (int i 0; i 1000; i) { v.push_back(i); // 擴容時的舊緩沖會歸還給 pool由 pool 復用 } pool.release(); // 一次性把所有內存還給上游upstream return 0; }libstdc 里還有一個歷史遺留的擴展__gnu_cxx::__pool_alloc頭文件ext/pool_allocator.h 那是 SGI STL 時代的池式分配器標準庫的std::allocator現(xiàn)在只是operator new的薄包裝 本身不做池化——這一點常被搞混。常見坑點塊大小沒有對齊取整? 直接按用戶給的sizeof(T)切塊T 為 9 字節(jié)時第 2 塊起地址不再對齊 在上面 placement new 是 UB。? 用align_up(size, alignof(std::max_align_t))把所有塊對齊到max_align_t。只歸還內存不調用析構函數(shù)?pool.deallocate(p);直接丟掉一個持有std::string成員的對象它的堆內存永遠不會釋放。?p-~T(); pool.deallocate(p);—— 先顯式析構再還內存順序不能反。同一塊內存釋放兩次? 歸還兩次會把同一個塊兩次插入空閑鏈表鏈表形成環(huán)下一次allocate可能拿到重復地址。? 建立所有權約定destroy之后立刻把指針置空接口文檔寫清不得重復釋放。 此外給池類顯式 delete拷貝構造與拷貝賦值能從類型層面杜絕 兩個池對象管同一塊內存引發(fā)的雙重釋放。塊大小小于sizeof(void*)? 用戶申請 4 字節(jié)塊并直接寫FreeNode越界覆蓋相鄰塊的next字段。? 用sizeof(FreeNode)做下限并在文檔里說明塊實際會被放大。構造拋異常時內存沒歸還?void* p pool.allocate(); return new (p) T(args...);—— T 的構造拋了異常p就永久丟失池越用越少。? 用try/catch(...)捕獲pool.deallocate(p)后重新throw。把池當成線程安全的? 多個線程直接共享一個FixedPoolfree_head_的讀改寫沒有任何同步—— 這是數(shù)據(jù)競爭標準不保證任何行為實際表現(xiàn)是鏈表斷裂、塊丟失或崩潰。? 單線程用不加鎖的池每個線程一個thread_local池必須共享時加std::mutex 或直接用std::pmr::synchronized_pool_resource。池對象先銷毀對象指針還在用?FixedPool析構后仍在訪問之前allocate出來的塊——那些內存已經(jīng)還給系統(tǒng) 訪問是 UB。? 池的生存期必須覆蓋所有從它分配的對象把它做成成員變量聲明在所有使用者之前。過度對齊類型直接塞進池? 對struct alignas(64) CacheLine { ... };用ObjectPool 即使塊大小取整到 64 的倍數(shù)::operator new(size)也不保證 64 字節(jié)對齊。? 改用 C17 的::operator new(size, std::align_val_t(64)) 并在對應的::operator delete(ptr, std::align_val_t(64))里成對使用??偨Y關鍵點結論空閑鏈表用侵入式鏈表next直接存在空閑塊里零額外開銷塊大小下限sizeof(void*)并向上取整到alignof(std::max_align_t)的倍數(shù)分配/釋放都是 O(1)無查找、無合并對象生命周期placement new 構造顯式p-~T()析構異常時要歸還內存線程安全池本身不安全共享必須加鎖或用std::pmr::synchronized_pool_resource現(xiàn)成設施C17 的std::pmr已提供池除非有特殊需求不必手寫內存池的本質是用塊尺寸固定這個約束換掉通用分配器的查找、合并和鎖開銷。 只要你的場景真的是同尺寸小對象高頻分配這筆交易就劃算一旦對象尺寸五花八門 就該退回通用分配器或改用std::pmr的分檔池。