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

  嗯,这篇讲可用的多线程内存池。

零、上期彩蛋:不要重载全局new

  或许,是一次很不愉快的经历,所以在会有这么一个“认识”。反正,大概就是:即使你足够聪明,也不要自作聪明;在这就是不要重载全局new,无论你有着怎样的目的和智商。因为:

class XXX{
public:
    XXX* createInstance();
};

  这是一个不对称的接口:只告诉了我们如何创建一个【堆】对象,但是释放呢??!  很无奈,只能假设其使用全局默认的delete来删除(除此之外,没有其他选择);这时,我为了某些目的,重载了全局delete(或许是为了监视、为了优化、为了...):

void operator delete(void* addr)
{
    ...
    //一些事情发生了
    ...
    std::free(addr);
}

  这是一种很自然的做法;但是,但是会崩的,在未来或现在的某日;其名为:堆错误。也就是崩在了堆上,原因也很简单:代码中有谁并不是使用std::malloc来分配内存的——比如说前面的那个【XXX】,我们谁也不知道它是分配在那个堆上面的:是默认的系统堆,还是VS-debug中的调试堆(此为坑点)。

  当然,我们可以足够小心;比如仔细考察每个对象的分配方式,对于非我们自己new出来的,给予特别的关怀。或者,也可以这样:

void operator delete(void* addr)
{
    ...
    //一些事情又发生了
    ...
    ::operator delete(add);
}

  我们最后使用了原来的全局释放方式;嗯,这是一种安全的方式。当然,这样的话,你可以自定义的部分,只有一种:监视。你不能够通过自定义的内存分配方式(比如将要讲的内存池),来优化。当然,如果没有那个【XXX】来搅局就好了;但,作为【全局】的操作,一旦修改了,你必须给予极其健壮的支持和保证。

  最后,因为是全局而隐式调用,你并不能够完全地控制,该操作什么时候是一定会被调用,什么时候却没有被调用(当你的代码作为lib/dll库被调用,而new重载没有被导出);假如,有同样一个聪明的人也重载了,那么当需要混用代码时(如lib/dll),你会觉得整个世界都不好了.......

  当然,这是个人见解;如果你执意用,建议使用宏的方式,去调用非全局版本的等价物。

一、什么是内存池?

  嗯,就是下面一坨代码:

struct BlockNode{
    BlockNode* next;
};

//创建一堆BlockNode
BlockNode* allocate(size_t index, size_t count);

//对外的分配内存接口
void* alloc(size_t size)
{
    size_t index = (size - 1)/8 + 1;
    BlockNode* data = freeList[index];
    if(data){
        freeList[index] = data->next;
        return data;
    }
    else{
        freeList[index] = allocate(index, /*一个合理的大数*/);
        return alloc(size);
    }
}

//对外的释放内存接口
void dealloc(void* addr, size_t size)
{
    //与alloc相反的操作(我懒)
}

  以上就是内存池本身的所有细节;至于allocate和dealloc,不难想象出来。

二、什么是多线程内存池?

  直白的翻译就是:支持多线程安全操作的内存池。当然,我们不能够通过加锁的方式来获得安全;否则,我们只会做的更糟(会比系统的慢....可能)。

  所以,这里我们需要用到下一篇将要讲到的技术之一:TLS(线程本地存储)。是的,我们将在每一个线程里创建这么一个内存池;这样,便不需要锁就能够自然地获得内存分配时的线程安全。那么,释放时呢?

//发生于线程A
void* addr = alloc(23);
...
...
//这里面发生很多很多的事情
...
...
dealloc(addr, 23);//这是哪里?是线程A吗?

  如果,你的代码支持多线程;那么,内存释放的时候,其绝对不会一定在原来分配时的线程!当然,我们可以将每段内存打上标记,来指明其出生在那个线程。嗯这是一个不错的失败的尝试;首先,其接下来的复杂度就会将你打垮(在不同的线程释放时,如何回到分配线程),其次,如果分配线程死了呢???(我相信聪明如斯的你,总会有办法的....)

  所以,我们需要一个合理且有效的模型:线程间内存池交换内存的模型——使用一个全局的共享内存池,然后各个线程内部的内存池,向其发起分配和释放的请求。这样,我们也就不在担心上面的问题了;我们可以通过这个全局池,来完成跨线程间的内存操作。

  当然,全局池需要加锁,这点毋庸置疑。为了减少加锁的消耗,我们可以缩短线程内部池的访问频率,比如:内部池的分配/释放频率与全局池的访问频率,比例在:10000:1,或更高。这样,通过均摊,最后加锁的消耗,几乎完全没有了(即使消耗1ms,现在均摊后也只有0.1us)。

  所以,现在的挑战就是:如何维持这个均摊比例?(因为,在畸形的分配中,会变成1:1甚至更低)

三、我们需要性能!

  没有更好的性能,那我们还造毛??所以,这里,最大的目的是保持住,我们预定的均摊比例。或者说,控制住线程内部池向全局池的访问频率。

  对于向全局池的分配申请策略;我们可以使用一个足够大的申请值:比如100000,或者10MB。

BlockNode* ThreadMemoryPool::allocate(size_t index, size_t size)
{
    //我们直接申请一个够大的
    BlockNode* result = globalPool.allocate(100000*8*(index + 1));
    return result;
}

  那么,在什么时候释放呢?比如,现在线程A申请了10MB的内存,那么在怎样的情况下才释放?释放什么?释放多少?这一直以来是一个盲区(对于我来说,数年来都没完全解决)。当然,聪明的你或许,马上就有了各种的方案。

  我们,为什么要释放???因为,其他线程没有内存可用了;而你,线程A正持有着100TB的内存。

四、我们需要均衡...

  我们不能够容忍,任何一个线程持有超过10MB的可用内存!!!所以,有了如下的方案:

void ThreadMemoryPool::dealloc(void* addr, size_t size)
{
    .....
    //我们就不要在意释放过程了
    ...
    if(listSize[index] > 1024*1024*10){
        globalPool.deallocte(freeList[index], listsize[index]);
    }
}

  在线程内部池的释放操作时,检测当前池是否有超过10MB的内存;如果有,那么我们就堕掉它!这时,便会有一个畸形的分配情形:

//线程A向全局池申请10MB
threadMemoryPool.allocate(index, 10MB);
//并内部消耗一个单位(当前持有10MB - 一个单位)
threadMemoryPool.alloc(...);
...
//线程A内部释放一个单位(当前持有10MB)
threadMemoryPool.dealloc(...);
...
//线程A内部释放一个单位(当前持有10MB + 一个单位 > 10MB)
threadMemoryPool.dealloc(...);

  总共进行了3次分配/释放操作,便向全局池返回了所申请的内存。这时均摊比例为3:2(内部操作3次,全局操作2次:申请+释放),其次全局池本身的任何操作都是消耗巨大的(比如那10MB内存是从何而来的,从系统),那么这个实际的比例会变成1:100甚至更低。

  当然,我们可以错开分配和释放的全局操作阈值,比如:分配1MB、释放10MB。这样,我们就有了10-1=9MB的余地,不会发生上面的情形。(当然,反过来绝对不行:分配10MB、释放1MB,可以自己想象。)

五、我们还需要什么?

  如果分配值和释放阈值不相等,那么,我们就有可能永远也没有机会回收小于释放阈值,但大于等于分配值的那部分内存。在最常见的情况下:线程A的所有分配释放操作,都在本地进行。

//线程A
for(i = 0 : 10000){
    data[i] = threadMemoryPool.alloc(size);
}
....
...
//线程A
for(i = 0 : 10000){
    threadMemoryPool.dealloc(data[i], size);
}
//没了

  在这之后在没有任何操作,那么,直到线程死亡;我们都不可能回收这段可用的内存!

  所以,我们需要分配值,足够的合理;也需要释放阈值足够的小,且能够维持均摊比例。当然,我们可以办到!我们只需要完全隔离当前内部池的持有的【分配】内存值,和【已释放】内存值。

void ThreadMemoryPool::dealloc(void* addr, size_t size)
{
    .....
    //我们依旧不要在意释放过程
    ...
    deadSize[index] += index*8;//使用了一个额外的死亡内存值
    if(deadSize[index] > 1024*1024*10){
        globalPool.deallocate(freeList[index], listsize[index]);
    }
}

  如此简单,却困扰了我如此之久.....这时,我们就可以随意地操作分配值和释放阈值;以维持一个我们所认为的合理的均摊比例。

六、我们还需要什么??

  可能有注意到了,我们维持了一种假象:我们的内存池可以回收。

  1、我们不能够向系统释放我们可能不会再用到的内存。(这对没有使用我们的内存池的部分代码和系统本身而言,就是内存泄漏!)

  2、可能大家有注意到了这个【index】,每个内部池,我们维护了数个不同大小节点链。而这些不同大小的链之间,我们是没有办法重复使用的。(这是内存池内部的泄露)

  是的,我们可能正在制造最大规模的内存泄漏;还是我们以一种不可避免的方式造成的(我们要用性能更高的内存分配)。从理论上来说,这是不可避免的;我们唯一能够做到的是,尽量避免上面的情况,演化成最糟糕的局面。

  所以,我们可能需要更加精细的模型了;我们要改造【全局池】!!使其能够支持一定程度上的:内存整理。

  所以,我们需要做一下两个改进:

  1、我们需要保存每一块从系统分配得到的大内存的地址(以可以释放)。

  2、我们需要一种算法,能够整理我们所有不用的内存;让其恢复到从系统分配时的状态(完整的大块内存)。

  这时,我们便可以完成之前的2个目标:向系统释放、节点链间的复用(通过释放给系统,而后再次从系统获取)。在我个人的内存池中是实现了类似的功能,所以,我相信,做到这点并不困难。

七、我们还需要什么???

  重要的事情说3遍......

  回到最开始的问题:为什么我们需要内存池?我们需要性能。所以,还有那些地方,值得我们关注;以获得更高的性能?

  有,还有很多!之前的向全局池的释放部分,有一个关键的细节,没有提到:我们释放的内存存在哪里?又怎么复用?有两种方案:

  1、如同线程内部池一样,维护一个链,将释放的部分,加入到链中(需要O(n));在分配时,从链中获取(同样需要O(n))。

  2、将该节点链整个打包,作为一个单独的链保存;在向内部池分配时,直接返回该链。(只有O(1))

  直观地,我们会选择第二种方案(即使,最初我们可能只会想到第一种)。但是,一旦使用第二种,我们将不能够控制每次线程所申请的内存大小:我们只能够返回一个可用的节点链,而并不能够保证是否是其所期望的大小。(我们最多只能够尽可能保证返回大于其期望值;而不能保证和期望值一致;否则会破坏O(1)的复杂度)

  嗯,我们丧失了一小部分控制均摊比例的能力。但,只要我们足够小心安排释放阈值,是不会发生什么畸形的情形。

  其次,分配时,我们可以再小心一些:不是直接申请1MB(可能只会用到很小一部分),而是按照某种增长策略来申请(比如:100、200、400....)。在能够维持均摊比例的前提下,我们可以做很多想做的事情。

  总结一下:我们需要足够大的分配值和释放阈值,以维持合理的均摊比例;而,我们又想要保留足够小的内存,以避免任何可能的内存泄漏。  嗯,这正是矛盾之处,也是我们所追求的。而剩下的,就只有一个:权衡。

PS:使用内存池后,一旦发生内存越界;其后果将是灾难性的,对于调试。

时间: 2024-08-03 15:38:50

详谈内存管理技术(二)、内存池的相关文章

垃圾回收GC:.Net自己主动内存管理 上(二)内存算法

垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(三)终结器 前言 .Net下的GC全然攻克了开发人员跟踪内存使用以及控制释放内存的窘态.然而,你或午想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包括很具体的内在算法描写叙述.同一时候.还将讨论GC的内存清理流程及什么时清理,怎么样强制清理. 内

垃圾回收GC:.Net自动内存管理 上(二)内存算法

垃圾回收GC:.Net自动内存管理 上(二)内存算法 垃圾回收GC:.Net自动内存管理 上(一)内存分配 垃圾回收GC:.Net自动内存管理 上(二)内存算法 前言 .Net下的GC完全解决了开发者跟踪内存使用以及控制释放内存的窘态.然而,你或午想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包含非常详细的内在算法描述.同时,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理. 内存算法 GC检测用于查看堆中是否有对象不再被程序使用.如果这样的对象存在,这

垃圾回收GC:.Net自己主动内存管理 上(一)内存分配

垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(三)终结器 前言 .Net下的GC全然攻克了开发人员跟踪内存使用以及控制释放内存的窘态.然而,你也许想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包括很具体的内在算法描写叙述. 同一时候,还将讨论GC的内存清理流程及什么时清理.怎么样强制清理.

Objective-C----MRC内存管理 、 自动释放池 、 面向对象三大特性及封装 、 继承 、 组合与聚合

1 MRC练习 1.1 问题 引用计数是Objective-C语言采用的一种内存管理技术,当一个对象被创建在堆上后,该对象的引用计数就自动设置为1,如果在其它对象中的对象成员需要持有这个对象时,则该对象的引用计数被加上1,此时如果该对象被释放,内存管理程序将首先把该对象的引用计数减1,然后判断该对象的引用计数是否为0,由于其它对象在持有该对象时将引用计数加了1,所以此时该对象的引用计数减1后不为0,则内存管理程序将不会释放该对象.直到持有该对象的其它对象也被释放时,该对象的引用计数再次减1,变为

objective-C 的内存管理之-自动释放池(autorelease pool)

如果一个对象的生命周期显而易见,很容易就知道什么时候该new一个对象,什么时候不再需要使用,这种情况下,直接用手动的retain和release来判定其生死足矣.但是有些时候,想知道某个对象在什么时候不再使用并不那么容易.如果下面的代码,看上去非常简单: Sample.h类接口部分 #import @interface Sample : NSObject { } -(NSString*) toString; @end Sample.m 类实现部分 #import "Sample.h"

iOS开发ARC内存管理技术要点

本文来源于我个人的ARC学习笔记,旨在通过简明扼要的方式总结出iOS开发中ARC(Automatic Reference Counting,自动引用计数)内存管理技术的要点,所以不会涉及全部细节.这篇文章不是一篇标准的ARC使用教程,并假定读者已经对ARC有了一定了解和使用经验.详细的关于ARC的信息请参见苹果的官方文档与网上的其他教程:) 本文的主要内容: ARC的本质 ARC的开启与关闭 ARC的修饰符 ARC与Block ARC与Toll-Free Bridging 技术交流新QQ群:41

(转)iOS开发ARC内存管理技术要点

转自:http://www.cnblogs.com/flyFreeZn/p/4264220.html 本文来源于我个人的ARC学习笔记,旨在通过简明扼要的方式总结出iOS开发中ARC(Automatic Reference Counting,自动引用计数)内存管理技术的要点,所以不会涉及全部细节.这篇文章不是一篇标准的ARC使用教程,并假定读者已经对ARC有了一定了解和使用经验.详细的关于ARC的信息请参见苹果的官方文档与网上的其他教程:) 本文的主要内容: ARC的本质 ARC的开启与关闭 A

黑马程序员----内存管理之二《多对象的内存管理》

内存管理之二——<多对象的内存管理> 1.多对象的内存管理方式: 只要有人使用了这个对象,这个对象就不能被销毁: 只要你想使用这个对象,就让这个对象的引用计数器的值+1(让对象做一次retain操作): 当你不再使用这个对象,就让这个的对象的引用计数器的值-1(让对象做一次release操作): 谁alloc,谁就release: 谁retain,谁就release: 2.内存管理的代码规范: 只要调用了alloc必须有release/autorelease set方法的代码规范: 1.基本数

手动内存管理和自动释放池

手动内存管理 在进行内存管理的时候要注意内存管理的准则:谁开辟内存,谁释放内存(谁污染的谁治理) .开辟内存之后,对象的引用计数为1,只有继承自NSObject的对象才有内促管理的概念, 当对象引用计数为0的时候对象的内存会被清理. 下列关键字会开辟内存,对象引用计数器+1 alloc new copy mutableCopy 下列是内存管理的相关方法. retain :保留对象,对象的引用计数器+1. release : 释放对象,对象引用计数器-1. retainCount : 获取对象当前

属性与内存管理(属性与内存管理都是相互关联的)

<span style="font-size:18px;"> 属性与内存管理(属性与内存管理都是相互关联的)第一部分 一,属性: 属性是OC2.0之后出来的新语法,用来代替setter和getter方法,使用属性可以快速创建setter以及getter方法的声明,setter和getter方法的实现,另外添加了对实例变量操作的安全处理(其安全是通过内存管理实现的) setter 方法作用:为单一的实例变量重新赋值, 规范: (- 号方法)无返回值, 名字以set开头后面加上