定長內存池的實現、測試及錯誤分析

背景

C/C++ 申請內存使用的是 mallocmalloc 其實就是一個大眾貨,什么場景下都可以用,但是什么場景下都可以用就意味著什么場景下都不會有很高的性能。

定長內存池解決固定大小的內存申請釋放需求, 性能達到極致,不考慮內存碎片等問題。

定長內存池成員設計

先分配一塊大內存,為了方便對這段空間進行操作,將指針定義成 char 類型,想移動指針只需要加減 n 即可。

申請內存時,會判斷大內存還有沒有空間。如果大內存有空間,但不能滿足需分配的小內存的需要,這時分配就會導致內存溢出。所以需要記錄大內存剩余字節數 _remain_size

if(_memory == nullptr) {//malloc 分配定長內存
}
T* obj = (T*)_memory;
_memory += sizeof(T);

這時申請內存就不會出現上述問題了。

//剩余內存不夠一個對象大小時,重新開內存
if (_remain_size < sizeof(T)) {_remain_size = 128 * 1024;_memory = (char*)malloc(_remain_size );if (_memory == nullptr) {throw std::bad_alloc(); }
}
obj = (T*)_memory;
_memory += objsize;
_remainSizeBytes -= objsize;

內存被申請了,如何還回來呢?

可以通過自由鏈表的方式管理內存塊,但并不需要設計鏈表,可以用內存塊的前 4 個字節(32位機器)或者前 8 個字節(64 位)作為一個指針指向下一個內存塊,再用一個頭指針 freelist 維護這個鏈表。

指針的類型決定了解引用后可訪問的字節大小。在 32 位系統上,一個指針通常是 4 個字節;在 64 位系統上,一個指針通常是 8 個字節。

*(void**)obj = _freelist;
_freelist = obj;

obj 指針強制轉換為 void** 類型,也就是指向指針的指針類型,然后對其解引用,實際上就是把 obj 所指向的內存位置當作一個指針變量來看待,并將當前自由鏈表的頭指針 _freelist 的值賦給它,并更新自由鏈表的頭指針。

所以,定長內存池需要三個成員

char* _memory = nullptr;  	// 指向大塊內存的指針,char* 有利于拓展
size_t _remain_size=0;  	// 內存塊后面剩余的字節大小
void* _free_list;       	// 自由鏈表管理還回來的內存

申請內存時也要優先使用自由鏈表中的內存。

//優先使用還回來的內存塊對象
if (_free_list) {void* next = *((void**)_free_list);obj = _free_list;_free_list = next;
}

但是分配內存時還存在著問題:

size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

分配內存大小要滿足上面代碼, 方便還回來的時候連接到一起。

定長設計

利用非類型模板參數,讓內存池每次申請的內存塊大小都是固定的

template <size_t N>
class ObjectPool {};

或者使用模板實現,將內存池設計為模板類,創建一個內存池需要給它賦一個類型,這樣每次申請的對象都是一個類型,做到了定長。

template <class T>
class ObjectPool {};

內存池如何申請對象

當內存池要申請對象時,優先使用前面釋放的內存,即在 _free_list 中維護的內存塊。

_free_list 為空,說明前面的空間沒有釋放,需要分配內存池后面的空間。

若后面的空間不夠申請一個對象,內存池就重新向 OS 申請一塊大內存,再申請對象。

不過這里定長內存池也是調用了 malloc 去申請大塊的空間,能不能直接調用系統申請空間的接口呢?

可以直接向堆申請內存空間,在 Linux 下,可以調用 brkmmap 函數。在 Windows 下用的是 VirtualAlloc 函數。

brk 函數

brk 函數通常用于在堆的末尾擴展或收縮內存。

原型

#include <unistd.h>
void *brk(void *addr);

用法

  • addr 參數指定新的數據段末尾的地址。
  • 如果調用成功,返回新的數據段末尾的地址;如果失敗,返回 (void *) -1 并設置 errno

示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main() {char *heap_start;// 獲取當前堆的末尾heap_start = (char *) sbrk(0);// 請求增加1024字節的堆空間if (brk(heap_start + 1024) == (void *) -1) {perror("brk");exit(EXIT_FAILURE);}// 在新申請的堆空間中寫入數據snprintf(heap_start, 1024, "Hello, heap!");printf("%s\n", heap_start);return 0;
}

優點

  • 相對簡單,適用于小塊內存的分配。

缺點

  • 不適合多線程環境,因為 brk 是全局的,會影響整個進程的堆。
  • 擴展堆空間時可能會導致內存碎片。
mmap 函數

mmap 函數用于在進程的地址空間中映射文件或匿名內存區域。它通常用于需要大塊內存或希望避免內存碎片的場景。

原型

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

用法

  • addr:建議映射的內存起始地址(通常傳 NULL 讓系統自動選擇)。
  • length:映射區域的長度。
  • prot:期望的內存保護標志(如 PROT_READ, PROT_WRITE)。
  • flags:控制映射對象的類型和行為(如 MAP_PRIVATE, MAP_ANONYMOUS)。
  • fd:文件描述符(對于匿名內存映射,傳 -1)。
  • offset:文件中的偏移量(對于匿名內存映射,傳 0)。

示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main() {size_t length = 1024;void *mapped_memory;// 匿名內存映射mapped_memory = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (mapped_memory == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}// 在映射的內存中寫入數據snprintf(mapped_memory, length, "Hello, mmap!");printf("%s\n", (char *)mapped_memory);// 釋放映射的內存if (munmap(mapped_memory, length) == -1) {perror("munmap");exit(EXIT_FAILURE);}return 0;
}

優點

  • 適用于大塊內存分配。
  • 映射的內存區域可以獨立管理,不容易受到其他內存分配的影響。
  • 可以更好地利用虛擬內存機制,提高內存管理的靈活性。

缺點

  • 相對復雜,需要更多的系統管理開銷。
  • 需要手動釋放內存(通過 munmap)。

選擇使用 brk 還是 mmap 取決于具體的應用場景和需求。對于小塊內存分配和簡單的內存管理,brk 可能更合適;而對于大塊內存分配和需要避免內存碎片的場景,mmap 通常更合適。

ObjectPool.h 的實現

#pragma once#include<iostream>#ifdef _WIN32#include<Windows.h> // Windows下的頭文件
#else#include <sys/mman.h> // Linux下的頭文件
#endif // _WIN32// 直接去堆上按頁申請空間
// 每頁大小 8KB(2^13),kpage 為頁數,總大小為 kpage << 13 字節
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32 // Windows下的系統調用接口void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等void* ptr = mmap(0, kpage<<13, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}// 定長內存池模板類
template<class T>
class ObjectPool
{
public:// 申請一個T類型大小的空間T* New() {T* obj = nullptr;// 優先從空閑鏈表中分配if (_free_list) { // 取出空閑鏈表頭部obj = (T*)_free_list;// 將 _free_list 更新為下一個空閑塊(原頭部存儲的地址)_free_list = *(void**)_free_list;}// 當前內存塊剩余空間不足,申請新的大塊內存else {// _memory中剩余空間小于T的大小的時候再開空間if (_remain_size < sizeof(T)) {_remain_size = 128 * 1024; // 128KB(16頁)//_memory = (char*)malloc(_remain_size);// 申請16頁_memory = (char*)SystemAlloc(_remain_size >> 13); if (_memory == nullptr) {throw std::bad_alloc();}}// 計算步長:確保至少按指針大小劃分,以兼容空閑鏈表指針操作size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);obj = (T*)_memory;       // 分配當前內存塊起始位置_memory += objsize;      // 移動內存塊指針_remain_size -= objsize; // 更新剩余空間}// 定位 new 顯式調用構造函數new(obj)T;return obj;}// 釋放對象內存,將其加入空閑鏈表void Delete(T* obj) {// 顯式調用析構函數obj->~T();// 將釋放的內存頭插到空閑鏈表*(void**)obj = _free_list; // 將 obj 的前 8 字節指向原鏈表頭_free_list = obj;          // 更新鏈表頭為當前釋放的塊}private:char* _memory = nullptr;     // 指向當前大塊內存的起始地址size_t _remain_size = 0;     // 當前大塊內存的剩余字節數void* _free_list = nullptr;  // 空閑鏈表頭指針(維護返還的小內存塊)
};

為什么 New() 中要用到定位 new ?

在最后返回申請到內存地址之前,使用了定位 new 的方法顯示調用了 T 類型的默認構造函數去初始化這塊對象空間。

new(obj)T;

這是有必要的,因為這塊空間不是我們直接定義對象得到的,而是通過一個 T* 類型的指針變量 obj 指向一塊內存得來的:

obj = (T*)_memory;

這種情況系統不會自動調用 T 類型的默認構造函數去初始化 obj 指向的這塊內存空間,所以我們就在函數內部調用定位 new 來完成對這塊內存空間初始化。

定位 new 的基本語法

定位 new 允許你在已經分配的內存中創建一個對象,而不再通過 new 來請求堆內存。它通常用于內存池管理、對象池等場景。

基本語法如下:

  • 調用默認構造函數(無初始化):

    new (place_address) type;
    

    這會在 place_address 指定的內存位置上調用 type 的默認構造函數,初始化對象。

  • 調用帶參數的構造函數(使用初始化列表):

    new (place_address) type(initializer-list);
    

    這會在 place_address 指定的內存位置上調用 type 的帶有初始化參數的構造函數。

定位 new 詳解
  • place_address:這是你已經分配好的內存地址,通常是通過 mallocoperator new 或其他方法分配的內存地址。注意,place_address 必須是足夠大的內存區域,足以存儲你要創建的對象。

  • type:這是你要在 place_address 處構造的對象的類型。

  • initializer-list:這是一個用于初始化對象的列表,它會傳遞給 type 的構造函數。如果沒有提供,默認構造函數會被調用。

默認構造函數
#include <iostream>class MyClass {
public:MyClass() {std::cout << "MyClass default constructor" << std::endl;}
};int main() {char buffer[sizeof(MyClass)]; // 預分配足夠空間// 在 buffer 中創建 MyClass 對象,調用默認構造函數new (buffer) MyClass;return 0;
}
帶參數的構造函數
#include <iostream>class MyClass {
public:MyClass(int a, int b) {std::cout << "MyClass constructor with arguments: " << a << ", " << b << std::endl;}
};int main() {char buffer[sizeof(MyClass)]; // 預分配足夠空間// 在 buffer 中創建 MyClass 對象,調用帶參數的構造函數new (buffer) MyClass(10, 20);return 0;
}
重要事項
  • 必須確保有足夠的內存place_address 指定的內存區域必須足夠大,能夠容納你要創建的對象。如果內存空間不足,可能會導致未定義行為(如內存越界)。

  • 顯式調用析構函數:當你使用定位 new 創建對象時,需要手動顯式調用對象的析構函數。如果你使用 new 分配內存并創建對象,C++ 會自動處理析構函數,但使用定位 new 后,你需要顯式銷毀對象。例如:

    obj->~MyClass();
    
  • 內存管理:定位 new 并不負責內存的管理。它只是告訴編譯器在指定內存位置上構造對象,因此你需要自己負責內存的分配和回收。

在對象池中的應用示例

在對象池中,定位 new 可以幫助你在已分配的內存池中構建對象,而不是每次都調用 new 來分配新的內存空間。這樣可以有效避免頻繁的內存分配和釋放。

template <typename T>
class ObjectPool {
public:T* allocate() {// 假設我們有一個已分配的足夠大的內存區域char buffer[sizeof(T)];// 在預分配的內存中創建對象T* obj = new (buffer) T();return obj;}void deallocate(T* obj) {// 銷毀對象,調用析構函數obj->~T();}
};

TestObjectPool.cpp 的實現

#include "ObjectPool.h"
#include <vector>// 樹節點結構體定義
struct TreeNode {int _val;         // 節點存儲的值TreeNode* _left;  // 左子節點指針TreeNode* _right; // 右子節點指針// 構造函數:初始化值為0,子節點指針為空TreeNode() : _val(0), _left(nullptr), _right(nullptr) {}
};// 內存池性能測試函數
void TestObjectPool() {// 測試參數配置const size_t Rounds = 10;   // 測試輪次(申請釋放循環次數)const size_t N = 10000000;   // 每輪次操作次數/****************** 傳統new/delete測試 ******************/size_t begin1 = clock();  // 記錄測試開始時間std::vector<TreeNode*> v1; // 使用vector管理節點指針v1.reserve(N);             // 預分配內存避免擴容影響測試for (size_t j = 0; j < Rounds; ++j) {// 批量申請節點for (int i = 0; i < N; ++i) {v1.push_back(new TreeNode); // 每次new一個TreeNode}// 批量釋放節點for (int i = 0; i < N; ++i) {delete v1[i]; // 逐個delete釋放內存}v1.clear(); // 清空vector準備下一輪測試}size_t end1 = clock();  // 記錄測試結束時間/****************** 內存池性能測試 ******************/ObjectPool<TreeNode> TNPool; // 創建TreeNode內存池實例size_t begin2 = clock();  // 記錄內存池測試開始時間std::vector<TreeNode*> v2;v2.reserve(N);            // 預分配內存for (size_t j = 0; j < Rounds; ++j) {// 使用內存池申請節點for (int i = 0; i < N; ++i) {v2.push_back(TNPool.New()); // 從內存池獲取節點}// 使用內存池釋放節點for (int i = 0; i < N; ++i) {TNPool.Delete(v2[i]); // 將節點歸還內存池}v2.clear(); // 清空vector}size_t end2 = clock();  // 記錄內存池測試結束時間/****************** 結果輸出 ******************/std::cout << "傳統new/delete耗時: " << (end1 - begin1) / CLOCKS_PER_SEC << "s" << std::endl;std::cout << "定長內存池實現耗時:  " << (end2 - begin2) / CLOCKS_PER_SEC << "s" << std::endl;
}int main() {TestObjectPool();  // 執行性能測試return 0;
}

測試代碼易錯點

std::vector<TreeNode*> v2.reserve(N)std::vector<TreeNode*> v2(N) 的關鍵區別在于容器初始化方式內存行為,這是導致段錯誤的根本原因。

行為v2.reserve(N)v2(N)
初始化元素數量0N
內存分配策略預分配,避免擴容精確分配,無預留空間
指針安全性安全(地址有效)危險(擴容后地址失效)
適用場景已知最大元素數量的高效插入需要初始值的場景

結論:在需要動態插入元素且已知最大數量的場景中,始終優先使用 reserve() + push_back,避免混合使用初始化大小和動態插入。

推薦一下

https://github.com/0voice

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

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

相關文章

vue3 下載文件 responseType-blob 或者 a標簽

在 Vue 3 中&#xff0c;你可以使用 axios 或 fetch 來下載文件&#xff0c;并將 responseType 設置為 blob 以處理二進制數據。以下是一個使用 axios 的示例&#xff1a; 使用 axios 下載文件 首先&#xff0c;確保你已經安裝了 axios&#xff1a; npm install axios然后在你…

Search API:讓數據獲取變得簡單高效的搜索引擎代理商

Search API&#xff1a;讓數據獲取變得簡單高效的搜索引擎代理商 在當今數字化時代&#xff0c;數據驅動的決策變得越來越重要&#xff0c;而獲取精準、實時的數據是眾多企業、研究機構和開發者的核心需求。然而&#xff0c;直接爬取搜索引擎或行業資訊網站可能會遇到諸多挑戰&…

halcon三維對象處理例程總結(二)

目錄 一、intersect_plane_object_model_3d二、interactive_intersection三、measure_plant四、moments_object_model_3d五、projective_trans_object_model_3d六、read_object_model_3d_generic_ascii一、intersect_plane_object_model_3d 計算三維物體模型與平面之間的相交部…

基于 Python 的項目管理系統開發

基于 Python 的項目管理系統開發 一、引言 在當今快節奏的工作環境中&#xff0c;有效的項目管理對于項目的成功至關重要。借助信息技術手段開發項目管理系統&#xff0c;能夠顯著提升項目管理的效率和質量。Python 作為一種功能強大、易于學習且具有豐富庫支持的編程語言&…

2月24(信息差)

&#x1f30d;“任意舞蹈任意學”&#xff01;宇樹機器人又進化了 傳Meta有意合作拋出橄欖枝 &#x1f384;兩部門&#xff1a;深入推進公路沿線充電基礎設施建設 推動大功率充電技術標準應用 ?小米15 Ultra、小米SU7 Ultra定檔2月27日 雷軍宣布&#xff1a;向超高端進發 1.…

mysql 遷移到人大金倉數據庫

我是在windows上安裝了客戶端工具 運行數據庫遷移工具 打開 在瀏覽器輸入http://localhost:54523/ 賬號密碼都是kingbase 添加mysql源數據庫連接 添加人大金倉目標數據庫 添加好的兩個數據庫連接 新建遷移任務 選擇數據庫 全選 遷移中 如果整體遷移不過去可以單個單個或者幾個…

C++和OpenGL實現3D游戲編程【連載23】——幾何著色器和法線可視化

歡迎來到zhooyu的C++和OpenGL游戲專欄,專欄連載的所有精彩內容目錄詳見下邊鏈接: ??C++和OpenGL實現3D游戲編程【總覽】 1、本節實現的內容 上一節課,我們在Blend軟件中導出經緯球模型時,遇到了經緯球法線導致我們在游戲中模型光照顯示問題,我們在Blender軟件中可以通過…

JUC并發—12.ThreadLocal源碼分析

大綱 1.ThreadLocal的特點介紹 2.ThreadLocal的使用案例 3.ThreadLocal的內部結構 4.ThreadLocal的核心方法源碼 5.ThreadLocalMap的核心方法源碼 6.ThreadLocalMap的原理總結 1.ThreadLocal的特點介紹 (1)ThreadLocal的注釋說明 (2)ThreadLocal的常用方法 (3)ThreadL…

Deepseek和Grok 3對比:寫一段冒泡排序

1、這是訪問Grok 3得到的結果 2、grok3輸出的完整代碼&#xff1a; def bubble_sort(arr):n len(arr) # 獲取數組長度# 外層循環控制排序輪數for i in range(n):# 內層循環比較相鄰元素&#xff0c;j的范圍逐漸減少for j in range(0, n - i - 1):# 如果當前元素大于下一個元…

Java-01-源碼篇-04集合-05-ConcurrentHashMap(1)

1.1 加載因子 加載因子&#xff08;Load Factor&#xff09;是用來決定什么時候需要擴容的一個參數。具體來說&#xff0c;加載因子 當前元素數量 / 桶的數量&#xff0c;當某個桶的元素個數超過了 桶的數量 加載因子 時&#xff0c;就會觸發擴容。 我們都知道 ConcurrentHas…

vue3: directive自定義指令防止重復點擊

第一章 前言 相信很多小伙伴會在各個渠道上搜如何防止重復點擊&#xff0c;之后會推薦什么防抖、節流來避免這一操作&#xff0c;該方法小編就不繼續往下說了。接下來說說小編的場景&#xff0c;項目已經完成的差不多了&#xff0c;但是由于之前大家都是直接點擊事件調用方法的…

忽略Git文件的修改,讓它不被提交

使用Git托管的工程中&#xff0c;經常有這樣的需求&#xff0c;希望文件只是本地修改&#xff0c;不提交到服務端。 如果僅僅是本地存在的文件&#xff0c;我們可以通過.gitignore配置避免文件被提交。 有的時候文件是由git托管的&#xff0c;但是我們希望只在本地修改&#…

Zap:Go 的高性能日志庫

文章目錄 Zap&#xff1a;Go 高性能日志庫一、Zap 的核心優勢二、快速入門 Zap1. 安裝2. 基本用法輸出示例 三、Logger 與 SugaredLogger&#xff1a;如何選擇&#xff1f;1. **Logger&#xff08;高性能模式&#xff09;**2. **SugaredLogger&#xff08;開發友好模式&#xf…

每日一題——順時針旋轉矩陣

順時針旋轉矩陣 目錄 一、問題描述二、解題思路 1. 原地旋轉矩陣2. 旋轉邏輯3. 代碼實現 三、代碼解析 1. 參數說明2. 原地旋轉邏輯3. 返回矩陣 四、示例測試代碼五、復雜度分析 1. 時間復雜度2. 空間復雜度 一、問題描述 以下是內容轉換為 CSDN 的 Markdown 格式&#xf…

接雨水的算法

題目 代碼 # 接雨水算法 def trap(height):# 1. 特殊情況&#xff1a;數組為空 則返回0if not height:return 0n len(height)# 2. 初始化左右指針&#xff0c;左右最大值&#xff0c;結果left, right 0, n - 1# maxleft代表左邊最大值&#xff0c;maxright代表右邊最大值max…

會話對象 HttpSession 二、HttpSession失效

session失效有如下幾個原因&#xff1a; session.invalidate()方法注銷sessionsession超時 <session-config><!-- session的超時時間&#xff0c;以分鐘為單位 --><session-timeout>1</session-timeout> </session-config>Cookie被禁用

Jenkins 創建 Node 到 Windows

Jenkins 創建 Node 到 Windows 一. 新建 Node Dashboard -> Manage Jenkins -> Manage Nodes and Clouds Dashboard -> Nodes -> New Node 二. 配置節點 Node&#xff1a;節點名 Description&#xff1a;節點描述 Number of executors&#xff1a;節點最大同…

Opengl常用緩沖對象功能介紹及使用示例(C++實現)

本文整理了常用的opengl緩沖區對象并安排了使用示例 名稱英文全稱作用簡述頂點數組對象Vertex Array Object (VAO)管理 VBO 和 EBO 的配置&#xff0c;存儲頂點屬性設置&#xff0c;簡化渲染流程&#xff0c;避免重復設置狀態頂點緩沖區對象Vertex Buffer Object (VBO)存儲頂點…

矩陣加減乘除的意義與應用

矩陣加法 數學意義 線性空間的封閉性線性變換的疊加矩陣分解與表示 實際應用 數據聚合與統計圖像處理與計算機視覺物理學與工程學動態系統與優化經濟學與運籌學信號處理與通信游戲開發與計算機圖形學環境科學與地理信息矩陣加法的關鍵特點 矩陣減法 數學意義線性空間封閉性 線…

【Redis原理】底層數據結構 五種數據類型

文章目錄 動態字符串SDS(simple dynamic string )SDS結構定義SDS動態擴容 IntSetIntSet 結構定義IntSet的升級 DictDict結構定義Dict的擴容Dict的收縮Dict 的rehash ZipListZipListEntryencoding 編碼字符串整數 ZipList的連鎖更新問題 QuickListQuickList源碼 SkipListRedisOb…