浅谈 Windows 编程中的堆

提起堆,大部分人都不陌生,但是其实很多人也不见得就很了解。我见过的大部分人,对堆的理解其实还停留在,全局的一种内存,速度没有栈快,不会自动销毁,需要开发人员自己管理。这其实不怪 Windows,怪就怪面试人员水平参差不齐,五百年了,问堆还是,堆栈究竟有什么区别。然后在中国这个应试教育横行的地方,也必然是各种针对性的突击,问八百个人都是上边的答案。然而,对于 Windows 的堆,作为一个开发人员,这些了解显然是不够的。

其实想深入了解 Windows 中的堆,仅需要两篇文章,日常开发就够用了。

这两篇文章,说的还算详尽,至少基本的开发会清晰很多。

堆的使用条件

  • 当程序需要的对象不能提前知晓的时候,也就是说需要在运行时动态分配的对象,要在堆上
  • 栈上放不下的对象,要在堆上

堆的种类

这里要引用一段 MSDN 原文:

GlobalAlloc/GlobalFree: Heap calls that talk directly to the per-process default heap.

LocalAlloc/LocalFree: Heap calls that talk directly to the per-process default heap.

COM’s IMalloc allocator (or CoTaskMemAlloc / CoTaskMemFree): Functions use the default per-process heap. Automation uses the Component Object Model (COM)’s allocator, and the requests use the per-process heap.

C/C++ Run-time (CRT) allocator: Provides malloc() and free() as well as new and delete operators. Languages like Microsoft Visual Basic® and Java also offer new operators and use garbage collection instead of heaps. CRT creates its own private heap, which resides on top of the Windows heap.

Traditionally, the operating system and run-time libraries come with an implementation of the heap. At the beginning of a process, the OS creates a default heap called Process heap. ** The Process heap is used for allocating blocks if no other heap is used. Language run times also can create separate heaps within a process. (For example, C run time creates a heap of its own.) Besides these dedicated heaps, the application program or one of the many loaded dynamic-link libraries (DLLs) may create and use separate heaps. Windows offers a rich set API for creating and using private heaps.

从这段描述上看:

  • 每个进程会有一个默认堆
  • C/C++ 运行时会有自己的私有堆。
  • 进程中用到的模块,允许创建自己的私有堆。

这就非常清晰了。这也就是传说中的一个模块一个堆。而关于堆的种类的认知是非常必要的,因为对于堆上的内存,要本着谁申请谁释放的原则,如果在模块的私有堆中申请的内存,拿到模块外由别人释放,就会引发崩溃,因为别人释放的时候会去自己的堆中找那部分内容,找不到就GG了。

而其实在 Windows 中关于堆分配器,其实是有前后端之分的。前端分配器维护一个固定大小的块列表,一个内存分配过来以后先在列表中找未被使用的块,如果找不到才会到后端分配器,新分配出一个块,并且后端分配器还会把这个操作提交到虚拟内存。因为有前后端分配器之分,所以性能问题肯定也会在这中间产生。一个显而易见的就是如果用到后端分配器的操作,必然会比只用前端分配器慢,所以解决这种性能问题还是尽量避免后端分配器操作。

堆的性能问题

  • 内存分配

    内存分配导致的慢主要还是在于当前端分配器找不到可用块时,调用后端分配器,创建新块,以及跟虚拟内存的交互会有性能损耗

  • 内存释放

    内存释放导致的慢是由于释放内存会有一个块合并的操作,将空闲块合并到一起重组成一个大的空闲块,但是这中间会引发对内存的无序访问,导致缓冲命中失败和性能下降。

  • 堆竞争

    在多线程的情况,出现多个线程访问一个堆,需要有一个等待过程。而加锁,会引起线程的上下文切换也是性能下降的原因之一。

  • 堆破坏

    程序没有正确使用堆导致对破坏

  • 频繁的 alloc 和 realloc

    脚本语言容易发生,不过现在的脚本语言解释器都比较机智了,都会分配一块很大的内存自己用,来避免这个。

提升堆性能的一般操作

  • 避免使用指针关联两个数据结构

    使用指针关联两个数据结构会导致对象的分配和释放被分离,产生额外开销。

  • 把孩子对象嵌入父亲对象。

    减少额外分配内存的次数。

  • 合并小对象组成一个大对象(聚合)

    可以减少被分配的块的数量来提升性能,关键是要找好聚合边界

  • 用 Buffer 满足 80% 的需求(二八原则)

    用内存 Buffer 存储字符串或者二进制数据,开一个能满足 80% 需求的大小的 Buffer 即可。剩下 20% 可以开一个新的 Buffer,然后持有指针即可。这样可以减少内存分配和释放,也可以减少数据空间,会提升性能

  • 成块分配内存对象

    小声BB(我个人理解就是指内存池)

  • 使用_amblksiz

    C语言运行时(CRT)特有的前端分配器,可以用它跟后端分配器申请分配一个比较大的块,从而减少对后端分配器的请求。

提升堆性能的进阶操作

  • 使用 Windows Heap
  • 使用内存池
  • 使用 MP Heap。(一个多进程友好的包)
  • 重新思考算法与数据结构

改善堆性能之前需要做的

  1. 评估代码中堆的使用方法
  2. 梳理代码,减少关于堆的调用,修复错误并调整数据结构
  3. 要对堆的性能消耗做具体评估

总结

很多人会认为这些过于底层,对一般开发用处不大,但其实对堆的深入了解,除了可以在程序性能上有一些更大的提升,对于一般开发则可以写出质量更高的代码。只有对操作系统的了解足够的深入,才能写出跟操作系统有着完美配合的代码,这看似是一种玄学,其实是一种科学。是基于 Windows 平台开发应用的开发人员与操作系统的开发人员的一种默契。