内存池 是 池化技术 的一种,通过池化,可以有效地减少资源对象创建的次数,提高程序运行的性能。
庞杂的内存问题,如何理出自己的思路出来
一、 内存池简介
1.1 基本概念
由上图,我们可以了解内存池的大概思想在于:
- 如果不使用内存池,用户程序是直接向操作系统申请内存的
- 如果使用内存池,用户程序会先询问内存池能不能够分配所申请的内存大小,如果不能则会先向操作系统申请内存到内存池,再由内存池间接返回内存给用户程序
综上,内存池的实现主要是为了 避免 用户程序频繁的直接向操作系统申请内存而造成的 大量的系统调用开销,使用内存池可以将一次向操作系统申请大块的内存,然后再在用户态逐次的分配给用户程序,大大减少了系统调用次数,从而使得程序的性能更高。
二、 内存池代码实现
2.1 基本接口介绍
该内存池代码来自于,Google 的 LevelDB 库,该 Arena 对象就是一个极简的内存池实现
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
class Arena {
public:
Arena();
// 内存池禁止拷贝
Arena(const Arena &) = delete;
Arena &operator=(const Arena &) = delete;
~Arena();
// 内存申请,返回指向所申请内存块的指针
char *Allocate(size_t bytes);
// 内存统计,记录该对象共申请了多少内存
size_t MemoryUsage() const { return memory_usage_; }
private:
char *AllocateNewBlock(size_t block_bytes);
// 分配状态
char *alloc_ptr_;
size_t alloc_bytes_remaining_;
//分配的内存块地址记录,便于释放内存
std::vector<char *> blocks_;
// 内存申请数量统计
size_t memory_usage_;
};
仔细查看上面的代码,发现内存池的核心就在于如何分配内存以及如何释放内存,下面将详细讲解这两部分。
2.2 内存池内存分配
内存池的 内存分配主要分为三步,这三步的依赖关系为如果完成前面一步,则不需要继续后面的步骤,下面将详细解释这三部分:
(1)如下图,Allocate 方法向内存池(Arena)申请 bytes 大小的内存,如果内存池中 alloc_bytes_remaining_ ≥ bytes, 那么则由内存池自己分配内存;如果内存池中 alloc_bytes_remaining_ < bytes,则进入第二步或者第三步
(2)如下图,Allocate 方法向内存池(Arena)申请的内存≥ kBlockSize / 4,那么 Allocate 方法会直接向操作系统申请内存,也即调用操作系统的 new/malloc 方法
(3)如下图,Allocate 方法向内存池(Arena)申请的内存小于 kBlockSize / 4,如果频繁直接向操作系统申请这么小的内存,性能上不划算,所以内存池会向操作系统申请 kBlockSize 大小的内存,然后分配给 Allocate 方法所需要的 bytes 大小内存,自己保留 kBlockSize - bytes 的内存(该保留的内存也即剩余的内存,使用alloc_bytes_remaining_ 成员变量记录大小,使用 alloc_ptr_成员变量记录地址)
了解完以上步骤后,相信很容易看懂 Allocate 的代码
char *Arena::Allocate(size_t bytes) {
assert(bytes > 0);
if (bytes <= alloc_bytes_remaining_) {
// 内存分配第一步
char *result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
if (bytes > kBlockSize / 4) {
// 内存分配第二步
// 如果大于kBlockSize / 4,那么调用AllocateNewBlock分配, 也即找操作系统分配
char *result = AllocateNewBlock(bytes);
return result;
}
// 内存分配第三步
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize;
char *result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
// 直接向操作系统申请指定大小的内存
char *Arena::AllocateNewBlock(size_t block_bytes) {
char *result = new char[block_bytes];
blocks_.push_back(result);
memory_usage_ += (block_bytes + sizeof(char *));
return result;
}
2.3 内存池内存释放
之前的内存分配,内存池都会用一个 blocks_ 私有成员记录分配的地址,下图的箭头就表示指向地址的指针,那么内存池销毁需要做的工作就是遍历 blocks_,delete 每一个指针所指向的内存
内存池的销毁在Arena的析构函数里面,代码如下
Arena::~Arena() {
for (size_t i = 0; i < blocks_.size(); i++) {
delete[] blocks_[i];
}
}
2.4 总结
内存池的主要思想就在于内存分配和内存销毁,了解这些,就能动手实现一个简易的内存池了!
三、 性能比较
3.1 简易性能比较
之前提到,内存池的主要作用就是为了避免用户程序频繁的向操作系统申请内存造成的开销,那么如何验证内存池真的能减少开销呢?
这里做了一个简单的实验验证:
1 . A组:不使用内存池,不断向操作系统申请内存
2 . B组:使用内存池,不断向内存池申请内存
对比A、B两组的耗时,如果耗时时间短,则性能更高
void test_allocate_time() {
int N = 100000;
char *p;
clock_t start_time_without_memory_pool;
clock_t end_time_without_memory_pool;
start_time_without_memory_pool = clock();
for (int i = 1; i < N; ++i) {
p = new char[i]; // 这里没有delete,会造成内存泄漏,不够严谨
}
end_time_without_memory_pool = clock();
std::cout << "Allocate time without memorypool: "
<< (end_time_without_memory_pool - start_time_without_memory_pool)
<< std::endl;
Arena arena;
clock_t start_time_with_memory_pool;
clock_t end_time_with_memory_pool;
start_time_with_memory_pool = clock();
for (int i = 1; i < N; ++i) {
p = arena.Allocate(i);
}
end_time_with_memory_pool = clock();
std::cout << "Allocate time with memorypool: "
<< (end_time_with_memory_pool - start_time_with_memory_pool)
<< std::endl;
}
最后的实验结果如下:
Allocate time without memorypool: 340470
Allocate time with memorypool: 245339
可见,使用内存池分配内存所花费的CPU时钟周期更少,也即分配时间更短,故该场景下性能更高。
LinuxC/C++服务器开发/架构师 学习地址
Linux服务器开发/架构师面试题、学习资料、教学视频和学习路线图(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),分享有需要的可以自行添加 学习交流群