优化内存管理
优化内存管理
复习C++内存管理器API
-
动态变量的生命周期
动态变量有五个唯一的生命阶段。最常见的new表达式的各种重载形式执行分配和放置生命阶段。在使用阶段后,delete表达式会执行销毁和释放阶段。C++提供了单独管理每个阶段的方法。
-
分配(malloc、new)
程序要求内存管理器返回一个指向至少包含指定数量未类型化的内存字节的连续内存地址的指针。如果没有足够的可用内存,那么分配将会失败。 -
放置(new)
程序创建动态变量的初始值,将值放置到被分配的内存中。如果构造函数抛出异常,那么放置会失败,需要将分配的存储空间返回给内存管理器。 -
使用
程序从动态变量中读取值,调用动态变量的成员函数并将值写入到动态变量中。 -
销毁(delete)
调用析构函数,返回持有的所有系统资源,完成所有清理工作。如果析构函数抛出一个在析构函数体内不会处理的异常,析构会失败,程序会无条件终止。显式地调用变量的析构函数能够销毁一个变量但不释放它的存储空间。 -
释放(free、delete)
程序将属于被销毁的动态变量的存储空间返回给内存管理器。
-
-
内存管理函数分配和释放内存
-
new()运算符实现分配
C++定义了new()运算符的几种重载形式
void* ::operator new(size_t);
void* ::operator new;
void* ::operator new(size_t, const std::nothrow_tag&);
void* ::operator new[](size_t, const std::nothrow_tag&);
标准库提供并隐式声明了定位放置new()运算符的两种重载版本,它们不会分配内存。
void* ::operator new(size_t, void);
void ::operator new[](size_t, void*); -
delete()运算符释放被分配的内存
如果一个程序定义了new()运算符来从一个特殊的内存池或是以一种特别的方式分配内存,它也必须在相同的作用域内相应地定义一个delete()运算符。 -
C语言库中的内存管理函数
为了确保与C程序的兼容性,C++提供了C语言库函数malloc、calloc、realloc来分配内存,以及free来返回不再需要的内存。根据C++标准,malloc和free作用于堆上,而new和delete作用于自由存储区。
-
-
new表达式构造动态变量
:: new (placement-params) (type) initializer
或:: new (placement-params) type initializer-
不抛出异常的new表达式
如果placement-params中包含关键字std::nothrow,那么new表达式不会抛出std::Bad_alloc。它不会尝试构造对象,而是直接返回nullptr。 -
定位放置new表达式执行定位放置处理而不进行分配
如果placement-params是一个指向已经存在的有效存储空间的指针,那么new表达式不会调用内存管理器,而只是简单地将type放置在指针所指向的内存地址,而且这块内存必须能够容下type。
char mem[1000];
class Foo;
Foo* _p = new (mem) Foo(123);
由于定位放置new表达式并不分配存储空间,因此它没有相应的定位放置delete表达式。开发人员需要显式地调用析构函数来销毁定位放置new表达式创建的实例。 -
自定义定位放置new表达式——内存分配的腹地
如果placement-params是其他东西,那么这个new表达式被称为自定义定位放置new表达式。当程序需要建立多种创建动态变量或是传递参数用于内存管理器诊断的机制时,自定义定位放置new表达式非常有用。但它也有一个问题,就是无法指定匹配的“自定义定位放置delete表达”。 -
类专用new()运算符允许我们精准掌握内存分配
类专用new()运算符是高效的,因为它为大小固定的对象分配内存。如果类没有被用在多线程中,那么就可以免去确保类的内部数据结构是线程安全的这项开销。类专用new()运算符需要定义为类的静态成员函数,并且需要实现相应的delete()运算符。
-
-
delete表达式处置动态变量
:: delete expression
或:: delete [] expression -
显式析构函数调用销毁动态变量
通过显式地调用析构函数,而不是使用delete表达式,能够只执行动态变量的析构,但不释放它的存储空间。
高性能内存管理器
提供类专用内存管理器
当动态创建类实例的代码被确定为热点代码时,通过提供类专用内存管理器能够改善程序性能。
- 能够高效地复用被返回的内存。不必担心碎片。
- 能够以很少甚至零内存间接开销的方式实现。
- 能够确保所消耗内存总量的上限。
- 分配和释放内存的函数都非常简单,因此会被高效地内联。
- 具有优秀的高速缓存行为。最后一个被释放的节点可是下一个被分配的节点。
-
分配固定大小内存的内存管理器
-
内存块分配区
-
添加一个类专用new()运算符
-
分配固定大小内存块的内存管理器的性能
-
分配固定大小内存块的内存管理器的变化形式
-
非线程安全的内存管理器是高效的
-
它们不需要同步机制来序列化临界区。
-
它们不会挂起在同步原语上。
-
更加容易编写
-
自定义标准库分配器
C++模板提供了一种定义每种容器所使用的内存管理器的机制。标准库容器可以接收一个Allocator参数,它具有与类专用new运算符相同的自定义内存管理器的能力。它会做三件事:
- 从内存管理器中获取存储空间
- 返回存储空间给内存管理器
- 从相关联的分配器中复制构造出它自己。
-
最小C++11分配器
-
C++98分配器的其他定义
-
一个分配固定大小内存块的分配器
-
字符串的分配固定大小内存块的分配器
小结
-
相比于内存管理器,在其他地方看看有没有可能会带来更好性能改善效果的优化机会。
-
对几个大型开源程序的研究表明,替换默认内存管理器对程序整体运行速度的性能提升最多只有30%。
-
为申请相同大小内存块的请求分配内存的内存管理器是很容易编写的,它的运行效率很高。
-
同一个类的实例的分配内存的请求所申请的内存的大小是一样的。
-
可以在类级别重写new()运算符。
-
标准库容器类std::list、std::map、std::multimap、std::set、std::multiset都从许多同等的节点中创建数据结构。
-
标准库容器接收一个Allocator作为参数,与类专用new()运算符一样,它也允许自定义内存管理。
-
编写一个自定义的内存管理器或分配器可以提高程序性能,但相比于移除对内存管理器的调用等其他优化方法,它的效果没有那么明显。