宜兴市城乡建设局网站,网站建设与设计学了做什么的,自建房设计,国外做gif的网站分配器是STL中的六大部件之一#xff0c;是各大容器能正常运作的关键#xff0c;但是对于用户而言确是透明的#xff0c;它似乎更像是一个幕后英雄#xff0c;永远也不会走到舞台上来#xff0c;观众几乎看不到它的身影#xff0c;但是它又如此的重要。作为用户#xff…分配器是STL中的六大部件之一是各大容器能正常运作的关键但是对于用户而言确是透明的它似乎更像是一个幕后英雄永远也不会走到舞台上来观众几乎看不到它的身影但是它又如此的重要。作为用户你几乎不用关心它的底层是怎么实现的甚至也很少有能使用到它的机会。这里简单聊一下我对它的认识。
正常情况下我们如何取得一块内存
malloc能够帮你获取一块内存并返回这块内存的首地址new operator的底层也是用malloc实现只是相较于malloc它不光会给你一块内存还会帮你自动初始化这块内存即调用对应对象的构造函数operator new是C获取内存的方式注意new operator和operator new是两种不同的东西它也是调用了malloc来实现获取内存只是封装了一些东西增加了一些异常机制。而VCBC,GNU C等等编译器厂商最初提供的allocate的底层也是通过调用operator new实现的。
所以你发现没有殊途同归大家几乎都是通过调用malloc来实现获取内存这一操作的。而malloc根据机器的不同去调用操作系统底层提供的api接口去获得真正的内存。 但是如果你申请一块10个字节的内存malloc给你的内存的大小却并不真的是10个字节。这里面你能用的内存有10个字节没错但是还会有一些额外的开销在里面它们会在这块内存的两头加上所谓的“cookie”来处理一些其他事情就比如你买东西收到的其实并不是东西本身还会有快递盒快递袋快递单等额外的东西帮助你自己买的东西到达你的手上。这些东西对你来说可能没用但确确实实是不可避免的开销。
从这个角度而言如果一个容器里放的东西很小但是元素的数量又很多假如容器里你想放一个2个字节的short类型的元素而这样的容器的数量有100w个这样轮到这个容器底层的分配器去帮你开辟内存的时候由于cookie的存在申请一个这样的容器你可能会得到10个字节其中2个字节是你想要的内存其余8个字节是额外的开销这样下来100w个容器本来只需要200w个字节现在你却不得不得到1000w个字节性能实在是不这么高。
这里并不是说cookie很消耗内存才造成的你的性能不理想而是存在一个比例问题。如果你的容器里放的元素的内存很大那么这额外的开销就显得很渺小完全可以接受但是更多的情况下容器里放的元素其实并没有那么大这也就显得性能不理想。
如何解决这种问题
SGI STL中给出的一个思路是先放很多的分配器但是每个分配器只负责某种固定大小的内存的申请等到容器真的申请内存的时候对应大小的分配器会去申请一块很大的内存然而自己将这些内存切割成固定大小的内存再返回给使用者某一块固定大小的内存的首地址。
使用这种策略便不再会对额外开销产生困扰因为真正的申请内存只有刚开始的那次所以只会得到一次cookie得到的这块大的内存被切割成固定大小时每块内存上并不会带cookie也就不会有额外开销。
STL提供了两层内存分配器
当分配大于128KB时直接采用new operator也就是一级内存分配器当分配小于128KB时采用二级内存分配器也就是内存池具体是通过自由链表实现的。参考文章。
为什么要分两级呢主要是为了减少内存碎片减少malloc的次数。所以内存池就相当于应用代码和系统调用申请内存的中间件。
第一层内存分配器
operator new
operator new可以被重载
重载时返回类型必须声明为void*重载时第一个参数类型必须为分配空间的大小字节类型为size_t当然也可以带其它参数
如 class Foo{public:static void *operator new (size_t size){Foo *p (Foo*)malloc(size);return p;}static void operator delete(void *p, size_t size){free(p);}};这里只是简单的用malloc和free来实现后续可以用内存池。
C还提供了全局的operator new和operator delete可以通过::operator new和::operator delete来访问全局操作符。
placement new
operator new实现了new表达式的第一步即分配内存那么谁来调用构造函数呢就是placement new它的语法是 Object * p new (address) ClassConstruct(...)这里要求address是void*并且placement new被定义在#includenew头文件中。同样的也可以重载它也提供了全局下的placement new通过::访问。
举个例子 int* ptr ::operator new(sizeof(int));::new ((void*)ptr) int(); 其实本质上placement new也是operator new的一个重载版本只不过这个重载版本我们常用来调用构造函数。如 class Foo{public://一般的 operator new 重载void* operator new(size_t size){ return malloc(size); }//标准库已经提供的 placement new() 的重载形式void* operator new(size_t size, void* start){ dosomething;return start; }}; 那对new operator和delete operator拆分为两部分功能有什么好处呢使用new表达式在分配内存时需要在堆中查找足够大的剩余空间显然这个操作速度是很慢的而且有可能出现无法分配内存的异常空间不够。
placement new就可以解决这个问题。在一个预先准备好了的内存缓冲区上进行构造函数不需要查找内存内存分配的时间是常数。而且不会出现在程序运行中途出现内存不足的异常。所以placement new非常适合那些对时间要求比较高长时间运行不希望被打断的应用程序。
总之new造成的反复分配内存很浪费所以placement new直接固定内存在这个固定内存上反复构造和析构但不再反复分配内存和释放内存。
note如果采用placement new可别忘记在operator delete前调用析构函数除非元素的析构函数是无关紧要的。
allocator
STL的allocator负责对容器的分配内存、释放内存、调用元素的构造函数、调用元素的析构函数。
其实理解了上面的内容STL的allocator也就很简单。
对外提供四大方法
allocator方法即调用operator newconstruct方法即调用placement newdeallocator方法即调用operator deletedestroy方法即调用~T()
note不是所有类都需要调用destroy当类的析构函数是无关紧要的时候我们可以不进行析构那么什么样的是无关紧要的?可以用std::is_trivially_destructible类模板判断。具体来说
使用隐式定义的析构函数即没有定义自己析构函数析构函数不是虚函数其基类与非静态成员也是可trivially析构
其实会发现basic_string在释放内存前没有调用析构函数正是因为basic_string严格要求元素类的析构函数是无关紧要的。而vector等则需要在释放内存前调用析构函数。
第二层内存分配器
先申请一大块内存然后切割成小块由单向链表串起来内存池包括十六条链表分别负责不同大小的内存大小比如第7个负责256字节的区块以8的倍速增长。
至于STL内存池设计的好坏也颇有争议C 标准库中的allocator是多余的allocator作为模板参数这就导致不同allocator是不同的type。