一.項目介紹
本項目是樓主在實驗樓中學習的,這裏主要分享一下學習心得和總結一些經驗~
在 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
效果還是很明顯的~