终于,我们要开始研究mt allocator里最核心的东西了。__pool是实际上的内存池类,以前我们介绍的那么多类,都是在它的基础上建立起来的“上层建筑”。
188 template<bool _Thread>
189 class __pool;
这是__pool的原型,模板参数_Thread表示是否支持多线程。接着分别对_Thread为false和true的情况,进行了特化实现。由于有一部分__pool的实现代码在GCC源码中的“libstdc++-v3/src/ mt_allocator.cc”(以下简称mt_allocator.cc)里,所以读者请注意代码所属的文件,没有标明的情况下默认为mt_allocator.h。
191 /// Specialization for single thread.
192 template<>
193 class __pool<false> : public __pool_base
单线程下的__pool<false>,它的基类是__pool_base。__pool_base同时也是多线程下__pool<true>的基类,它的作用就是提炼出线程无关的部分。
194 {
195 public:
196 union _Block_record
197 {
198 // Points to the block_record of the next free block.
199 _Block_record* volatile _M_next;
200 };
_Block_record是每个块的头信息,在单线程下只有一个成员,即指向下一个块的指针。对于分配给用户使用的块,_M_next是没有用的。但是为了与多线程下的头信息结构兼容,单线程的_M_next也不会被用户数据占用。
202 struct _Bin_record
203 {
204 // An "array" of pointers to the first free block.
205 _Block_record** volatile _M_first;
206
207 // A list of the initial addresses of all allocated blocks.
208 _Block_address* _M_address;
209 };
_Bin_record是bin的内部结构,下图虽然在前面出现过,但现在仍是最好的注释。在单线程情况下,只有_M_first[0]会真正被使用到,所以__pool<false>当然不会傻到给_M_first分配4097个指针空间。后面可以看到,__pool<false>里所有bin的_M_first数组的长度是1。
_M_address是一个内存块链表,每当__pool从OS申请到一块新内存,都会加入到这个链表中去。
127 struct _Block_address
128 {
129 void* _M_initial;
130 _Block_address* _M_next;
131 };
成员变量_M_initial指向当前申请到的新内存,_M_next指向下一个。
__pool<false>成员变量
__pool<false>的成员变量包括2部分:自己的,和从__pool_base继承来的。
我们先介绍它自己的。
245 // An "array" of bin_records each of which represents a specific
246 // power of 2 size. Memory to this "array" is allocated in
247 // _M_initialize().
248 _Bin_record* volatile _M_bin;
显然,这就是上图中的bin数组。
250 // Actual value calculated in _M_initialize().
251 size_t _M_bin_size;
这是bin数组的长度。在__pool<false>对象构造的时候,它并不知道bin数组会有多长,因为这与其他参数有关,实际上_M_bin_size的值是在初始化函数_M_initialize里计算出来的,当然只计算一次。
__pool_base成员变量
从__pool_base继承来的成员变量有:
172 // Configuration options.
173 _Tune _M_options;
我们以前介绍过的可调参数,包括7个:
l 字节对齐:空闲块里数据区的偏移
l 多少字节以上的内存直接用new分配
l 可分配的最小的数据区大小
l 每次从OS申请的内存块的大小
l 可支持的最多线程数目
l 单个线程能保存的空闲块的百分比(超过的空闲块会归还给全局空闲链表)
l 是否直接使用new和delete
175 _Binmap_type* _M_binmap;
字节数到bin索引的映射数组。_M_binmap的作用就是最快的找到用户申请的内存大小应该归哪个bin管理。
177 // Configuration of the pool object via _M_options can happen
178 // after construction but before initialization. After
179 // initialization is complete, this variable is set to true.
180 bool _M_init;
记录__pool是否已经初始化过了。注释的意思是,对于_M_options的更改可能发生在__pool构造完,但是还没有初始化的时候,所以用_M_init标记。当初始化工作做完了,_M_init会设置成true。
需要补充的一点是,_Binmap_type的类型:
50 // Using short int as type for the binmap implies we are never
51 // caching blocks larger than 32768 with this allocator.
52 typedef unsigned short int _Binmap_type;
注释里说,由于_M_binmap数组元素的类型是short int,所以分配的块最大不能超过32768。
说实话对这个结论我不太理解。因为据我所知,_M_binmap数组元素的意义是bin数组的索引,所以short int最大值为32768只是表示bin数组最大长度为32768,而232768已经是很大的一个内存块了。不过暂时我保留自己的看法,等研究到后面的代码再说。
初始化
__pool<false>的初始化工作包括2部分,构造函数和初始化函数。
238 explicit __pool()
239 : _M_bin(NULL), _M_bin_size(1) { }
240
241 explicit __pool(const __pool_base::_Tune& __tune)
242 : __pool_base(__tune), _M_bin(NULL), _M_bin_size(1) { }
除了默认构造函数,__pool<false>还可以用调节参数__tune来构造。有一点注意的是,_M_bin_size被初始化为1。
函数_M_initialize是__pool<false>用来初始化内部参数的,它的实现代码位于mt_allocator.cc里面。
<mt_allocator.cc>
155 void
156 __pool<false>::_M_initialize()
157 {
158 // _M_force_new must not change after the first allocate(), which
159 // in turn calls this method, so if it's false, it's false forever
160 // and we don't need to return here ever again.
161 if (_M_options._M_force_new)
162 {
163 _M_init = true;
164 return;
165 }
可调参数_M_force_new表示是否直接使用new和delete进行内存分配和释放。如果它为true,那么__pool<false>无需任何初始化,allocate和deallocate函数直接调用new和delete。这里只把_M_init设置为true,然后返回。
167 // Create the bins.
168 // Calculate the number of bins required based on _M_max_bytes.
169 // _M_bin_size is statically-initialized to one.
170 size_t __bin_size = _M_options._M_min_bin;
可调参数_M_min_bin是可分配的最小的数据区大小,也是bin管理的最小字节数。
171 while (_M_options._M_max_bytes > __bin_size)
可调参数_M_max_bytes表示“多少字节以上的内存直接用new分配”,那么就是bin管理的最大字节数。
172 {
173 __bin_size <<= 1;
174 ++_M_bin_size;
这里计算出了从_M_min_bin到_M_max_bytes之间需要多少个bin,每个bin管理的字节数是指数递增的。注意_M_bin_size初始值为1。
175 }
176
177 // Setup the bin map for quick lookup of the relevant bin.
178 const size_t __j = (_M_options._M_max_bytes + 1) * sizeof(_Binmap_type);
计算_M_binmap数组的字节数。_M_binmap数组的长度是“_M_max_bytes + 1”,每个元素的类型是_Binmap_type。
179 _M_binmap = static_cast<_Binmap_type*>(::operator new(__j));
分配足够的空间给_M_binmap。
180 _Binmap_type* __bp = _M_binmap;
181 _Binmap_type __bin_max = _M_options._M_min_bin;
182 _Binmap_type __bint = 0;
183 for (_Binmap_type __ct = 0; __ct <= _M_options._M_max_bytes; ++__ct)
184 {
185 if (__ct > __bin_max)
186 {
187 __bin_max <<= 1;
188 ++__bint;
189 }
190 *__bp++ = __bint;
191 }
这段代码设置_M_binmap每个元素的值,使其等于对应的bin索引。首先注意到__bp和__ct,初始值分别是_M_binmap和0,在for循环里是同步增长的。然后看到__bint,它表示bin索引,初始值为0,每当“__ct > __bin_max”的时候加1。而__bin_max则表示每个bin管理的字节数,从_M_min_bin开始,在设置完前面的所有_M_binmap元素的时候,“__bin_max <<= 1”变成下一个bin管理的字节数。
如果读者看懂了这段代码,我就提出几个问题:
1._M_min_bin和_M_max_bytes的类型都是size_t,在mt_allocator.h的76和81行可以找到它们的定义。为什么__bin_max和__ct的类型是_Binmap_type(unsigned short int)?
2.代码里唯一给_M_binmap数组元素赋值的语句是“*__bp++ = __bint”,而__bint作为_Binmap_type类型变量只在“__bin_max <<= 1”的时候会加1。即使__bin_max是size_t类型的,__bint的值也不会超过32(32位机器,或64,在64位机器上),也就是说_M_binmap数组元素的最大值不会超过64,那么何必要用unsigned short int类型表示?
193 // Initialize _M_bin and its members.
194 void* __v = ::operator new(sizeof(_Bin_record) * _M_bin_size);
195 _M_bin = static_cast<_Bin_record*>(__v);
分配bin数组的内存,_M_bin_size在前面已经计算出来了。
196 for (size_t __n = 0; __n < _M_bin_size; ++__n)
197 {
198 _Bin_record& __bin = _M_bin[__n];
199 __v = ::operator new(sizeof(_Block_record*));
200 __bin._M_first = static_cast<_Block_record**>(__v);
从这里可以看出来,__pool<false>里每个bin的_M_first数组只有一个元素。
201 __bin._M_first[0] = NULL;
202 __bin._M_address = NULL;
203 }
204 _M_init = true;
把_M_init设置为true,表示初始化工作完成了。
205 }