简介: 代码的性能优化是一项非常重要的工作。经常可以看到,采用 C 或 C++ 编写的、功能正确的软件在执行时耗费大量的内存、时间、或者在最糟的情况下既耗内存又费时间。作为一名开发人员,可以使用 C/C++ 提供的功能强大的工具来改进处理时间,并且防止内存破坏,这些工具其中之一是控制如何在代码中分配或者释放内存。通过介绍如何针对特定的情况创建自己的内存管理器,本教程对内存管理的相关概念进行了揭秘。
开始之前
了解本教程中包含的内容以及如何最好地利用本教程。
本教程采用了一种基本方法为任何应用程序构建内存管理器。本教程解释了为什么需要内存管理器,并提供了为应用程序编写自定义的内存管理器(以满足特定的需求)的方法。
在本教程中,您将了解在设计内存管理器之前需要考虑的一些注意事项、可用于创建这种内存管理器的一些特定技术,并在文章的最后了解创建内存管理器的方法。您还将了解各种类型内存管理器设计的优点和不足之处。
本教程面向那些入门级到中级水平的 Linux® 或 UNIX® 程序员。您应该对使用 UNIX 命令行 Shell,以及 C/C++ 语言的应用知识有基本的了解。此外,还需要了解 malloc
、calloc
、free
、memcpy
和 memset
等系统调用(即处理内存分配、释放和内容修改的例程)的内部工作方式。
要运行本教程中的示例,您需要具备安装了 g++ 编译器工具链的 Linux 或者 UNIX 系统。还需要具有足够大的 RAM(大约 256 MB)。
为什么要创建自定义的内存管理器呢?
要理解内存分配控制如何帮助提高代码的运行速度,首先需要回忆一下 C/C++ 中内存管理的基础知识。C 中的标准库函数malloc
、free
、calloc
和 realloc
,以及 C++ 中的 new
、new [ ]
、delete
和 delete [ ]
操作符,是这两种语言中内存管理的关键之处。在了解了这一点之后,有几个内容值得注意。
如 malloc
和 new
函数是通用的内存分配器。您的代码可能是单线程的,但它所链接的 malloc
函数同样可以处理多线程的范例。正是由于这个额外功能,使得这些例程的性能有所降低。
在执行时,malloc
和 new
将向操作系统内核请求内存,而 free
和 delete
则请求释放内存。这意味着,操作系统必须在每次提出内存请求时在用户空间代码和内核代码之间进行切换。反复调用 malloc
或者 new
的程序,最终将由于不断地进行上下文切换而导致运行缓慢。
对于在程序中分配的、并且以后不再需要使用的内存,常常会忘记对其进行删除,并且 C/C++ 并没有提供自动的垃圾收集。这将导致程序的内存空间占用进一步增长。在实际的大型程序中,性能将受到显著的影响,因为可用内存变得越来越少,并且硬盘访问是非常耗时的。
设计目标
您的内存管理器应该满足下面的设计目标:
- 速度
- 健壮性
- 用户使用便利性
- 可移植性
与编译器提供的分配器相比,内存管理器的速度必须更快一些。重复的分配和释放不应该降低代码的执行速度。如果可能的话,应该对内存管理器进行优化,以便处理代码中频繁出现的某些分配模式。
在程序终止之前,内存管理器必须归还它向系统请求的所有内存。也就是说,不应该存在内存泄漏。它还应该能够处理错误的情况(例如,请求过大的内存),并且很好地解决这些问题。
在将内存管理器集成到他们的代码中时,用户应该只需要更改很少的代码。
应该可以很容易地将内存管理器移植到其它的系统,并且不应该使用与平台相关的内存管理特性。
创建内存管理器的实用策略
在创建内存管理器时,下面的这些策略是很实用的:
- 请求较大的内存块。
- 对常见的请求大小进行优化。
- 在容器中收集删除的内存。
最常见的内存管理策略之一是,在程序启动期间请求一些较大的内存块,然后在代码执行期间反复地使用它们。可以从这些内存块划出部分内存,以满足各种数据结构的内存分配请求。这将极大地减少系统调用的次数,并提高执行性能。
在任何程序中,某些特定的请求大小将比其它大小的请求更加常见。如果对您的内存管理器进行优化以便更好地处理这些请求,那么它将工作得更加出色。
应该将程序执行期间删除的内存收集到容器中。然后,应该使用这些容器来满足进一步的内存请求。如果某个请求失败,那么应该将内存访问委托给程序启动期间分配的某个较大的内存块。虽然内存管理最初用于加速程序的执行和防止内存泄漏,但这种技术可能会潜在地导致程序的较低内存空间占用,这是因为它可以重用删除的内存。这是编写您自己的内存分配器的另一个原因!
分析 C++ new/delete 操作符的执行时间
我们将从一个简单示例开始。假定您的代码使用了一个称为 Complex
的类(该类用于表示复数),并且它使用了 new
和 delete
操作符所提供的机制,如清单 1 中所示。
清单 1. Complex 类的 C++ 代码
class Complex { public: Complex (double a, double b): r (a), c (b) {} private: double r; // Real Part double c; // Complex Part }; int main(int argc, char* argv[]) { Complex* array[1000]; for (int i = 0;i < 5000; i++) { for (int j = 0; j < 1000; j++) { array[j] = new Complex (i, j); } for (int j = 0; j < 1000; j++) { delete array[j]; } } return 0; } |
外层循环的每次迭代都会导致 1000 次分配和释放。5000 次这样的迭代将导致 10 百万次用户和内核代码之间的切换。在 Solaris 10 计算机中使用 gcc-3.4.6 进行编译之后,执行这个测试程序平均需要花费 3.5 秒。这是编译器提供的全局 new
和 delete
操作符实现的基准性能度量。要为 Complex
类创建自定义的内存管理器以改进编译器的实现,您需要重写 Complex
类特定的 new
和 delete
操作符。
New/Delete:深入研究
在 C++ 中,对内存管理进行组织实际上就是重载 new
或者 delete
操作符。代码中不同的类可能需要使用不同的内存分配策略,这意味着每个类需要特定的 new
。否则,必须重写 new
或者 delete
全局操作符。可以采用这两种形式中的任何一种来实现操作符重载,如清单 2 中所示。
清单 2. 重载 new 或者 delete 操作符
void* operator new(size_t size); void operator delete(void* pointerToDelete); -OR- void* operator new(size_t size, MemoryManager& memMgr); void operator delete(void* pointerToDelete, MemoryManager& memMgr); |