loki 是书籍 《Modern C++ Design》配套发行的一个 C++ 代码库,里面对模板的使用发挥到了极致,对设计模式进行了代码实现。这里是 loki 库的源码。

ps. 有空是不是应该把里面的设计模式的代码学习学习, hahahah

loki 库里面有两个文件,SmallObj.h 以及 SmallObj.cpp,就是一个内存管理的内,可以单独使用。下面就其源码进行分析。

1. 类层次结构

SmallObj 文件里面有三个类:ChunkFixedAllocatorSmallObjAllocator。它们的类层次关系如下,其中SmallObjAllocator 是最上层的,直接供客户端使用的类。

2. Chunk

Chunk 就是直接管理单一内存块的类。它负责向操作系统索取内存块,并将内存块串成 “链表”。

2.1 初始化

先来看看其中的初始化函数 Init()

void FixedAllocator::Chunk::Init(std::size_t blockSize, unsigned char blocks)
{    pData_ = new unsigned char[blockSize * blocks];Reset(blockSize, blocks);
}void FixedAllocator::Chunk::Reset(std::size_t blockSize, unsigned char blocks)
{firstAvailableBlock_ = 0;blocksAvailable_ = blocks;unsigned char i = 0;unsigned char* p = pData_;for (; i != blocks; p += blockSize) //指向下一个可用的 block 的 index{*p = ++i;}
}
  • 传入参数为 block 的大小和数量
  • 用 operator new 分配出一大块内存 chunk,并用指针 pData_ 指向 chunk。
  • Reset() 函数对这块内存进行分割。利用嵌入式指针的思想,每一块 block 的第一个 byte 存放的是下一个可用的 block 距离起始位置 pData_ 的偏移量(以 block 大小为单位),以这种形式将 block 串成 “链表”。
  • firstAvailableBlock_ 表示当前可用的 block 的偏移量;blocksAvailable_ 表示当前 chunk 中剩余的 block 数量。

初始状态的 chunk 如下图所示:

2.2 内存分配 allocate
void* FixedAllocator::Chunk::Allocate(std::size_t blockSize)
{if (!blocksAvailable_) return 0;unsigned char* pResult =pData_ + (firstAvailableBlock_ * blockSize);firstAvailableBlock_ = *pResult;--blocksAvailable_;return pResult;
}

这段代码也很好理解,可以结合下图理解:

2.3 内存回收 deallocate
void FixedAllocator::Chunk::Deallocate(void* p, std::size_t blockSize)
{unsigned char* toRelease = static_cast<unsigned char*>(p);//链接到链表上*toRelease = firstAvailableBlock_;//修改 “头指针”//回收的 block 指针距离头指针 pData_ 的距离(以 block 为单位)firstAvailableBlock_ = static_cast<unsigned char>((toRelease - pData_) / blockSize);++blocksAvailable_;
}

同之前的内存回收一样,回收的时候,都把 block 插到链表的头部。

3. FixedAllocate

FixedAllocate 则负责管理一个具有相同 block size 的 chunk 的集合。它负责根据客户需求,创建特定 block 大小的 chunk ,并放置在容器 vector 中进行管理。

3.1 内存分配 allocate
void* FixedAllocator::Allocate()
{if (allocChunk_ == 0 || allocChunk_->blocksAvailable_ == 0){   //目前没有标定的 chunk ,或者这个 chunk 已经用完了//从头找起Chunks::iterator i = chunks_.begin();for (;; ++i){//没找到,则创建一个新的 chunkif (i == chunks_.end()){// Initializechunks_.reserve(chunks_.size() + 1);Chunk newChunk;newChunk.Init(blockSize_, numBlocks_);chunks_.push_back(newChunk);allocChunk_ = &chunks_.back();//上面容器的大小会增长一个,可能会引起 vetor 的扩容操作,导致原来的元素被搬移到新的地方//所以这里的 deallocChunk 需要重新标定,直接标定为第一个。原来可能并不是指向第一个,//原来的那个经过搬移之后已经无法确定新的位置了deallocChunk_ = &chunks_.front();break;}if (i->blocksAvailable_ > 0){allocChunk_ = &*i;break;}}}return allocChunk_->Allocate(blockSize_);
}
  • allocChunk_ 指向当前正在使用中的 chunk。如果 allocChunk_ 指向的 chunk 中的 block 已经用完了,那么就在容器中去寻找其他可用的 chunk 。如果没有找到,就新建一个 chunk ,放进容器中,并标定为当前的 allocChunk_
  • 本来这里的 allocate 动作跟 deallocChunk_ 成员没有关系的。但是,创建新的 chunk 并添加进 vector 中后,可能会引起 vector 的内存重分配动作,导致原来的 deallocChunk_ 指向的内存并不存在了,所以要对 deallocChunk_ 重新标定。它这里直接重新标定为第一个 chunk,因为原来的那个已经无法确定位置了。 ps. 大神就是大神,这都能想到~

其实我这里有一个疑问。用 allocChunk_ 变量对当前使用的 chunk 进行标定,如果当前使用的 chunk 没有用完,会一直使用这一块 chunk,直到这一块 chunk 用完。那么,当这一块 chunk 用完的时候,其他的 chunk 难道不是能够确定一定用完了吗?那么还有必要去对 vector 进行遍历,去寻找可用的 chunk 吗?为什么不直接就去创建一个新的 chunk ?

3.2 内存回收 deallocate

我们需要根据归还内存的位置,把这块内存回收到相对应的 chunk 中。

void FixedAllocator::Deallocate(void* p)
{    deallocChunk_  = VicinityFind(p);DoDeallocate(p);
}
  • 我们知道每一块 chunk 的头指针,以及这一块 chunk 的大小,这样的话,我们就可以计算出每一块 chunk 的地址范围。我们只要找到归还的内存的地址是落在哪一个 chunk 地址范围内,就可以确定 chunk。这一功能由函数 VicinityFind() 实现。
  • VicinityFind() 函数采用一种分头查找的算法,从上一次 deallocChunk_ 的位置出发,在容器中分两头查找。这也应该是设计这个 deallocChunk_ 指针的原因把。内存分配通常是给容器服务的。而容器内元素连续创建时,通常就从同一个 chunk 获得连续的地址空间。归还的时候当然也需要归还到同一块 chunk 。通过对上一次归还 chunk 的记录,能提高搜索的效率。下面是 VicinityFind() 的实现代码:
FixedAllocator::Chunk* FixedAllocator::VicinityFind(void* p)
{const std::size_t chunkLength = numBlocks_ * blockSize_;Chunk* lo = deallocChunk_;Chunk* hi = deallocChunk_ + 1;Chunk* loBound = &chunks_.front();Chunk* hiBound = &chunks_.back() + 1;// Special case: deallocChunk_ is the last in the arrayif (hi == hiBound) hi = 0;for (;;){if (lo){if (p >= lo->pData_ && p < lo->pData_ + chunkLength){return lo;}if (lo == loBound) lo = 0;else --lo;}if (hi){if (p >= hi->pData_ && p < hi->pData_ + chunkLength){return hi;}if (++hi == hiBound) hi = 0;}}
}
  • 最后内存回收的动作由函数 DoDeallocate() 完成。如果当前回收的 chunk 已经将所有的 block 全部回收完了,即 deallocChunk_->blocksAvailable_ == numBlocks_ ,本来这块内存就可以归还给 OS 了的。但是这里采取了一个延迟归还的动作。把这个空的 chunk 通过 swap 函数放在 vector 的末尾,并且将 allocChunk_ 指向它,供下一次再使用。只有当有两个空 chunk 出现时,才会把上一个空的 chunk 归还给 OS。下面是源码:
void FixedAllocator::DoDeallocate(void* p)
{// call into the chunk, will adjust the inner list but won't release memorydeallocChunk_->Deallocate(p, blockSize_);//如果已经全回收了if (deallocChunk_->blocksAvailable_ == numBlocks_){// deallocChunk_ is completely free, should we release it? Chunk& lastChunk = chunks_.back();//最后一个就是当前的 deallocChunkif (&lastChunk == deallocChunk_){// check if we have two last chunks emptyif (chunks_.size() > 1 && deallocChunk_[-1].blocksAvailable_ == numBlocks_){// Two free chunks, discard the last onelastChunk.Release();chunks_.pop_back();allocChunk_ = deallocChunk_ = &chunks_.front();}return;}if (lastChunk.blocksAvailable_ == numBlocks_){// Two free blocks, discard onelastChunk.Release();chunks_.pop_back();allocChunk_ = deallocChunk_;}else{// move the empty chunk to the endstd::swap(*deallocChunk_, lastChunk);allocChunk_ = &chunks_.back();}}
}

这里我突然明白上面说到的,为什么要遍历容器寻找下一个可用的 chunk 的问题了。因为在这里,allocChunk_ 会转而指向这个回收完成了的空的 chunk,它原来指向的 chunk 可能并没有使用完。
不过这里我又有一个新的疑惑。如果当前空的 chunk 就是容器中最后一个时,为什么要往前看一个 chunk,看它是不是空?前面一个有可能是空吗??

4. SmallObjAllocator

SmallObjAllocator 则负责管理具有不同 block size 的 FixedAllocate 的 vector 集合。

4.1 内存分配 allocate
void* SmallObjAllocator::Allocate(std::size_t numBytes)
{if (numBytes > maxObjectSize_) return operator new(numBytes);if (pLastAlloc_ && pLastAlloc_->BlockSize() == numBytes){return pLastAlloc_->Allocate();}//找到第一个 >= numBytes 的位置Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes);//没找到相同的,就重新创建一个 FixedAllocatorif (i == pool_.end() || i->BlockSize() != numBytes){i = pool_.insert(i, FixedAllocator(numBytes));pLastDealloc_ = &*pool_.begin();}pLastAlloc_ = &*i;return pLastAlloc_->Allocate();
}
  • SmallObjAllocator 不可能无穷无尽的满足客户不同的 block size 的需求。它设有一个最大的 block size 变量 maxObjectSize_ 。如果客户端需求的 block size 大于这个 threshold,就直接交由 operator new 去进行处理。
  • pLastAlloc_ 记录上一次分配 block 的 FixedAllocator object 。如果这一次需求的 block size 等于上一次分配的 block size,就直接使用同一个 FixedAllocator object 去分配内存。我认为这个变量的设计和 FixedAllocator 中 deallocChunk_ 的设计道理是一样的。 SmallObjAllocator 是给容器服务,而容器通常连续多次为其中的 element 索取多个相同 size 的 block,所以对上一次分配的 FixedAllocator object 进行记录能够减少不必要的查找动作。
  • 如果这一次需求的 block size 不等于上一次分配的 block size,就遍历容器寻找不小于需求的 block size 而且最接近的位置,也就是 std::lower_bound() 函数的功能。如果找到 block size 相等的,就直接分配;如果没找到相等的,就在该位置上插入一个新的 FixedAllocator object。同样,为了防止 vector 扩容操作引起重新分配内存,需要对 pLastDealloc_ 进行重定位。
4.2 内存回收 deallocate
void SmallObjAllocator::Deallocate(void* p, std::size_t numBytes)
{if (numBytes > maxObjectSize_) return operator delete(p);if (pLastDealloc_ && pLastDealloc_->BlockSize() == numBytes){pLastDealloc_->Deallocate(p);return;}Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes);assert(i != pool_.end());assert(i->BlockSize() == numBytes);pLastDealloc_ = &*i;pLastDealloc_->Deallocate(p);
}
  • 设计上没啥新颖的,不多说了。

5. 总结

相比于之前分析的 std::alloc 的内存管理:

  • std::alloc 一旦向 OS 索取了新的 chunk,就不会还给 OS 了,一直在自己的掌控之中。因为它里面的指针拉扯比较复杂,几乎不可能去判断一块 chunk 中给出去的 block 是否全部归还了。但是 loki::allocator 通过利用一个 blocksAvailable_ 变量,就很容易的判断出某一块 chunk 中的 block 是否已经全部归还了,这样就可以归还给 OS。
  • std::alloc 只负责一些特定 block size 的内存管理。如果客户端需要的 block size 它并不支持,那个客户端的 block size 会被取整到最接近的大小 (当然前提是小于它所能够分配的最大的 block size);但是 loki::allocator 能够为不大于最大 block size 的所有 block size 服务。

【C++内存管理】loki::allocator 源码分析相关推荐

  1. Android磁盘管理-之vold源码分析(2)

    作者:gzshun. 原创作品,转载请标明出处! Vold是Android系统处理磁盘的核心部分,取代了原来Linux系统中的udev,主要用来处理Android系统的热插拔存储设备.在Android ...

  2. redis evict.c内存淘汰机制的源码分析

    众所周知,redis是一个内存数据库,所有的键值对都是存储在内存中.当数据变多之后,由于内存有 限就要淘汰一些键值对,使得内存有足够的空间来保存新的键值对.在redis中,通过设置server.max ...

  3. hadoop作业初始化过程详解(源码分析第三篇)

    (一)概述 我们在上一篇blog已经详细的分析了一个作业从用户输入提交命令到到达JobTracker之前的各个过程.在作业到达JobTracker之后初始化之前,JobTracker会通过submit ...

  4. List接口的常用方法以及ArrayList/LinkedList源码分析

    1.List接口的常用方法 ArrayList list = new ArrayList();list.add(123);list.add(456);list.add("AA"); ...

  5. storm启动supervisor源码分析-supervisor.clj

    storm启动supervisor源码分析-supervisor.clj supervisor是storm集群重要组成部分,supervisor主要负责管理各个"工作节点".sup ...

  6. Python3.5源码分析-内存管理

    Python3源码分析 本文环境python3.5.2. 参考书籍<<Python源码剖析>> python官网 Python3的内存管理概述 python提供了对内存的垃圾收 ...

  7. 【Linux 内核 内存管理】物理内存组织结构 ④ ( 内存区域 zone 简介 | zone 结构体源码分析 | zone 结构体源码 )

    文章目录 一.内存区域 zone 简介 二.zone 结构体源码分析 1.watermark 成员 2.lowmem_reserve 成员 3.zone_pgdat 成员 4.pageset 成员 5 ...

  8. Spark源码分析之九:内存管理模型

    Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了.那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Sp ...

  9. nginx源码分析—内存池结构ngx_pool_t及内存管理

    本博客( http://blog.csdn.net/livelylittlefish)贴出作者(阿波)相关研究.学习内容所做的笔记,欢迎广大朋友指正! Content 0.序 1.内存池结构 1.1 ...

最新文章

  1. 3.Deep Neural Networks for YouTube Recommendations论文精细解读
  2. 以太坊知识教程------智能合约(2)调用
  3. Log4net 中输出日志到文件,文件名根据日期生成
  4. docker redis重启_Docker解决傻瓜式安装软件
  5. 仁慈型dea matlab程序,数据包络分析(DEA)方法..docx
  6. 看博客学学Android(五)
  7. android之隐示意图跳转启动另一个activity
  8. android Mvp简单实用
  9. Navicat Report Viewer 如何连接到 MySQL 数据库
  10. Nginx源码分析 - 主流程篇 - 多进程实现(14)
  11. 【语音加密】基于matlab GUI语音信号加密解密【含Matlab源码 295期】
  12. 用C语言来统计文件中单词的个数(C语言笔记)
  13. 【SQL注入-02】SQL注入点的简单判断
  14. 最新版android迅雷,迅雷下载2021安卓最新版_手机app官方版免费安装下载_豌豆荚...
  15. 大数据剖析:想与北上争雄,深圳到底还差在哪儿?
  16. php实现分时线图,史上最全分时图买卖点图解(转发收藏)!
  17. 大数据薪水大概多少_大数据各岗位薪资收入水平多少?出路在哪里?
  18. Verilog快速入门(13)—— 用3-8译码器实现全减器
  19. 扫地机器人灰尘堵住_不怕脏更不怕累!这才是清理扫地机器人的正确姿势
  20. 视频安全之授权播放和防录屏跑马灯

热门文章

  1. AI×IoT时代打造未来家居,TCL智能新品必须加入购物车...
  2. matlab之常微分方程(ODE)求解
  3. java实现csv导入pg数据库
  4. ffmpeg的filter分析
  5. 2020.7.22 T3押韵(jz暑假训练day7)
  6. 苏州到底有没有互联网?
  7. STM32F103的FLASH代码
  8. 全网显示 IP 归属地,可以考虑这个开源库
  9. Android View座标
  10. 多项目如何高效协同合作 | springcloud系列之bus消息总线