引言
内存池通过预分配和高效管理内存,显著提高了内存分配的性能和效率,非常适合需要频繁分配和释放内存的应用场景,也是大型项目中较为常见的组件。
我们为什么要使用内存池?
当一个程序运行时间较长时,频繁的 new / delete 具有不确定性,操作系统管理的连续内存被分为很多的非连续碎片,内存碎片大量的产生。这时,我们想要得到一块较为大的连续内存,就会有些困难,久而久之,程序就会因为无法满足大内存的需求崩溃。
我们使用内存池就是为了避免这种情况的发生,我们提前分配一块较大的内存,自己进行管理,让我们的程序能够长久的运行下去。
实现原理
内存池的实现难点在于:
- 如何划分内存
- 记录已使用和未使用的内存
- 搜寻合适大小的内存
关于这些问题,有许多专业的论文提出了许多种算法。但是我在这里不会讲,我讲一讲我是如何实现一个简单的高效率内存池的。
划分内存
首先我们要明白我们的需求:为程序稳定的分配大内存。
所以不考虑小内存的分配,因为操作系统做的已经足够好,这样做没有意义。
先定义一个 block_size,即能得到内存的最小单位。
我们能从中取到的内存只可能是 block_size << n (n 为非负整数)。
这样做的原因是是为了记录已使用和未使用的内存。
记录已使用和未使用的内存
我们还需要一个固定大小的数组(如果总大小为 m, 数组大小为 (m / block_size) * 2)。
初始状态:
我们把一个二叉树压缩到数组中。
树的叶节点为 block_size。
父节点的值是子节点的二倍。
根节点为总大小。
重要规则:
每一个节点记录着该节点下能得到的最大内存。
分配时:
已经使用的内存将它节点(刚好大于等于你需要的内存大小的第一个查找到的节点)的值置为0,同时递归的向上修改父节点的值。
父节点值根据子节点而定:
- 如果子节点的值相同且该值等于对应层数的大小(例如根节点是总大小,根节点下的子节点就是总大小的一半,以此类推……),父节点就可以置为父节点对应层数的大小(也就是此时子节点值的2倍)。
- 否则父节点的值等于子节点值中较大的那个值。
归还时:
我们归还内存时也根据这些规则对数组进行修改:
根据大小和位置的偏移量锁定其对应置0的那个节点。
把它的 size 加回来,递归的按规则修改父节点。
搜寻合适大小的内存
比较简单,根据节点的大小就能确定能否分配,就这样选择子节点,直到内存大小刚刚合适。
注意选择时有偏好,可以尽量保留连续的大内存:
- 如果子节点都能满足要求时选更小的的那一个。
- 子节点值相同时选左边的那一个。
代码实现
以下代码来自于个人项目。
Mutex 对应 std::mutex Lock 对应 std::lock_guard 可以自行替换(别忘了 uint64 NoCopy,懂得都懂)
BufferPool.hpp
//
// Created by taganyer on 24-7-24.
//
#ifndef BASE_BUFFERPOOL_HPP
#define BASE_BUFFERPOOL_HPP
#ifdef BASE_BUFFERPOOL_HPP
#include <vector>
#include "Base/Mutex.hpp"
namespace Base {
class BufferPool : NoCopy {
public:
constexpr static uint64 block_size = 1 << 12;
/// 返回与 block_size 向上对齐的大小。
static uint64 round_size(uint64 target);
class Buffer;
explicit BufferPool(uint64 total_size);
~BufferPool();
Buffer get(uint64 size);
[[nodiscard]] uint64 total_size() const { return _size; };
[[nodiscard]] uint64 max_block() const { return _rest[0]; };
private:
uint64 _size = 0;
char* _buffer;
Mutex _mutex;
std::vector<uint64> _rest;
void put(Buffer &buffer);
[[nodiscard]] std::pair<uint64, char *> positioning(uint64 size) const;
[[nodiscard]] uint64 location(const char* target, uint64 size) const;
friend class Buffer;
public:
/// 自动归还内存
class Buffer : NoCopy {
public:
Buffer(Buffer &&other) noexcept : _buf(other._buf), _size(other._size), _pool(other._pool) {
other._buf = nullptr;
other._size = 0;
};
~Buffer() { put_back(); };
char* data() { return _buf; };
void put_back() { if (_buf) _pool.put(*this); };
[[nodiscard]] const char* data() const { return _buf; };
[[nodiscard]] uint64 size() const { return _size; };
[[nodiscard]] operator bool() const { return _buf; };
private:
char* _buf;
uint64 _size;
BufferPool &_pool;
Buffer(char* b, uint64 s, BufferPool &pool) : _buf(b), _size(s), _pool(pool) {};
friend class BufferPool;
};
};
}
#endif
#endif //BASE_BUFFERPOOL_HPP
BufferPool.cpp
//
// Created by taganyer on 24-7-24.
//
#include "BufferPool.hpp"
using namespace Base;
static inline std::pair<uint64, uint64> parent_sibling(uint64 i) {
uint64 p, s;
if (i & 1) {
p = (i - 1) >> 1;
s = i + 1;
} else {
p = (i - 2) >> 1;
s = i - 1;
}
return { p, s };
}
uint64 BufferPool::round_size(uint64 target) {
uint64 pre = 0, size = block_size;
while (size < target && size > pre) {
pre = size;
size <<= 1;
}
return size < pre ? pre : size;
}
BufferPool::BufferPool(uint64 total_size) :
_size(round_size(total_size)), _buffer(new char[_size]),
_rest(std::vector<uint64>(_size / block_size << 1)) {
for (uint64 s = _size, t = 1, i = 0; s >= block_size; s >>= 1, t <<= 1)
for (uint64 end = i + t; i < end; ++i)
_rest[i] = s;
}
BufferPool::~BufferPool() {
assert(_rest[0] != _size ? nullptr : "memory leak");
delete[] _buffer;
}
BufferPool::Buffer BufferPool::get(uint64 size) {
Lock l(_mutex);
if ((size = round_size(size)) > _rest[0])
return { nullptr, 0, *this };
auto [i, ptr] = positioning(size);
assert(_rest[i] == size);
_rest[i] = 0;
while (i > 0) {
auto [p, s] = parent_sibling(i);
uint64 ms = std::max(_rest[i], _rest[s]);
assert(_rest[p] >= ms);
if (_rest[p] == ms) break;
_rest[p] = ms;
i = p;
}
return { ptr, size, *this };
}
void BufferPool::put(Buffer &buffer) {
Lock l(_mutex);
uint64 i = location(buffer._buf, buffer._size), fs = buffer._size;
buffer._buf = nullptr;
buffer._size = 0;
assert(_rest[i] == 0);
_rest[i] = fs;
while (i > 0) {
auto [p, s] = parent_sibling(i);
uint64 ms = std::max(_rest[i], _rest[s]);
if (_rest[i] == fs && fs == _rest[s]) ms = fs << 1;
assert(_rest[p] <= ms);
if (_rest[p] == ms) break;
_rest[p] = ms;
i = p;
fs <<= 1;
}
}
std::pair<uint64, char *> BufferPool::positioning(uint64 size) const {
uint64 i = 0;
int n = 0;
for (uint64 s = _size; s != size; s >>= 1, ++n) {
uint64 _l = (i << 1) + 1, _r = (i << 1) + 2;
assert(_rest[_l] >= size || _rest[_r] >= size);
i = _rest[_r] < size || _rest[_l] >= size && _rest[_l] <= _rest[_r] ? _l : _r;
}
return { i, _buffer + (i - ((1 << n) - 1)) * size };
}
uint64 BufferPool::location(const char* target, uint64 size) const {
uint64 i = 0;
for (auto s = size; _size > s; s <<= 1) ++i;
i = (1 << i) - 1;
return i + (target - _buffer) / size;
}