内存池
一.什么是内存池
对于C/C++使用者,在进行内存分配时,可能会频繁调用 new / delete / free / malloc 等标准库内存分配函数,这些函数会造成大量不必要的内存开销。
当我们频繁地动态申请和释放内存时,使用这些系统调用而造成性能损耗的在一定程度上是可观的。
二.内存池的思想
内存池首先申请一块大内存,并将内存分为较小的部分。
每次请求时都会返回其中一个小块(放在应用层上执行,减少系统调用的开销)。
三.内存池的优缺点
优点:
(1) 快于malloc new等系统调用
(2) 由于每个对象的大小都是事先已知的(即无需存储分配元数据(metadata:可以包括:内存块的大小,是否已经被分配或释放,边界地址,用于分配管理![]()的数据结构等))
(3) 几乎没有内存碎片(外部碎片:零散分配而无法满足连续大块的需求(主要在堆内存中);内部碎片:分配的内存块中有一部分浪费(与内存的对齐方式有关))
(4) 无需逐个释放对象。分配器将在调用析构函数后将释放所分配的内存(仅在对象有默认析构函数时才有效)
缺点:
(1) 对象具有固定的大小且必须事先知道(通常不是问题)
(2) 对特定的应用程序需要进行微调(通过使用模板类可以解决)
四.先导知识
4.1 allocator / allocator_traits
"Allocators are classes that define memory models to be used by some parts of the Standard Library, and most specifically, by STL containers."
上述allcator定义来自于cpluscplus reference中的定义,说明allocator是一种定义内存模型的类,用于STL容器。
"This template supplies a uniform interface for allocator types."
而对于 allocator_traits上述语句说明其是一种为allocator type提供统一接口的模板。
4.2 new / operator new / placement new / delete / operator new
(1) new:可分为以下几步:
1) 调用 operator new 分配一块内存(如果分配失败,会抛出bad_alloc异常)
2) 调用对象构造函数进行构造
(2) delete: 可分为以下几步:
1) 调用 operator delete 释放内存
2) 自动调用析构函数进行清理
(3) operator new: 内存分配函数(形如:void* operator new(size_t size, std::nothrow_t&);):operator new -> malloc -> sbrk/brk/mmap
(4) operator delete: 内存释放函数(形如:void* operator delete(void* ptr);):operator delete -> free -> sbrk/brk/mmap
(5) placement new: operator new的重载版本,允许在指定的内存地址上构造对象(已分配内存,形如:void* operator new(size_t size, void* ptr) noexcept;)
五.关键成员变量
5.1 内存单元(slot_)
1 union slot_
2 {
3 value_type element; // 用于存储实际对象
4 slot_* next; // 用于指向下一个空闲 slot
5 };
为什么使用 union :
1)union 的所有成员共享同一块内存。
2)在内存池中,一个 slot 在某个时刻要么存储分配的对象(element
),要么是空闲链表的一部分(next
),因此可以使用 union
节省内存开销。
5.2 空闲(内存单元)链表(free_slots_)
链表数据结构;当需要分配内存时,优先从 free_slots_中获取
1 if (freeSlots_) {
2 Slot* result = freeSlots_;
3 freeSlots_ = freeSlots_->next;
4 return reinterpret_cast<pointer>(result);
5 }
5.3 当前分配的内存块 (current_block_)
指向当前分配的内存块的起始位置,每次调用 allocate_block() 分配新内存块,该指针指向新的内存起始块
1 Slot* newBlock = reinterpret_cast<Slot*>(::operator new(blockSize));
2 newBlock->next = currentBlock_;
3 currentBlock_ = newBlock;
六.关键成员函数
6.1 allocate
- 设计思路:
- 优先从空闲链表(
freeSlots_
)中分配。 - 如果空闲链表为空且当前内存块已满,则分配新的内存块。
- 优先从空闲链表(
- 实现细节
-
if (freeSlots_) { Slot* result = freeSlots_; freeSlots_ = freeSlots_->next; return reinterpret_cast<pointer>(result); } if (!currentBlock_ || currentSlot_ >= lastSlot_) { allocateBlock(); } return reinterpret_cast<pointer>(currentSlot_++);
-
6.2 deallocate
- 设计思路:
- 通过
Slot
的next
指针将释放的块链接到空闲链表中。
- 通过
- 实现细节:
Slot* slot = reinterpret_cast<Slot*>(p); slot->next = freeSlots_; freeSlots_ = slot;
6.3 construct
- 作用:
- 在分配的内存上构造对象。
- 实现细节:
new (p) U(std::forward<Args>(args)...);
6.4 destroy
- 作用:
- 调用对象的析构函数。
- 实现细节:
p->~U();
6.5 allocate_block
- 设计思路:
- 使用
operator new
分配新内存块。 - 将内存块划分为多个 slot,并链接成空闲链表。
- 使用
- 实现细节
-
size_type blockSize = alignUp(BlockSize, alignof(Slot)); Slot* newBlock = reinterpret_cast<Slot*>(::operator new(blockSize)); newBlock->next = currentBlock_; currentBlock_ = newBlock; char* body = reinterpret_cast<char*>(newBlock) + sizeof(Slot); size_type bodyPadding = alignUp(reinterpret_cast<size_type>(body), alignof(Slot)) - reinterpret_cast<size_type>(body); freeSlots_ = reinterpret_cast<Slot*>(body + bodyPadding); Slot* lastSlot = reinterpret_cast<Slot*>(reinterpret_cast<char*>(newBlock) + blockSize - sizeof(Slot)); for (Slot* slot = freeSlots_; slot < lastSlot; ++slot) { slot->next = slot + 1; } lastSlot->next = nullptr;
-
七.在不同编译环境下的优化情况
7.1 Windows(MSVC, visual studio 2022)
1 #include"MemoryPool.h"
2 #include<memory>
3 #include<ctime>
4 #include<iostream>
5
6 using namespace my_memory_pool;
7 const int ELEMS1 = 500;
8 const int ELEMS2 = 100000;
9 int main()
10 {
11 clock_t start;
12
13 memory_pool<size_t> pool;
14 start = clock();
15 for (int i = 0; i < ELEMS1; ++i)
16 {
17 for (int j = 0; j < ELEMS2; ++j)
18 {
19 // 创建元素
20 size_t* x = pool.allocate();
21
22 // 释放元素
23 pool.deallocate(x);
24 }
25 }
26 std::cout << "my_memory_pool 内存池耗时: ";
27 std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";
28
29
30 start = clock();
31 for (int i = 0; i < ELEMS1; ++i)
32 {
33 for (int j = 0; j < ELEMS2; ++j)
34 {
35 size_t* x = new size_t;
36
37 delete x;
38 }
39 }
40 std::cout << "new/delete动态内存分配耗时: ";
41 std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";
42
43 return 0;
44 }
7.2 Ubuntu 22.04.5(g++ 11.4.0)
使用命令 g++ main.cpp -o main
使用 -O2 或 -O3时,new / delete效果更好
-O2:
-O3:
参考:cacay/MemoryPool: An easy to use and efficient memory pool allocator written in C++.