C++11实现高效内存池

本文深入解析C++11内存池项目,探讨内存池原理及allocator设计,重点介绍MemoryPool类的构造、内存分配与释放机制。

前言

本篇文章主要内容为讲述自己对于C++11内存池项目(一位大佬所写)的解析(注意这里是我记录一下自己对项目的解析)。初次上手项目,很多知识点都没有遇到过,有些地方会提供其他的博文帮助理解,有描述不清楚或存在错误的地方还请大家一一指出。


【源码剖析】MemoryPool —— 简单高效的内存池 allocator 实现

项目源码:GitHub源码

项目介绍

内存池是什么

话不多说,这里摘录最具权威的原作者对于项目的解释:
什么是内存池?什么是 C++ 的 allocator?
内存池简单说,是为了减少频繁使用 malloc/free new/delete 等系统调用而造成的性能损耗而设计的。当我们的程序需要频繁地申请和释放内存时,频繁地使用内存管理的系统调用可能会造成性能的瓶颈,嗯,是可能,毕竟操作系统的设计也不是盖的(麻麻说把话说太满会被打脸的(⊙v⊙))。内存池的思想是申请较大的一块内存(不够时继续申请),之后把内存管理放在应用层执行,减少系统调用的开销。

allocator详解

那么,allocator 呢?它默默的工作在 C++ 所有容器的内存分配上。默默贴几个链接吧:

C++ allocator

C++学习笔记(十) 内存机制与Allocator

class templatestd::allocator

allocator_traits


当你对 allocator 有基本的了解之后,再看这个项目应该会有恍然大悟的感觉,因为这个内存池是以一个 allocator 的标准来实现的。一开始不明白项目里很多函数的定义是为了什么,结果初步了解了 allocator 后才知道大部分是标准接口。这样一个 memory pool allocator 可以与大多数 STL 容器兼容,也可以应用于你自定义的类。像作者给出的例子 —— test.cpp, 是用一个基于自己写的 stack 来做 memory pool allocator 和 std::allocator 性能的对比 —— 最后当然是 memory pool allocator 更优

内存池是一个一个的 block 以链表的形式连接起来,每一个 block 是一块大的内存,当内存池的内存不足的时候,就会向操作系统申请新的
block 加入链表。还有一个 freeSlots_ 的链表,链表里面的每一项都是对象被释放后归还给内存池的空间,内存池刚创建时
freeSlots_> 是空的,之后随着用户创建对象,再将对象释放掉,这时候要把内存归还给内存池,怎么归还呢?就是把指向这个对象的内存的指针加到
freeSlots_ 链表的前面(前插)。
用户在创建对象的时候,先检查 freeSlots_ 是否为空,不为空的时候直接取出一项作为分配出的空间。否则就在当前 block 内取出一个 Slot_ 大小的内存分配出去,如果 block 里面的内存已经使用完了呢?就向操作系统申请一个新的 block。

内存池工作期间的内存只会增长,不释放给操作系统。直到内存池销毁的时候,才把所有的 block delete 掉

注释源码: 点我到注释源码
下图左一为刚申请的Blcok↓
Blcok详情


MemoryPool.tcc

该文件对Memory.h文件中声明过的函数进行实现,在此一些简单的类函数就不再一一赘述,只挑选出部分函数给出详细剖析,其余可在最后的代码汇总中查看注释。函数中使用了大量的强制转换,这里贴几个有关博客:
【C++】四种强制类型转换
c++类型转换


allocateBlock 创建Block块

函数用于申请一块Block内存块,并使用头插法放入Blcok内存块链表。

//申请一块新的分区block
template <typename T, size_t BlockSize>
void
MemoryPool<T, BlockSize>::allocateBlock()
{
   
   
  // Allocate space for the new block and store a pointer to the previous one
  //申请 BlockSize字节大小的一块空间并用char* 类型指针接收
  data_pointer_ newBlock = reinterpret_cast<data_pointer_>
                           (operator new(BlockSize));
  //newBlock成为新的block内存首址              
  reinterpret_cast<slot_pointer_>(newBlock)->next = currentBlock_;

  //currentBlock 更新为 newBlock的位置
  currentBlock_ = reinterpret_cast<slot_pointer_>(newBlock);

  // Pad block body to staisfy the alignment requirements for elements
  //保留第一个slot 用于Block链表的链接
  data_pointer_ body = newBlock + sizeof(slot_pointer_);

  //求解空间对齐需要的字节数
  size_type bodyPadding = padPointer(body, alignof(slot_type_));

  //若出现残缺不可以作为一个slot的空间,跳过这块空间作为第一个可用slot的位置 
  currentSlot_ = reinterpret_cast<slot_pointer_>(body + bodyPadding);
  lastSlot_ = reinterpret_cast<slot_pointer_>
              (newBlock + BlockSize - sizeof(slot_type_) + 1);
   //始址: newBlock  块大小 BlockSize 末址 newBlock + BlockSize 末址减去一个slot槽大小
   //得到倒数第二个slot的末址 再加一得到最后一块slot的始址
}

这里需要注意的是:在生成一块BlockSize字节大小的Block内存块时需要保留Slot类型大小的空间用于后续Block链表的链接。对应图中的蓝色内存区。此外,在调用padPointer函数 的时候传入的参数使用了 C++11 的新特性 C++11 内存对齐 alignof alignas


padPointer 空间对齐

每一个Block内存块的大小(BlockSize字节)并不是刚好为Slot槽大小的整数倍(取决于使用者定义的BlockSize),例如一个内存块Block大小设置为BlockSize = 11 bytes,每个Slot槽存放数据的大小为4字节时,BlockSize % sizeof(Slot) = 11 % 4 = 2 … 3,余下的三个字节是不足一个Slot槽所需空间的,这时我们必须略去这些多余的空间。上述空间在图中用橙色区域进行了表示。
在这里插入图片描述

//计算对齐所需补的空间
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::size_type
MemoryPool<T, BlockSize>::padPointer(data_pointer_ p, size_type align)
const noexcept
{
   
   
  uintptr_t result = reinterpret_cast<uintptr_t>(p);//将char* 类型的指针转换为 uintptr_t 类型(无符号整型)
  return ((align - result) % align);//多余不够一个Slot槽大小的空间,需要跳过
}

MemoryPool 构造函数

这里说明一下内存池中使用到的指针,先看看原作者是怎么描述的:

    slot_pointer_ currentBlock_;  // 内存块链表的头指针
    slot_pointer_ currentSlot_;   // 元素链表的头指针
    slot_pointer_ lastSlot_;      // 可存放元素的最后指针
    slot_pointer_ freeSlots_;     // 元素构造后释放掉的内存链表头指针

当时阅读项目时很不解的点就是 currentSlot 指针的含义,作者给出的解释是元素链表的头指针 ,我思来想去,每一个Slot槽是一个联合体:

union Slot_ {
   
   
        value_type element;//使用时为 value_type 类型
        Slot_* next;//需要回收时为 Slot_* 类型并加入 空闲链表中
    };

Slot槽
当Slot用来存放数据时,根据联合的特性,另一个指针属性是不可以使用的,这两个属性的关系也很简单,不可同时出现。因此当Slot槽存储数据时不可能同时是一个链表(链表至少需要一个指针),正是这样,再根据后面对于currentSlot指针的使用我将其解释为
指向第一个可用元素 Slot ,这是狭义上的第一个可用,在后面allocate函数中为每一个元素分配空间的时候currentSlot指针是明确的只能往后走的,也就是说这个 “可用” 不包括已经在空闲链表上的Slot(使用过但现在释放并还给内存池的槽),指向的是从没有被置放过数据的Slot槽。

//构造函数 初始化所有指针为 nullptr
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool()
noexcept
{
   
   
  currentBlock_ = nullptr;//指向第一块Block区 即Block内存块链表的头指针
  currentSlot_ = nullptr;//当前第一个可用槽的位置
  lastSlot_ = nullptr;//最后可用Slot的位置
  freeSlots_ = nullptr;//空闲链表头指针
}

allocate 内存分配

为每个element元素分配内存,先查询槽的空闲链表是否为空,若不为空则直接将链表头结点表示的空间分配出去,反之到Block中分割新的Slot大小的空间。

template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::pointer
MemoryPool<T, BlockSize>::allocate(size_type n, const_pointer hint)
{
   
   //在内存池中申请 n 个Node,hint 默认设置为 空
  if (freeSlots_ != 
评论 24
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nepu_bin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值