【STL源码剖析】配置器
- 说明
- 2.1 空间配置器的标准接口
- 2.2.2 SGI特殊的空间配置器 std::alloc
- 2.2.3 构造和析构的基本工具:construct()和destroy()
- 2.2.4 空间的配置与释放:std::alloc
- 2.2.5 第一级配置器 __malloc_alloc_template剖析
- 2.2.6 第二级配置器 __default_alloc_template 剖析
- 2.2.7 空间配置函数 allocate()
- 2.2.8 空间释放函数 deallocate()
- 2.2.9 重新填充 free lists
- 2.2.10 内存池(memory pool)
- 2.3.1 uninitialized_copy
- 2.3.2 uninitialized_fill
- 2.3.3 uninitialized_fill_n
说明
标题前的数字(如 “2.1 空间配置器的标准接口”中的数字 2.1 )代表在《STL源码剖析》一书中的章节号。
2.1 空间配置器的标准接口
“字典”:
ptrdiff_t
ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个头文件内。ptrdiff_t通常被定义为long int类型。
set_new_handler(x)
在利用operator new请求内存分配失败的时候,在抛出异常之前,会调用一个回调函数给程序员一个机会去处理内存分配失败的情况。x 代表的是一个函数指针,指向程序员自己定义的用户处理内存分配失败时候的函数。在这段代码中将 x 设置为nullptr或者0表示程序员放弃这个处理机会,直接抛出bad_alloc异常。
::operator new , ::operator delete
当我们在C++中使用new 和delete时,其实执行的是全局的 ::operator new 和 ::operator delete , :: 前不加命名空间则表示最外层的默认的命名空间。
new( p ) T1(value);
就是在指针p所指向的内存空间创建一个T1类型的对象,但是对象的内容是从T2类型的对象转换过来的,就是在已有空间的基础上重新调整分配的空间。这个操作就是把已有的空间当成一个缓冲区来使用,这样就减少了分配空间所耗费的时间,因为直接用new操作符分配内存的话,在堆中查找足够大的剩余空间速度是比较慢的。
inline 内联函数
使用内联函数可以避免函数调用的开销,当调用一个函数时,除了函数执行语句的开销外,还有很多其他的开销,如拷贝形参、返回时恢复等等,使用内联函数时,程序并不会执行“调用”,而是相当于把函数内的语句在此处抄了一遍,执行单纯的语句比执行函数的速度要快一些,因此使用内联函数可以避免函数调用的开销。
注释:
2.2.2 SGI特殊的空间配置器 std::alloc
2.2.3 构造和析构的基本工具:construct()和destroy()
主要是针对对象的构造和析构,在对象构造前,内存已经分配了,在对象析构后,内存才会被释放。
trivial destructor
如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor。反之,如果特定定义了析构函数,则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor。
2.2.4 空间的配置与释放:std::alloc
size_t
size_t的全称是size_type,用来表示某种类型的大小、字节数,size_t是一种记录大小的数据类型。
空间的配置与释放的设计哲学:
- 向system heap中要求空间
- 考虑多线程(multi-threads)的状态(本文不考虑多线程的情况)
- 考虑内存空间不足时的应变措施
- 考虑碎片空间过多的问题
C++内存配置的基本操作 ::operator new() (全局函数)
C++内存释放的基本操作 ::operator delete()(全局函数)
它们的底层使用的是C语言的malloc()函数和free()函数完成的内存空间的配置与释放
SGI的双层级设置器(为了解决碎片空间问题):
第一级配置器(__malloc_alloc_template):直接使用 malloc() 和 free() 函数进行分配空间和释放空间
第二级配置器(__default_alloc_template):针对碎片空间采用memory pool的整理方法
使用策略:当配置区块超过128kb时,视为空间足够大,直接采用第一级配置器;否则采用第二级配置器。
2.2.5 第一级配置器 __malloc_alloc_template剖析
cerr与cout的区别
cerr是输出到标准错误的ostream对象,常用于程序错误信息,cerr不会经过缓存,会直接输出到屏幕中,通常用来输出警告和错误信息;cout则是输出到标准输出的ostream对象,经过缓存后才会输出到屏幕中。
exit(0)、exit(1)
用来立即中止当前程序的执行,并将一个整数返回给系统,该整数的作用与“由mian函数返回的整数”相同,如果是0表示程序正常退出,如exit(0);如果非0表示程序异常退出,如exit(1)。
realloc(void *p, size_t ,size_t new_sz)
其作用是重新分配已经分配的内存空间的大小,其中*p指向一块内存空间的地址,前一个是old_size,后一个是新的空间大小
set_new_handler()
- 其函数原型是new_handler set_new_handler(new_handler new_p) throw();
- set_new_handler函数的作用是设置new_p指向的函数为new操作或new[]操作失败时调用的处理函数。new_handler函数会尝试为新的内存申请请求提供更多的可用空间。
- 当且仅当函数成功地提供了更多的可用空间,它才返回。否则,或者抛出bad_alloc异常(或bad_alloc派生类)或者终止程序(比如调用abort或exit)。
- 如果new_handler函数返回(提供了更多可用空间)后,当内存申请函数申请指定的内存空间失败时,它会被再次调用,直到new_handle函数不返回或被替换。
- 在set_new_handler函数第一次被调用前,或者new_p是一个空指针,默认的内存申请函数在内存申请失败的时候直接抛出bad_alloc异常。
volatile关键字
- 禁止指令重排
- 每次都会直接从内存中读取值
2.2.6 第二级配置器 __default_alloc_template 剖析
union:一种节省空间的类
union是声明一个特殊的类的关键词,一个union可以有多个数据成员,在任意时刻,联合类中只能有一个数据成员可以有值,当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义的状态了。
第二级配置器的主要算法:
如果区块足够大(超过128B),则使用第一级配置器进行配置;
否则采用内存池(memory pool)进行管理,该方法也称为次层配置,具体的算法流程如下:
- 每次配置一大块内存,并维护对应的自由链表(free-list)
- 下次再有相同大小的内存需求,则直接从自由链表里取出
- 如果客端返还了小额的区块,则由配置器回收到自由链表中,为了方便管理,SGI第二级配置器会主动将任何小额区块(小于128B)的内存需求上调至8的倍数,如需要30B的内存,第二级配置器则会分配32B(8的倍数)的内存,并维护16个自由链表(128 ÷ 8 = 16,每个自由链表的大小为8B、16B、24B、32B、40B、48B、56B、… 、128B)
free-list节点的数据结构:
union obj{
union obj* free_list_link;
char client_data[1];
}
由于obj被声明为union,因此从第一个字段来看,obj会被视为一个指针,用来指向相同形式的另一个obj;从第二个字段来看,obj可以被视为一个指针,用来指向实际的区块。
使用union的好处是不会为了维护链表所必须的指针造成内存的浪费。
free_list采用的是一种数组+链表的形式,类似于邻接表:
2.2.7 空间配置函数 allocate()
问题一:" *my_free_list = result -> free_list_link; " 是什么意思?
- 此时数据结构使用的是数组+链表的形式,那么首先我们要明确free_list节点中的指针的含义是什么——free_list节点的指针指向它的下一个相同大小区块的首地址;
- 结合此信息,我们又知道free_list是一个长度为16的数组,每个地址上存储的是一个大小为8的倍数的内存大小的地址;free_list + FREEELIST_INDEX(n) 等效于 free_list[FREELIST_INDEX(n)],因此my_free_list 首先找到free_list中大小为n的地址在数组中的哪里,然后使my_free_list指向的节点从原来的节点指向原来节点的下一块(即 *my_free_list = result -> free_list_link; )完成区块的拔出操作。
2.2.8 空间释放函数 deallocate()
2.2.9 重新填充 free lists
问题一:" obj * volatile * my_free_list; " 是什么意思?
- 这个声明原来的形式是:obj** my_free_list,这样 *my_free_list (空闲的内存块指针数组中的一个元素)可能被优化到寄存器中,从而使库代码无法lock住对它的读调用(如果在寄存器中则另一个线程可能会无意中修改该寄存器的值,而在内存中由于另一个线程没有访问权力所以不能修改)。
- 因此要声明变量必须在内存中就要用volatile修饰,这里修饰的是*my_free_list,是free_list数组中的一个元素,而不是数组指针,所以volatile放在两个*中间。
问题二:" *my_free_list = next_obj = (obj *)(chunk + n); " 是什么意思
- chunk会返回分配出来的空间的首地址
- 因此 chunk + n 是下一个(大小为n的)空间的首地址
- 然后用my_free_list 和 next_obj 指针指向它。
问题三:“nobjs - 1 == i” 为什么可以用来判断节点是否串连完毕
- 注意nobjs是一个引用传递,虽然nobjs默认数值是20,但是在进行chunk_alloc()的过程中,nobjs会被调整为实际能够被分配的区块的数量
- 因此,for循环从1开始,到 nobjs - 1 结束
2.2.10 内存池(memory pool)
内存池通过控制几个参数,如start_free(内存空间起始地址)、end_free(内存空间结束地址)等来控制内存的分配使用。
2.3.1 uninitialized_copy
uninitialized_copy使我们可以将内存的配置与对象的构造行为分离开(解耦),对于输入范围内的每一个迭代器 i ,该函数会调用 construct(& * (result + (i - first)),*i) ,产生*i的复制,放置在输出范围的相对的位置上。
如果要实现一个容器,那么使用uninitialized_copy会带来很大的帮助,因为容器的全区就构造函数通常以如下两步完成:
- 配置内存区块,足以包含范围内的所有元素
- 使用 uninitialized_copy() 在该内存区块上构造元素
STL定义了五个全局函数,作用于未初始化的空间上:
- construct() 【2.2.3节】
- destory()【2.2.3节】
- uninitialized_copy()
- uninitialized_fill()
- uninitialized_fill_n()
value type
value type可分为POD类型(Plain Old Data、标量型别、传统C struct型别)与非POD类型(non-POD):
POD型别必含有trivial ctor/dtor/copy/assignment函数,可以更加高效地进行复制;
non-POD型别则需要采用其他的方法来进行复制。
construct的实现:
void construct(pointer p,const T& value)
{
_construct(p,value);
}
template <class T1,class T2>
inline void _construct(T1 *p,const T2& value)
{
new(p) T1(value); // placement new
}
uninitialized_copy使我们可以将内存的配置与对象的构造行为分离开(解耦),对于输入范围内的每一个迭代器 i ,该函数会调用 construct(& * (result + (i - first)),*i) ,产生*i的复制,放置在输出范围的相对的位置上。
如果要实现一个容器,那么使用uninitialized_copy会带来很大的帮助,因为容器的全区就构造函数通常以如下两步完成:
- 配置内存区块,足以包含范围内的所有元素
- 使用 uninitialized_copy() 在该内存区块上构造元素
uninitialized_copy具备原子性。
uninitialized_copy的源码及注释:
/*
参数说明:
1.迭代器first指向欲初始化空间的起始处
2.迭代器last指向输入端的结束位置(前闭后开区间)
3.迭代器result指向输出端(欲初始化空间)的起始处
对于char 和 wchar,有特殊的优化(memmove函数)
*/
template <class InputIterator ,class ForwardIterator>
inline ForwardIterator uninitialized_copy(InputIterator first,InputIterator last,ForwardIterator result)
{
// 通过value_type()返回result的value type
return __uninitialized_copy(first,last,result,value_type(result));
}
template <class InputIterator ,class ForwardIterator,class T>
inline ForwardIterator __uninitialized_copy(InputIterator first,InputIterator last,ForwardIterator result,T*)
{
typedef typename __type_traits<T>::is_POD_type is_POD;
// is_POD()判断是否是POD类型
return __uninitialized_copy_aux(first,last,result,is_POD());
}
// 如果是POD型别,执行下面的函数
template <class InputIterator ,class ForwardIterator,class T>
inline ForwardIterator __uninitialized_copy_aux(InputIterator first,InputIterator last,ForwardIterator result,__true_type)
{
// 直接调用copy函数
return copy(first,last,result);
}
// 如果是non-POD型别,则执行下面的函数
template <class InputIterator ,class ForwardIterator,class T>
ForwardIterator __uninitialized_copy_aux(InputIterator first,InputIterator last,ForwardIterator result,__false_type)
{
// 如果是非POD型别,就只能一个个地进行构造
ForwardIterator cur = first;
for (;first != last;++first,++cur)
{
construct(&*cur,*first);
}
return cur;
}
//针对char和wchar_t两种数据类型,以最具效率的memmove来执行赋值行为
inline char* uninitialized_copy(const char* first,const char* last,char* result)
{
memmove(result,first,last-first);
return result + (last - first);
}
inline wchar_t* uninitialized_copy(const wchar_t* first,const wchar_t* last,wchar_t* result)
{
memmove(result,first,sizeof(wchar_t) * (last - first));
return result + (last - first);
}
2.3.2 uninitialized_fill
/*
参数说明:
1.迭代器first指向欲初始化空间的起始处
2.迭代器last指向输入端的结束位置(前闭后开区间)
3.x表示初值
*/
template <class ForwardIterator,class T>
inline void uninitialized_fill(ForwardIterator first,ForwardIterator last,const T& x)
{
// value_type返回first的类型(POD?non-POD?)
__uninitialized_fill(first,last,x,value_type(first));
}
// 执行以下代码判断类似(POD?non-POD?)
template <class ForwardIterator,class T,class T1>
inline void __uninitialized_fill(ForwardIterator first,ForwardIterator last,const T& x,T1*)
{
typedef typename __type_traits<T1>::is_POD_type is_POD;
__uninitialized_fill_aux(first,last,x,is_POD());
}
// 如果是POD,直接使用fill函数赋值
template <class ForwardIterator,class T>
inline void __uninitialized_fill_aux(ForwardIterator first,ForwardIterator last,const T& x,__true_type)
{
fill(first,last,x);
}
// 如果是non-POD,需要一个个单独构造
template <class ForwardIterator,class T>
void __uninitialized_fill_aux(ForwardIterator first,ForwardIterator last,const T& x,__false_type)
{
ForwardIterator cur = first;
for (;cur != last;++cur)
{
construct(&*cur,x); // 必须一个一个元素地建构,无法批量进行
}
return cur;
}
uninitialized_fill也可以将内存的配置与对象的构造行为分离开(解耦),如果[first,last)范围内的每个迭代器都指向未初始化的内存,那么 uninitialized_fill 会在该范围内产生x的复制,即 uninitialized_fill 会针对操作范围内的所有迭代器 i ,调用 construct(&* i, x) ,在 i 所指之处产生 x 的复制。
uninitialized_fill具备原子性。
2.3.3 uninitialized_fill_n
/*
参数说明:
1.迭代器first指向欲初始化空间的起始处
2.n表示欲初始化空间的大小
3.x表示初值
*/
template <class ForwardIterator,class size,class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first,size n,const T& x)
{
// 利用value_type取出first的value type
return __uninitialized_fill_n(first,n,x,value_type(first));
}
template <class ForwardIterator,class size,class T,class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first,size n,const T& x,T1*)
{
typedef typename __type_traits<T1>::is_POD_type is_POD;
return __uninitialized_fill_n_aux(first,n,x,is_POD());
}
//如果是POD类型,直接调用fill_n函数进行构造即可
template <class ForwardIterator,class size,class T>
ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first,size n,const T& x,__true_type)
{
return fill_n(first,n,x);
}
// 如果不是POD类型,必须一个个单独构造
ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first,size n,const T& x,__false_type)
{
ForwardIterator cur = first;
for (;n>0;--n,++cur)
{
construct(&*cur,x);
}
return cur;
}
uninitialized_fill_n也可以将内存的配置与对象的构造行为分离开(解耦),如果[first,first+n)范围内的每个迭代器都指向未初始化的内存,那么 uninitialized_fill_n会在该范围内产生x的复制,即 uninitialized_fill 会针对操作范围内的所有迭代器 i ,调用 construct(&* i, x) ,在 i 所指之处产生 x 的复制。
uninitialized_fill_n具备原子性。