内存池架构

TBOX的内存管理模型,参考了linux kernel的内存管理机制,并在其基础上做了一些改进和优化。

内存整体架构

large_pool

整个内存分配的最底层,都是基于large_pool的大块内存分配池,类似于linux的基于page的分配管理,不过有所不同的是,large_pool并没有像linux那样使用buddy算法进行(2^N)*page进行分配,这样如果需要2.1m的内存,需要分配4m的内存块,这样力度太大,非常浪费。

因此large_pool内部采用N*page的基于page_size为最小粒度进行分配,因此每次分配顶多浪费不到一页的空间。

而且如果需要的内存不到整页,剩下的内存也会一并返回给上层,如果上层需要(比如small_pool),可以充分利用这多余的部分内存空间,使得内存利用率达到最优化。

而且根据tb_init实际传入的参数需求,large_pool有两种模式:

1.  直接使用系统内存分配接口将进行大块内存的分配,并用双链维护,这种比较简单,就不多说了。

2.  在一大块连续内存上进行统一管理,实现内存分配。

具体使用哪种方式,根据应用需求,一般的应用只需要使用方式1就行了,这个时候tb_init传tb_null就行了,如果是嵌入式应用,需要管理有限的一块内存空间,这个时候可以使用方式2, tb_init传入指定内存空间地址和大小。

这里就主要看下方式2的large_pool的内存结构(假设页大小是4KB):

--------------------------------------------------------------------------

|                                     data                                 |

--------------------------------------------------------------------------

|

--------------------------------------------------------------------------

| head | 4KB | 16KB | 8KB | 128KB | ... | 32KB |       ...       |  4KB*N  |

--------------------------------------------------------------------------

由于large_pool主要用于大块分配,而超小块的分配在上层small_pool中已经被分流掉了,所以这个应用中,large_pool不会太过频繁的分配,所以碎片量不会太大,为了进一步减少碎片的产生,在free时候都会对下一个邻近的空闲块进行合并。而malloc在分配当前空闲块空间不够的情况下,也会尝试对下一个邻近空闲块进行合并。

由于每个内存块都是邻近挨着的,也没用双链维护,没有内存块,都有个块头,合并过程仅仅只是改动内存块头部的size字段,这样的合并不会影响效率。

由于没像buddy算法那样,用双链维护空闲内存,虽然节省了链表维护的空间和时间,但是每次分配内存都要顺序遍历所有块,来查找空闲的内存,这样的效率实在太低了,为了解决这个问题,large_pool内部针对不同级别的块,进行了预测,每次free或者malloc的时候,如果都会把当前和邻近的空闲快,缓存到对应级别的预测池里面去,具体的分级如下:

--------------------------------------

| >0KB :      4KB       | > 0*page     |

|-----------------------|--------------

| >4KB :      8KB       | > 1*page     |

|-----------------------|--------------

| >8KB :    12-16KB     | > 2*page     |

|-----------------------|--------------

| >16KB :   20-32KB     | > 4*page     |

|-----------------------|--------------

| >32KB :   36-64KB     | > 8*page     |

|-----------------------|--------------

| >64KB :   68-128KB    | > 16*page    |

|-----------------------|--------------

| >128KB :  132-256KB   | > 32*page    |

|-----------------------|--------------

| >256KB :  260-512KB   | > 64*page    |

|-----------------------|--------------

| >512KB :  516-1024KB  | > 128*page   |

|-----------------------|--------------

| >1024KB : 1028-...KB  | > 256*page   |

--------------------------------------

由于通常不会分配太大块的内存,因此只要能够预测1m内存,就足够,而对于>1m的内存,这里也单独加了一个预测,来应对偶尔的超大块分配,并且使得整体分配流程更加的统一。

如果当前级别的预测块不存在,则会到下一级别的预测块中查找,如果都找不到,才回去遍历整个内存池。

实际测试下,每个块的预测成功基本都在95%以上,也就说大部分情况下,分配效率都是维持在O(1)级别的。

small_pool

小块内存分配池

在上层每次调用malloc进行内存分配的时候,回去判断需要多大的内存,如果这个内存超过或者等于一页,则会直接从large_pool进行分配,如果小于一页,则会优先通过small_pool进行分配,small_pool针对小块的内存进行了高速缓存,并优化了空间管理和分配效率。

由于程序大部分情况下,都在使用小块内存,因此small_pool对内存的分配做了很大的分流,使得large_pool承受的压力减小,碎片量减少很多,而small_pool内部由于都是由fixed_pool来对固定大小的内存进行管理,是不会存在外部碎片的。而小块内存的粒度本身就很小,所以内部碎片量也相当少。

small_pool中的fixed_pool,就像是linux kernel中的slub,在small_pool中总共有12级别的fixed_pool,每个级别分别管理一种固定大小的内存块,具体级别如下:

--------------------------------------

|    fixed pool: 16B    |  1-16B       |

|--------------------------------------|

|    fixed pool: 32B    |  17-32B      |

|--------------------------------------|

|    fixed pool: 64B    |  33-64B      |

|--------------------------------------|

|    fixed pool: 96B*   |  65-96B*     |

|--------------------------------------|

|    fixed pool: 128B   |  97-128B     |

|--------------------------------------|

|    fixed pool: 192B*  |  129-192B*   |

|--------------------------------------|

|    fixed pool: 256B   |  193-256B    |

|--------------------------------------|

|    fixed pool: 384B*  |  257-384B*   |

|--------------------------------------|

|    fixed pool: 512B   |  385-512B    |

|--------------------------------------|

|    fixed pool: 1024B  |  513-1024B   |

|--------------------------------------|

|    fixed pool: 2048B  |  1025-2048B  |

|--------------------------------------|

|    fixed pool: 3072B* |  2049-3072B* |

--------------------------------------

其中 96B, 192B,384B,3072B并不是按2的整数幂大小,这么做主要是为了更加有效的利用小块内存的空间减少内部碎片。

fixed_pool

顾名思义,fixed_pool就是用来管理固定大小的内存分配的,相当于linux中slub,而fixed_pool中又由多个slot组成,每个slot负责一块连续的内存空间,管理部分内存块的管理,类似linux中的slab, 每个slot由双链维护,并且参考linux的管理机制,分为三种slot管理方式:

  1. 当前正在分配的slot
  2. 部分空闲slots链表
  3. 完全full的slots链表

具体结构如下:

current:

--------------

|              |

--------------    |

|     slot     |<--

|--------------|

||||||||||||||||

|--------------|

|              |

|--------------|

|              |

|--------------|

||||||||||||||||

|--------------|

||||||||||||||||

|--------------|

|              |

--------------

partial:

--------------       --------------               --------------

|     slot     | <=> |     slot     | <=> ... <=> |     slot     |

|--------------|     |--------------|             |--------------|

||||||||||||||||     |              |             |              |

|--------------|     |--------------|             |--------------|

|              |     ||||||||||||||||             |              |

|--------------|     |--------------|             |--------------|

|              |     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             |              |

|--------------|     |--------------|             |--------------|

||||||||||||||||     |              |             |              |

|--------------|     |--------------|             |--------------|

|              |     |              |             ||||||||||||||||

--------------       --------------               --------------

full:

--------------       --------------               --------------

|     slot     | <=> |     slot     | <=> ... <=> |     slot     |

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

|--------------|     |--------------|             |--------------|

||||||||||||||||     ||||||||||||||||             ||||||||||||||||

--------------       --------------               --------------

具体的分配算法

  1. 如果当前slot中还有空闲的块,优先从当前slot进行分配
  2. 如果当前slot中没有空闲块,则把这个slot放到full链表中去
  3. 从部分空闲slot链表中,挑一个空闲的slot进行分配,并把它设为当前分配状态。

具体的释放算法

  1. 释放后如果这个slot完全空闲了,并且不是正在分配的slot,则把整个slot释放掉,这样既可以保证有一个可以分配的slot之外,还极大的降低了内存使用,也避免某些情况下频繁的释放分配slot。
  2. 如果释放的slot属于full链表并且变为了部分空闲,则把这个slot移到部分空闲slot链表中去。

额外要提一下的是:

large_pool每次分配一块空间给一个slot的时候,残留下来的部分剩余空间(<1*page), 也能直接返回给slot,让slot充分利用这部分数据,这样可以可以切分出更多地内存块。

例如:

fixed_pool每次增长一个包含256个32B内存块的slot(需要8192B大小+16B内部数据维护大小),其实在用large_pool分配的时候,需要8208B的大小,由于需要按页对齐(4KB),实际分配确占用了`8192+4096: 12288B`的大小的空间。

但是large_pool支持把所有空间数据一并返回给上层,这样slot其实获取到了一个12288B大小的内存,并且也知道其实际大小为:12288B,因此实际切分了`(12288-(32B的slot内部维护数据))/32`也就是383个内存块。

多维护了127个内存块,充分把large_pool的内部碎片也利用上了,进一步增加了内存利用率。

fixed_pool中的slot

虽然类比与linux中的slab,但是其数据结构确跟slab不太一样,它并没有像slab那样,对每个空闲小块都用链表维护,而是直接用位段来维护是否空闲的信息,这样更加节省内存,而且通过优化算法,其分配效率和slab几乎一样。

在fixed_pool的slot的头部,专门有一小块独立的数据,用于维护每个小块的空闲信息,每个块只暂用一比特位的信息,来判断这个块是否空闲,由于没有内存块都是固定大小的,所以比特位的位置定位,完全可以通过索引计算得到。

而且每次释放和分配,都会去缓存一个双字大小的位信息端,来预测下一次的分配,由于是双字大小,总共有32个比特位,所以每次缓存,最多可以预测邻近32个内存块。因此大部分情况下,预测成功率一直都是>98%的,分配效率都维持在O(1),比起large_pool的预测率还高很多,所以small_pool对large_pool的分流,还在一定程度上,进一步提高了内存分配效率。

而就算很倒霉,没预测成功,slot的顺序遍历来查找空闲快的算法,也相当高效,完全是高度优化的,下面就详细描述下。

slot的顺序遍历分配算法优化

我们这里主要用到了gcc的几个内置函数:

  1. __builtin_clz:计算32位整数前导0的个数
  2. __builtin_ctz:计算32位整数后置0的个数
  3. __builtin_clzll:计算64位整数前导0的个数
  4. __builtin_ctzll:计算64位整数后置0的个数

其实这四个类似,我们这里就拿第一说明好了,为什么要使用__builtin_clz呢?其实就是为了在一个32位端里面,快速查找某个空闲位的索引,这样就能快速定位某个空闲块的位置了。

比如有一个32位的位段信息整数:x,计算对应空闲位0的索引,主需要:`__builtin_clz(~x)`

简单吧,由于__builtin_clz这些内置函数,gcc用汇编针对不同平台高度优化过的,计算起来相当的快,那如果不是gcc的编译器怎么办呢?

没关系,我们可以自己用c实现个优化版本的,当然完全可以汇编继续优化,这里就先给个c的实现:

    static __tb_inline__ tb_size_t tb_bits_cl0_u32_be_inline(tb_uint32_t x)
    {
        // check
        tb_check_return_val(x, 32);

        // done
        tb_size_t n = 31;
        if (x & 0xffff0000) { n -= 16;  x >>= 16;   }
        if (x & 0xff00)     { n -= 8;   x >>= 8;    }
        if (x & 0xf0)       { n -= 4;   x >>= 4;    }
        if (x & 0xc)        { n -= 2;   x >>= 2;    }
        if (x & 0x2)        { n--;                  }
        return n;
    }

说白了,就是每次对半开,来减少判断次数,比起每次一位一位的枚举遍历,这种已经是相当高效了,更何况还有__builtin_clz呢。

接下来就看下具体的遍历过程:

  1. 按4/8字节对齐位段的起始地址
  2. 每次按4/8字节遍历位段数据,遍历过程利用cpu cache的大小,针对性的做循环展开,来优化性能。
  3. 通过判断 !(x + 1) 来快速过滤 0xffffffff 这些已经满了的位段,进一步提高遍历效率。
  4. 如果某个位段不是0xffffffff,则通过__builtin_clz(~x)计算实际的空闲块索引,并进行实际的分配。
  5. 最后如果这个的32位的位段没有被分配满,可以把它进行缓存,来为下次分配做预测。

string_pool

讲到这,TBOX的内存池管理模型,基本算是大概讲完了,这里就简单提下string_pool,即:字符串池

string_pool主要针对上层应用而言的,针对某些频繁使用小型字符串,并且重复率很高的模块,就可以通过string_pool进行优化,进一步减少内存使用,string_pool内部通过引用计数+哈希表维护,针对相同的字符串只保存一份。

例如可以用于cookies中字符串维护、http中header部分的字符串维护等等。。

----------

TBOX项目详情:http://www.oschina.net/p/tbox

TBOX项目源码:https://github.com/waruqi/tbox

TBOX项目文档:https://github.com/waruqi/tbox/wiki/%E7%9B%AE%E5%BD%95

内存池架构

时间: 2024-10-08 09:17:38

内存池架构的相关文章

内存池的实现(一)

1.引言 C/C++下内存管理是让几乎每一个程序员头疼的问题,分配足够的内存.追踪内存的分配.在不需要的时候释放内存——这个任务相当复杂.而直接使用系统调用malloc/free.new/delete进行内存分配和释放,有以下弊端: A.调用malloc/new,系统需要根据“最先匹配”.“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销 B.频繁使用时会产生大量内存碎片,从而降低程序运行效率 C.容易造成内存泄漏

基于C/S架构的3D对战网络游戏C++框架 _05搭建系统开发环境与Boost智能指针、内存池初步了解

本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): 1.实现基本通信框架,包括对游戏的需求分析.设计及开发环境和通信框架的搭建: 2.实现网络底层操作,包括创建线程池.序列化网络包等: 3.实战演练,实现类似于CS反恐精英的3D对战网络游戏: 技术要点:C++面向对象思想.网络编程.Qt界面开发.Qt控件知识.Boost智能指针.STL算法.STL.

内存池、进程池、线程池

首先介绍一个概念"池化技术 ".池化技术 一言以蔽之就是:提前保存大量的资源,以备不时之需以及重复使用. 池化技术应用广泛,如内存池,线程池,连接池等等.内存池相关的内容,建议看看Apache.Nginx等开源web服务器的内存池实现. 起因:由于在实际应用当中,分配内存.创建进程.线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作.           因此,当程序中需要频繁的进行内存申请释放,进程.线程创建销毁等操作时,通常会使用内存池.进程池.

详谈内存管理技术(二)、内存池

嗯,这篇讲可用的多线程内存池. 零.上期彩蛋:不要重载全局new 或许,是一次很不愉快的经历,所以在会有这么一个"认识".反正,大概就是:即使你足够聪明,也不要自作聪明:在这就是不要重载全局new,无论你有着怎样的目的和智商.因为: class XXX{ public: XXX* createInstance(); }; 这是一个不对称的接口:只告诉了我们如何创建一个[堆]对象,但是释放呢??! 很无奈,只能假设其使用全局默认的delete来删除(除此之外,没有其他选择):这时,我为了

InnoDB 存储引擎的线程与内存池

InnoDB 存储引擎的线程与内存池 InnoDB体系结构如下: 后台线程: 1.后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据: 2.另外,将以修改的数据文件刷新到磁盘文件: 3.同时,保证在数据库发生异常的情况下,InnoDB能恢复到正常运行状态. 内存池:InnoDB有多个内存块,这些内存块组成了一个大的内存池.这些内存块包括有:缓冲池(innodb_buffer_pool)和日志缓冲(log_buffer)以及额外内存池(innodb_addtional

内存池技术介绍(图文并茂,非常清楚)

看到一篇关于内存池技术的介绍文章,受益匪浅,转贴至此. 原贴地址:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html 6.1 自定义内存池性能优化的原理 如前所述,读者已经了解到"堆"和"栈"的区别.而在编程实践中,不可避免地要大量用到堆上的内存.例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从内存堆上分配或者释放一定的内存:在维护一个动态数组时,如果动态数组的

重写boost内存池

最近在写游戏服务器网络模块的时候,需要用到内存池.大量玩家通过tcp连接到服务器,通过大量的消息包与服务器进行交互.因此要给每个tcp分配收发两块缓冲区.那么这缓冲区多大呢?通常游戏操作的消息包都很小,大概几十字节.但是在玩家登录时或者卡牌游戏发战报(将整场战斗打完,生成一个消息包),包的大小可能达到30k或者更大,取决于游戏设定.这些缓冲区不可能使用glibc原始的new.delete来分配,这样可能会造成严重的内存碎片,并且效率也不高. 于是我们要使用内存池.并且是等长内存池,即每次分配的内

Innodb额外内存池的分配策略以及性能

Innodb额外内存池的分配策略以及性能 作者:明天会更好 QQ:715169549 备注:未经同意,严禁转载,谢谢合作. //内存池结构体 /** Data structure for a memory pool. The space is allocated using the buddy algorithm, where free list i contains areas of size 2 to power i. */ struct mem_pool_t{ byte* buf; /*!

[原创]loki库之内存池SmallObj

loki库之内存池SmallObj 介绍 loki库的内存池实现主要在文件smallobj中,顾名思义它的优势主要在小对象的分配与释放上,loki库是基于策略的方法实现的,简单的说就是把某个类通过模板参数传递给主类,比如某个对象的创建可以通过不同的创建策略进行创建,本文主要讲loki的大致实现. smallobj层次 loki.smallobj主要分四层: 应用层smallobject,重载了operator new 和operator delete,内存通过底层获取 内存分配smallobjA