__mt_alloc源码分析(1)
本文从源代码级别研究mt allocator的内部实现,使用GCC 4.1.2版本的源码,主要源文件为库文件<ext/mt_allocator.h>和GCC源码中的“libstdc++-v3/src/ mt_allocator.cc”。
假定读者对mt allocator的原理已有一定的了解。
class __mt_alloc
为避免一开始就深入到复杂的内存分配机制里,我采用从上往下的研究方法。最顶层自然是提供给用户使用的__mt_alloc类:
611 template<typename _Tp,
612 typename _Poolp = __common_pool_policy<__pool, __thread_default> >
613 class __mt_alloc : public __mt_alloc_base<_Tp>
每行代码前面都加注了所在的行号,方便查阅。如果没有特别注明文件名,所有的代码都摘自<ext/mt_allocator.h>。
__mt_alloc有2个模板参数,_Tp是管理的对象类型,_Poolp是所用的内存池策略。stl里实现的可用与__mt_alloc的内存池策略有2个,__common_pool_policy和__per_type_pool_policy,前者对所有的_Tp类型一视同仁,后者会区别对待每个_Tp类型,这在后面详细研究。__mt_alloc的基类是__mt_alloc_base。
根据stl标准,每个allocator都要定义几个固定的typedef类型:
616 typedef size_t size_type;
617 typedef ptrdiff_t difference_type;
618 typedef _Tp* pointer;
619 typedef const _Tp* const_pointer;
620 typedef _Tp& reference;
621 typedef const _Tp& const_reference;
622 typedef _Tp value_type;
而__mt_alloc还多出来2个:
623 typedef _Poolp __policy_type;
624 typedef typename _Poolp::pool_type __pool_type;
__policy_type是__mt_alloc采用的内存池策略,而__pool_type则是内存池的类型。
rebind嵌套类也是stl标准规定的allocator成员之一,目的在于把对T1的allocator重绑定到T2上。由于__mt_alloc有2个模板参数,所以它的rebind类也有2个参数。只要你能看懂模板参数的推导,就能看懂rebind的定义。
__mt_alloc的公有成员函数接口相比stl标准的allocator也多出2个,
648 const __pool_base::_Tune
649 _M_get_options()
650 {
651 // Return a copy, not a reference, for external consumption.
652 return __policy_type::_S_get_pool()._M_get_options();
653 }
654
655 void
656 _M_set_options(__pool_base::_Tune __t)
657 { __policy_type::_S_get_pool()._M_set_options(__t); }
658 };
分别是得到__mt_alloc的配置参数和设置配置参数。注意设置函数_M_set_options必须在任何内存分配动作之前调用,因为第一次调用allocate()的时候__mt_alloc会进行一次参数初始化,如果你在初始化之后再_M_set_options,不会有任何效果。
所有成员函数里,最重要的就是allocate和deallocate。
allocate
660 template<typename _Tp, typename _Poolp>
661 typename __mt_alloc<_Tp, _Poolp>::pointer
662 __mt_alloc<_Tp, _Poolp>::
663 allocate(size_type __n, const void*)
这是allocate函数的原型,参数__n是要分配的对象的个数。
664 {
665 if (__builtin_expect(__n > this->max_size(), false))
666 std::__throw_bad_alloc();
__builtin_expect是gcc的内建函数,原型是:long __builtin_expect(long exp, long c)。其第一个参数exp为一个整型表达式,这个内建函数的返回值也是这个exp,而c为一个编译期常量。这个函数的语义是:你期望exp表达式的值等于常量c,从而GCC为你优化程序,将符合这个条件的分支放在合适的地方。
max_size()函数返回“size_t(-1) / sizeof(_Tp)”,这个值在正常的运行情况下显然比__n大,所以这里用__builtin_expect进行一些优化。
668 __policy_type::_S_initialize_once();
这里就是__mt_alloc初始化配置参数的地方。从函数名字可以看出来,初始化工作只会进行一次。 _S_initialize_once属于__policy_type,在后面进行研究。
670 // Requests larger than _M_max_bytes are handled by operator
671 // new/delete directly.
672 __pool_type& __pool = __policy_type::_S_get_pool();
673 const size_t __bytes = __n * sizeof(_Tp);
674 if (__pool._M_check_threshold(__bytes))
675 {
676 void* __ret = ::operator new(__bytes);
677 return static_cast<_Tp*>(__ret);
678 }
得到内存池对象,计算实际需要的内存字节数__bytes,然后检查__bytes是否大于一个阀值。__mt_alloc默认对于128字节以上的内存块,直接调用new和delete进行分配和释放。这个阀值是可以配置的。
680 // Round up to power of 2 and figure out which bin to use.
681 const size_t __which = __pool._M_get_binmap(__bytes);
682 const size_t __thread_id = __pool._M_get_thread_id();
如果__bytes在阀值以内,那么首先找出它对应的bin索引。由于__mt_alloc只处理2的指数字节的内存块,所以对于其他的数值也要上调至2的指数,然后交给对应的bin来处理。_M_get_binmap使用一个长度为128(可配置)的数组,把字节数映射到bin的索引。__mt_alloc处理的最小的内存块默认为8字节(可配置),于是__mt_alloc总共有5个bin,分别对应8,16,32,64,128字节。这里__which的取值为0到4,取决于__bytes的大小。
为了在多线程之间管理内存,__mt_alloc给每个线程分配了一个线程id。不同于OS分配的线程id,__mt_alloc分配的线程id取值从1到4096(默认,可以配置),而且可以回收和重新分配给新线程,id值0保留给全局使用,不分配给任何线程。_M_get_thread_id函数负责给当前线程分配新的线程id,如果已有,那么直接返回这个id。
684 // Find out if we have blocks on our freelist. If so, go ahead
685 // and use them directly without having to lock anything.
686 char* __c;
687 typedef typename __pool_type::_Bin_record _Bin_record;
688 const _Bin_record& __bin = __pool._M_get_bin(__which);
由前面算出的__which,得到bin对象引用。
689 if (__bin._M_first[__thread_id])
每个bin都要维护4097个空闲块链表,其中__thread_id=0对应的是全局的空闲块链表,其他__thread_id对应线程自己的空闲块链表。这些链表的首地址存放在_M_first数组里,自然它的长度是4097。线程向__mt_alloc申请内存块时,首先检查自己的空闲块链表是否有元素,这通过判断_M_first[__thread_id]是否为NULL来完成。
690 {
691 // Already reserved.
692 typedef typename __pool_type::_Block_record _Block_record;
693 _Block_record* __block = __bin._M_first[__thread_id];
694 __bin._M_first[__thread_id] = __block->_M_next;
如果线程自己的空闲链表有元素,那么取出来到__block里,然后让链表头指向下一个块(如果有的话)。
696 __pool._M_adjust_freelist(__bin, __block, __thread_id);
在单线程情况下,_M_adjust_freelist什么事情都不做。在多线程情况下,_M_adjust_freelist负责调整_M_free和_M_used计数器,然后设置__block的_M_thread_id为当前线程的__thread_id。
697 __c = reinterpret_cast<char*>(__block) + __pool._M_get_align();
准备把__block返回给用户。注意用户得到的并不是指向__block的指针,而是加上了一个_M_get_align(),默认情况下返回值为8,表示__block与实际数据区的距离。跳过前面_M_adjust_freelist设置的_M_thread_id,这是每个块都要记录的信息,要保留到deallocate的时候使用。
698 }
699 else
700 {
701 // Null, reserve.
702 __c = __pool._M_reserve_block(__bytes, __thread_id);
如果线程自己的空闲链表没有有元素,就向内存池申请。_M_reserve_block里的详细内容在后面研究。
703 }
704 return static_cast<_Tp*>(static_cast<void*>(__c));
最后返回给用户申请到的内存的指针。
705 }
deallocate
内存块的释放过程与分配过程基本相反,__mt_alloc里实现的代码也比较简单,当然,复杂的内幕都交给内存池去处理了。
707 template<typename _Tp, typename _Poolp>
708 void
709 __mt_alloc<_Tp, _Poolp>::
710 deallocate(pointer __p, size_type __n)
这是deallocate的函数原型,参数__p为要释放的对象首地址,__n为对象的个数。
711 {
712 if (__builtin_expect(__p != 0, true))
713 {
显然在多数情况下,__p != 0是成立的。如果__p为0,那么deallocate什么都不需要做。
714 // Requests larger than _M_max_bytes are handled by
715 // operators new/delete directly.
716 __pool_type& __pool = __policy_type::_S_get_pool();
717 const size_t __bytes = __n * sizeof(_Tp);
718 if (__pool._M_check_threshold(__bytes))
719 ::operator delete(__p);
720 else
721 __pool._M_reclaim_block(reinterpret_cast<char*>(__p), __bytes);
722 }
723 }
接下来的代码都很容易理解,计算总共的字节数,检查是否超过了阀值。如果是则用delete释放内存,否则使用_M_reclaim_block归还给内存池。_M_reclaim_block的具体实现在后面研究。
class __mt_alloc_base
__mt_alloc_base 是__mt_alloc的基类,也许你认为它应该处理了所有__mt_alloc遗留下来的“难题”,但是结果可能令你失望:__mt_alloc_base其实异常简单,连我在初次见到它是时候都有点吃惊。
560 /// @brief Base class for _Tp dependent member functions.
这句话定位了__mt_alloc_base的角色,它只处理与_Tp有关的一些成员函数。
561 template<typename _Tp>
562 class __mt_alloc_base
563 {
564 public:
565 typedef size_t size_type;
566 typedef ptrdiff_t difference_type;
567 typedef _Tp* pointer;
568 typedef const _Tp* const_pointer;
569 typedef _Tp& reference;
570 typedef const _Tp& const_reference;
571 typedef _Tp value_type;
这是基本的typedef类型,与__mt_alloc里的完全一样。
573 pointer
574 address(reference __x) const
575 { return &__x; }
576
577 const_pointer
578 address(const_reference __x) const
579 { return &__x; }
580
581 size_type
582 max_size() const throw()
583 { return size_t(-1) / sizeof(_Tp); }
在__mt_alloc的allocate函数里调用的的max_size()就是这个函数。
585 // _GLIBCXX_RESOLVE_LIB_DEFECTS
586 // 402. wrong new expression in [some_] allocator::construct
587 void
588 construct(pointer __p, const _Tp& __val)
589 { ::new(__p) _Tp(__val); }
使用placement new操作符在__p指向的内存上建立起值为__val 的_Tp对象。
591 void
592 destroy(pointer __p) { __p->~_Tp(); }
593 };
在stl里,allocator的一个重要特点就是把内存分配与对象构造区分开来,同时也区分了内存释放与对象析构。函数construct和destroy分别负责构造和析构对象,而allocate和deallocate分别负责分配和释放内存。
class __common_pool_policy
虽然只是__mt_alloc的一个模板参数,但是它的意义远大于__mt_alloc_base。当然,__common_pool_policy也不是真正的“幕后主角”,它的作用只是提供_M_rebind,给__mt_alloc::rebind使用。
450 /// @brief Policy for shared __pool objects.
451 template<template <bool> class _PoolTp, bool _Thread>
452 struct __common_pool_policy : public __common_pool_base<_PoolTp, _Thread>
模板参数_PoolTp是真正的内存池,_Thread表示是否需要多线程支持。
453 {
454 template<typename _Tp1, template <bool> class _PoolTp1 = _PoolTp,
455 bool _Thread1 = _Thread>
由于_M_rebind只在__mt_alloc::rebind里使用过,而且只使用了第一个模板参数_Tp1,所以后2个模板参数其实只是为了与__common_pool_policy自己的定义“兼容”,所以这里用了默认值。
456 struct _M_rebind
457 { typedef __common_pool_policy<_PoolTp1, _Thread1> other; };
而other的类型其实只与_PoolTp1和_Thread1有关,所以_Tp1模板参数虽然是__mt_alloc::rebind唯一关心的东西,但是到了这里,却发现毫无用处。其实整个_M_rebind都是毫无用处的,如果你回头看__mt_alloc::rebind定义的629行,就会发现pol_type其实就是_Poolp1自己,因为模板参数_Tp1在_M_rebind里根本就没有用到过。
459 using __common_pool_base<_PoolTp, _Thread>::_S_get_pool;
460 using __common_pool_base<_PoolTp, _Thread>::_S_initialize_once;
这2个using的作用我也不太清楚,可能只是为了强调一下吧。因为_S_get_pool和_S_initialize_once在基类里本来就是公有的,所以即使去掉这2个using语句也可以。
461 };
__mt_alloc源码分析(1)相关推荐
- 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析
目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...
- SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...
- SpringBoot-web开发(二): 页面和图标定制(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...
- SpringBoot-web开发(一): 静态资源的导入(源码分析)
目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...
- Yolov3Yolov4网络结构与源码分析
Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...
- ViewGroup的Touch事件分发(源码分析)
Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...
- View的Touch事件分发(二.源码分析)
Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...
- MyBatis原理分析之四:一次SQL查询的源码分析
上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码 String res ...
- [转]slf4j + log4j原理实现及源码分析
slf4j + log4j原理实现及源码分析 转载于:https://www.cnblogs.com/jasonzeng888/p/6051080.html
最新文章
- 实现Excel数据导入到SQL2005中的方法(回顾)
- 整型变量(int)与字节数组(byte[])的相互转换
- 思维导图下载 注册安全_【思维导图】初中各科思维导图,涵盖3年各科所有知识点,可下载打印!...
- JAVA vo pojo javabean dto区别
- mysql 日志节点恢复_基于binlog二进制日志的MySQL恢复笔记
- hdu 4775 Infinite Go(暴力)
- leetcode之有效的括号
- 手把手图文教你从Eclipse项目迁移Android Studio
- 四个开放源代码审查工具【图文】
- 《编码的奥秘》读书笔记
- 使用MATLAB的trainNetwork设计一个简单的LSTM神经网络
- android模拟器快捷键,Android模拟器快捷键大全
- 利用wireshark分析Voip语音RTP协议
- ORACLE数据库字段类型说明
- java中isolate时间_Flutter 92: 图解 Dart 单线程实现异步处理之 Isolate (一)
- OpenCV绘制透明底的图片,简单易懂讲解alpha通道怎么用
- python URL解析转换成字典
- 2021年JAVA面试~光头佳的论述
- c语言体重指数怎么算_107股票信息网,股票涨3个点怎么算,股市行情今日大盘上证指数走势图...
- 未来会有哪些黑科技推动区块链技术的发展
热门文章
- python进行数据分析------相关分析
- java 递归扫描文件夹_java扫描文件夹下面的所有文件(递归与非递归实现)
- 复制小米手机Redmi Note 9 Pro中的录音文件到电脑端(phone到pc)
- 计算机网络实验四:应用层和传输层协议分析(PacketTracer)
- 职称计算机教程pdf,副高级职称计算机考试必考模块.pdf
- 老闪创业那些事儿(12)——产品经理小P的一天
- 华为云MapReduce、ModelArts实现大数据综合案例-在线拍卖数据分析
- Chakra-UI组件库介绍
- 打包下载图片(文件)
- 经过量子破坏后,一种方法毫发无损地幸存下来