zoukankan      html  css  js  c++  java
  • 详谈内存管理技术

    先自问一个问题:C++有几种new?

      我一直以为是两种:operator new 和 placement new。刚刚查了下,原来是3种:还有一个是new operator。而且,我还弄错了一个...但是,无论如何,我们能够改变的只有两个:

      1、operator new,分配内存。

      2、placement new,构造对象。

      而剩下的那个new operator很直白:负责调用上面两个new。也就是其仅仅是语法层次上的东西,用来产生operator new和placement new的语义。

      这是一个不错的开始,因为我要讲的“内存管理”,其实是上面所提到的所有:内存分配和对象构造。当然,还有一个对称的内存释放和对象析构,这些自然也会有,只是相对来说前者更加直白和重要。或者,我可以换一个题目:内存与对象管理技术。

      这将是一个,或者说数个相当大的话题;你不信,我可以给你一个列表(别眨眼):

      1、内存池,特别是一个可用的内存池,构建一个是相当困难的!其需要达到数个极其苛刻的要求:强悍的性能(否则我们还需要它?)、并发且线程安全(这点和上一点几乎是矛盾的存在)、高可用性(意味着其有着合理的回收机制,否则可能会浪费大量内存;而且能够处理相当畸形的环境,比如单一线程分配,单一线程回收)、可调试(简直不可能!!!)。

      2、线程模型,任何涉及并发的技术,都需要一个强大的线程模型,以提供各种额外的支持;为什么会提到它?内存分配不需要并发?不要开玩笑。没有TLS(线程本地存储),一切的并发都只能吃翔;而这却需要一个新的线程模型的支撑(系统的TLS,我个人而言,并不信任)。

      3、对象构造管理,这其实是一个很庞大的工程;其处于整个C++的最底层,也就意味着,我需要足够的努力才能够去完成我想要达到的目标——对于不同的类型,我能够做出正确且最好的选择,来构造它。

      4、类型系统,这是作为第三条的支撑性系统;如何达到上一条的目标?我需要一个类型系统,来提供足够的信息,以完成选择(需要注意的是:对象构造本身和类型系统,是独立的;只是恰巧,类型系统可以帮助对象获得最优的构造方式)。

      5、垃圾收集器,这是一个C++“永远的痛”,特别是当我用C++创造了一门有垃圾收集的脚本语言时,深感如此;所以,我陆续构建了两套垃圾收集系统,以C++可用的方式,当然会有各种限制。这里,提到的原因很简单:我们都梦想着能够用上或者创造一个属于C++的垃圾收集器!!!   特别是,在我做完上面的所有事情后。

      看到没有,上面任何一个部分,都是一个庞大的话题;所幸的是,这些我都完整地做了一遍(或多遍),我会慢慢地,详细到来。

      PS:写这部分,我是很高兴和激动的,因为,这正是我所最擅长的领域;而身边却没有一个可交流的人。

      一、内存池

      所有的STL容器,都有着一个不可忽视,但一直被无视的部分:内存适配器;其实,也就是一个预留的接口,某天我们能够使用更快的内存分配,来替换默认的malloc/free。毫无疑问,这就是内存池。许多书籍都提到并给出了详细的代码,来教会我们如何构建一个性能秒杀系统分配的单线程内存池;而在并发上,也就是支持多线程的内存池上,支支吾吾,止步于加锁...最后发现和系统分配并没有什么太大区别,毕竟系统分配也是加锁了的。

      当然,要构建一个可用的高性能多线程内存池,并不困难;国内的还有人,特意写了一本书,来讲解他自己命名的多线程内存池(当我发现他所实现的高端货,和我自己折腾出来的一模一样时,便没看了:因为,造出来所需要的努力,其实远比写一本书少的多;当然,后者钱要多得多)。其思路很简单:

      1、每一个线程一个内存池;通过TLS实现。

      2、一个所有线程共用的全局内存池;用来给线程内部的池,提供分配和回收服务;当然,这一级访问需要锁~

      3、线程内部池的分配回收策略(向共用池),共用池整体的分配回收策略(向系统)。

      之所以“简单”,是前两个部分,是很直白和自然的解决方案!只要你记住一点:我要并发,我要性能——请问TLS。而需要思考和抉择的是第三点;之所以需要“思考”,因为没有超大规模数据的支撑的前提下,任何的策略都只是我们的臆想而已,任何可能的畸形分配情形,都会使我们所“猜测”出的策略无效;而“抉择”,是在我们了解到了足够信息后,必然需要面对的(否则,系统分配,还会有存在的理由?)。

      所以,在面对一个“可用”的内存池时,我们需要足够的谨慎;而,我所要讲的就是这些“谨慎”。

      二、线程模型

      或许,我过于依赖TLS,因为我足够愚蠢:除此之外,我别无他法。

      在我所提到的内存池中所使用的关键技术之一便是TLS;同样的在垃圾收集器部分,也将大量运用到TLS技术。所以,是的我们无法避开线程模型:因为,TLS需要一些额外的支持,恰巧C++没有给我们(所以,我只能自己去造)。

      为什么,我不用Windows的TLS?主要原因是,我不信任它(属于个人直觉);另外的一部分,则是我的库中是尽量避免任何第三方依赖!包括STL,我也不会用到(所以,我自己写了一套;还有更深层次的原因:我不喜欢STL的现有部分接口方式)。

      什么是线程模型?我不知道。看到那么多人和大牛都在说,所以我也用了这个关键字。在我的库里就是:

      1、重新定义的一整套接口,用来提供完整的线程服务;我使用了类似Java的方式,只要继承了IThread,然后实现run,便可开启另外一个线程:

    复制代码
    class MyThread: public IThread{
        void run(){
            ...
        }
    };
    MyThread thread;
    thread.execute();//启动线程
    复制代码

      2、一个线程服务类,用于提供各种线程相关服务;其中最关键的服务就是TLS;还有一个稍微有一点霸气的功能:stop the world。也就是,它管理着当前所有运行着的线程,包括主线程。

      3、线程工具类,TaskRunner、Thread、Timer、ThreadPool提供了各种不同的线程支持。

      4、并发工具类,进行了抽象化后的锁和事件;当然重要的是锁:内核锁、临界区、读写锁。

      所有的这些都是简单而无聊的;但作为一个整体来提供,则需要付出许多额外的努力。

      三、对象构造

      用“构造”这个词来限定这个领域;很不正确,毕竟完整的是:无参数构造(默认构造)、有参数构造、复制构造、对象析构、对象移动。一共5个部分需要我们关心,如果你还不明白,看看下面的代码:

    复制代码
    Something* addr = (Something*)std::malloc(sizeof(Something));
    
    //1、无参数构造
    new (addr)Something();//调用Something();
    
    //2、有参数构造
    new (addr)Something(12);//调用Something(int);
    
    //3、复制构造
    Something value;
    new (addr)Something(value);//调用Something(const Something&);
    
    //4、对象析构
    addr->~Something();//调用~Something();
    
    //5、对象移动
    Something* other= (Something*)std::malloc(sizeof(Something));
    new (other)Something(*addr);//将addr处的对象移到other
    addr->~Something();//析构掉addr的对象;只保留other处的,保证“移动”语义
    复制代码

      上面都是最保守的方式;对于基本类型,以及自定义POD类型,我们并不需要这样“复杂”的调用:

    复制代码
    int* addr = (int*)std::malloc(sizeof(int));
    
    //1、无参数构造
    //do nothing
    
    //2、有参数构造
    *addr = 12;
    
    //3、复制构造
    int value = 12;
    *addr = value
    //或
    memcpy(addr, &value, sizeof(int));
    
    //4、对象析构
    //do nothing
    
    //5、对象移动
    int* other= (int*)std::malloc(sizeof(int));
    memcpy(other, addr, sizeof(int));
    复制代码

      这里似乎并没有太大的区别;在优化级别较高的情况下,自定义的POD也会生成第二种代码;但,但是,在构造一个数组时,这种区别将是巨大的:

    复制代码
    //第一种:默认的方式
    for(size_t i = 0; i != size; ++i){
        new (data + i)Something();
    }
    
    //第二种:最佳的方式
    //do nothing
    复制代码

      第一种方式,在优化时依然会产生代码,当然是足够“优化”的代码;但在最佳的方式下,我们可以不产生任何代码。因为,编译器并不是足够聪明,许多自定义的POD,它并不能够以最佳的方式来生成代码;而,身为人类的我们,可以!

      当然,是通过类型系统。

      四、类型系统

      其实第三部分的“对象构造”本身是相当“精简”的,只是为了到达目的;我们需要额外的努力,而这“努力”,我通过类型系统,来具现化。

      很简单的方式:我来告诉编译器,生成怎样的代码!

      1、对于每个类型(特别是可以优化的类型),我们定义它的类型信息:是否是平凡构造/复制/析构,是否是内存可移动的等。

      2、通过模板元编程,萃取以上信息;得到更多丰富的类型信息。

      3、使用上述原始信息和萃取的信息;来完成我们所有想做的事情;当然,也是通过模板元编程。

      本质上,我们是在构建一个庞大的类型数据库;也就是,需要手动定义许多,编译器并不知道的信息。

      这部分,复杂度上是很简单的;但,又有但是,你会模板元编程么?!学习这一技术,本身就是一个痛苦且漫长的过程;更为关键的是,其编程方式,不同于在C++中的其他所有,你将必须接受其函数式的思维方式;而这,很痛苦。

      五、垃圾收集器

      这是一个令人兴奋的话题,在C++的世界里;但我们,要知道相当一部分有GC的语言,其本身就是用C++写的(JAVA、C#等);这真是一个悲伤的话题。

      所以,我通过不怎么努力的努力;在我的库里,完成里两套C++本身可用的垃圾收集器。当然,都有着一些限制;第一个版本,只能够在独立的线程里运行,线程间的交互需要额外的机制(并非不可以);第二个版本,则是一个更加可用的收集器,其没有线程限制。

      当然,这两个收集器,都是给我的脚本语言使用的;因为,在动态语言里,环形引用是常见的景象;基于引用计数的技术,在这一点上,毫无作为。

      在这个部分,我并不是教大家如何构造出一个C++可用或不可用的垃圾收集器;而是,带大家看看我所知道的,那些垃圾收集方案,其能够有着怎样的作为;关于这些,我有着一个长长的列表,这里我就不详细展开。但,大致有以下内容:

      1、C++可用的,简单的各种回收方式:shared_ptr/scoped_ptr、基于链表和栈对象(RAII)的收集器、侵入式智能指针。

      2、完整的大型收集器:标记清扫、标记缩并、节点复制、分代式收集等等;不,我只会讲我所熟悉的,如:标记清扫、节点复制、分代式。

      3、C++的世界中,我们需要垃圾收集器?如何?

      嗯,大致上就这些;但这部分将会是在相当久远的时候,才会与大家见面,那时,或许会有不同。

      总结下,内存管理,的确是一个相当大的话题。

  • 相关阅读:
    一些业内有名的网站收集
    WCF重载
    FCKEditor fckconfig.js配置,添加字体和大小 附:中文字体乱码问题解决
    查询第几条到第几条的数据的SQL语句
    SPOJ 9939 Eliminate the Conflict
    UVA 10534 Wavio Sequence
    HDU 3474 Necklace
    POJ 2823 Sliding Window
    UVA 437 The Tower of Babylon
    UVA 825 Walking on the Safe Side
  • 原文地址:https://www.cnblogs.com/chenliyang/p/6543648.html
Copyright © 2011-2022 走看看