初探C++內存池項目 ---(一)鏈式棧的實現和原理詳解

一.項目介紹

本項目是樓主在實驗樓中學習的,這裏主要分享一下學習心得和總結一些經驗~

在 C/C++ 中,內存管理是一個非常棘手的問題,我們在編寫一個程序的時候幾乎不可避免的要遇到內存的分配邏輯,這時候隨之而來的有這樣一些問題:是否有足夠的內存可供分配? 分配失敗了怎麼辦? 如何管理自身的內存使用情況? 等等一系列問題。在一個高可用的軟件中,如果我們僅僅單純的向操作系統去申請內存,當出現內存不足時就退出軟件,是明顯不合理的。正確的思路應該是在內存不足的時,考慮如何管理並優化自身已經使用的內存,這樣才能使得軟件變得更加可用。本次項目我們將實現一個內存池,並使用一個棧結構來測試我們的內存池提供的分配性能。

二.內存池介紹

內存池是池化技術中的一種形式。通常我們在編寫程序的時候回使用 new delete 這些關鍵字來向操作系統申請內存,而這樣造成的後果就是每次申請內存和釋放內存的時候,都需要和操作系統的系統調用打交道,從堆中分配所需的內存。如果這樣的操作太過頻繁,就會找成大量的內存碎片進而降低內存的分配性能,甚至出現內存分配失敗的情況。
而內存池就是爲了解決這個問題而產生的一種技術。從內存分配的概念上看,內存申請無非就是向內存分配方索要一個指針,當向操作系統申請內存時,操作系統需要進行復雜的內存管理調度之後,才能正確的分配出一個相應的指針。而這個分配的過程中,我們還面臨着分配失敗的風險。
所以,每一次進行內存分配,就會消耗一次分配內存的時間,設這個時間爲 T,那麼進行 n 次分配總共消耗的時間就是 nT;如果我們一開始就確定好我們可能需要多少內存,那麼在最初的時候就分配好這樣的一塊內存區域,當我們需要內存的時候,直接從這塊已經分配好的內存中使用即可,那麼總共需要的分配時間僅僅只有 T。當 n 越大時,節約的時間就越多。

三.項目源碼及分析

源碼地址:
https://github.com/82457097/Linux/tree/master/MemoryPool

1.鏈式棧的實現

#ifndef STACK_ALLOC_H
#define STACK_ALLOC_H

#include<memery>	//std::allocator函數需要的頭文件

template<typename T>
struct StackNode {		//創建模板化鏈表
	T data;
	StackNode* next;
};

template<typename T, typename Alloc = std::allocator<T>>
class StackAlloc {	//鏈式棧的模板類,主要實現棧的一些基本功能,push、pop、top、還有判空empty、銷燬clear等等~
public:
	//取別名,方便書寫。
	typedef StackNode Node;
	typedef typename Alloc::template rebind<Node>::other allocator;//這裏下面會做詳細解釋
	
	//構造函數,將鏈表的頭結點初始化一下就好了
	StackAlloc() { m_head = nullptr; }
	//析構函數,直接調用Clear()銷燬銷燬所有內存
	~StackAlloc() { Clear(); }
	//入棧函數Push()
	void Push(T element) {
		//先調用allocate和construct創建一個新的節點,建議先去看一下std::allocator的成員函數說明
		Node* curr = m_allocator.allocate(1);
		m_allocator.construct(curr, Node());
		//然後直接頭插法將新建的節點插入鏈表
		curr->data = element;
		curr->next = m_head;
		m_head = curr;
	}
	//彈棧Pop()
	T Pop() {
		//思路就是將節點的數據彈出,然後銷燬節點。
		T result = m_head->data;
		//創建一個臨時節點保存m_head->next
		Node* ptemp = m_head->next;
		//銷燬頭指針所指節點
		m_allocator.destroy(m_head);
		m_allocator.deallocate(m_head, 1);
		m_head = ptemp;

		return result;
	}
	//清空鏈式棧Clear()
	void Clear() {
		//循環判空銷燬節點,直至每個節點都被銷燬,內存被釋放
		Node* curr = m_head;
		//從頭結點開始判斷
		while(curr != nullptr) {
			//新建一個Node來存放curr->next
			Node* ptemp = curr->next;
			//不爲空則銷燬此節點
			m_allocator.destroy(curr);
			m_allocator.deallocate(curr, 1);
			curr = ptemp;
		}
		m_head = nullptr;
	}
	//判斷棧是否爲空的函數Empty()
	bool Empty() { return (m_head == nullptr); }
	//取棧頂元素Top()
	T Top() { return m_head->data; }
	
private:
	Node* m_head;
	allocator m_allocator;
};

#endif

2.main函數測試效果

#include<iostream>
#include<ctime>
#include<cassert>
#include"StackAlloc.h"

#define ELEMS 100000 //這裏是分配的個數 可以自己隨便改
#define REPS 100	//重複分配次數

using namespace std;

int main() {
	clock_t start;	//clock()不清楚的去看一下clock()怎麼用
	start = clock();
	StackAlloc<int, allocator<int>> stackDefault;
	for(int i = 0; i < REPS; ++i) {		//測試push和pop 100000x100 次所耗的時間
		assert(stackDefault.Empty());	//斷言stackDefault未分配前是空的
		for(int j = 0; j < ELEMS; ++j)
			stackDefault.Push(j);
		for(int j = 0; j < ELEMS; ++j)
			stackDefault.Pop(j);
	}
	cout << "Default Alloc Time: ";
	cout << (((double)clock() - start) / CLOCKS_PER_SEC) << endl;	//算出實際花費的時間

	return 0;
}

以上代碼完全是手打的,不確保沒有小錯誤,這裏只做思路的解釋,項目源碼連接已經放在上面了~

3.關於typedef typename Alloc::template rebind::other allocator用法的說明

//關於 typedef typename Alloc<T>::template rebind<Node>::other allocator 的解釋

//我們用 typedef 給一個東西取了別名叫 allocator
//這個東西是

	typename Alloc::template rebind<Node>::other 

//它其實是爲了解決編譯器不認識的代碼的問題而出現的寫法
//首先我們定義了 Alloc = std::allocator<T>,而 rebind 其實是 std::allocator 的一個成員。
//巧就巧在,rebind 本身又是另一個模板, C++ 稱其爲 dependent name。完整的形式本來應該是:

	std::allocator<T>::rebind<Node>::other

//但是模板的相關解析已經在 <T> 出現過了,後面的 <Node> 中的 < 只能被解釋爲小於符號,這會導致編譯出錯。
//爲了表示 dependent name 是一個模板,就必須使用 template 前綴。
//如果沒有 template 前綴,< 會被編譯器解釋爲小於符號。所以,我們必須寫成下面的形式:

	std::allocator<T>::template rebind<Node>::other

//最後,編譯器在其實根本沒有任何辦法來區分 other 究竟是一個類型,還是一個成員。
//但我們其實知道 other 是一個類型(見這裏),所以使用 typename 來明確指出這是一個類型,最終纔有了:

	typename std::allocator<T>::template rebind<Node>::other

//rebind的作用是,對於給定的類型T的分配器(typename Alloc = std::allocator<T>),
//想根據相同的策略得到另一個類型U的分配器(這裏得到了std::allocator<StackNode_<T>>)。
	template<typename U> 
	struct rebind {
		typedef allocator other; 
	}; 

四.小結

這一篇主要解釋了鏈式棧的作用及其原理,我們這裏所測試的是系統的內存分配函數std::allocator的性能,下一篇我們將會實現自己的MemoryPool,與其進行性能上的比較,小夥伴們可以先自己研究MemoryPool的實現,有什麼不懂得歡迎討論,有錯誤或者建議也歡迎指正!
先附上最終性能比較圖:次數都是100000x100
在這裏插入圖片描述
效果還是很明顯的~

發佈了34 篇原創文章 · 獲贊 14 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章