8. 定制new和delete

49. 了解new-handler的行为

  1. 当operator new无法满足某一内存分配需求时,它会抛出异常。以前它会返回一个u1指针,某些旧式编译器目前也还那么做。
  2. 当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new_handler。
  3. 为了指定这个“用以处理内存不足”的函数,客户必须调用set new handler,那是声明于的个标准程序库函数:
1
2
3
4
namespace std{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
  1. set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set new handler被调用前正在执行(但马上就要被替换)的那个new-handler函数。
  2. 设计良好的new-handler函数必须做以下事情:
  3. 让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。
  4. 安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。果真如此,目前这个new-handler就可以安装另外那个new-handler以替换自己(只要调用set new handler)。下次当operator new调用new-handler,`调用的将是最新安装的那个。(这个旋律的变奏之一是让new-handler修改自己的行为,于是当它下次被调用,就会做某些不同的事。为达此目的,做法之一是令new-handler修改“会影响new-handler行为”的static数据、namespace数据或global数据。)
  5. 卸除new-handler,也就是将null指针传给set new handler。一旦没有安装任何new-handler, operator new会在内存分配不成功时抛出异常。
  6. 抛出bad alloc(或派生自bad alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
  7. 不返回,通常调用abort或exit。
  8. C++并不支持class专属之new-handlers,但其实也不需要。你可以自己实现出这种行为。只需令每一个class提供自己的set new handler和operator,new即可。其中set new handler使客户得以指定class专属的new-handler(就像标准的set new handler允许客户指定global new-handler),至于operator new.则确保在分配class对象内存的过程中以class专属之new-handler替换global new-handler。
  9. set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
  10. Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

50. 了解new和delete的合理替换时机

  1. 为什么替换编译器提供的operator new或operator delete呢?
  2. 用来检测运用上的错误。如果将“new所得内存”delete掉却不幸失败,会导致内存泄漏(memory leaks)。如果在“new所得内存”身上多次delete则会导致不确定行为。如果operator new持有一串动态分配所得地址,而operator delete将地址从中移走,倒是很容易检测出上述错误用法。
  3. 为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序(例如网页服务器,web servers)接受,也可被执行时间少于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题(fragmentation),这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存。
  4. 现实存在这么些个对内存管理器的要求,因此编译器所带的operator news和operator deletes采取中庸之道也就不令人惊讶了。它们的工作对每个人都是适度地好,但不对特定任何人有最佳表现。如果你对你的程序的动态内存运用型态有深刻的了解,通常可以发现,定制版之perator new和operator delete性能胜过缺省版本。说到胜过,我的意思是它们比较快,有时甚至快很多,而且它们需要的内存比较少,最高可省50%。对某些(虽然不是所有)应用程序而言,将旧有的(编译器自带的)new和delete替换为定制版本,是获得重大效能提升的办法之一。
  5. 为了收集使用上的统计数据。在一头栽进定制型news和定制型deletes之前,理当先收集你的软件如何使用其动态内存。分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LFO(后进先出)次序或随机次序来分配和归还?它们的运用型态是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所使用的最大动态分配量(高水位)是多少?自行定义operator new和operator delete使我们得以轻松收集到这些信息。
  6. 另一个选择是开放源码(open source)领域中的内存管理器。它们对许多平台都可用,你可以下载并试试。Boost程序库(见条款55)的Pool就是这样一个分配器,它对于最常见的“分配大量小型对象”很有帮助。
  7. 为了增加分配和归还的速度。泛用型分配器往往(虽然并不总是)比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。Class专属分配器是“区块尺寸固定”之分配器实例,例如Boost提供的Pool程序库便是。如果你的程序是个单线程程序,但你的编译器所带的内存管理器具备线程安全,你或许可以写个不具线程安全的分配器而大幅改善速度。当然,在获得'operator new和operator delete有加快程序速度的价值”这个结论之前,首先请分析你的程序,确认程序瓶颈的确发生在那些内存函数身上。
  8. 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然并非总是)不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如Boost的Pool程序库)本质上消除了这样的额外开销。
  9. 为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。一如先前所说,在x86体系结构.上doubles的访问最是快速一如果它们都是8-byte齐位。但是编译器自带的operator news并不保证对动态分配而得的doubles采取8-byte齐位。这种情况下,将缺省的operator new替换为一个8-byte齐位保证版,可导致程序效率大幅提升。
  10. 为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这么一来它们就可以被成簇集中在尽可能少的内存页(pages).上。new和delete的“placement版本”(见条款52)有可能完成这样的集簇行为。
  11. 为了获得非传统的行为。有时候你会希望operators new和delete做编译器附带版没做的某些事情。例如你可能会希望分配和归还共享内存(shared memory)内的区块,但唯一能够管理该内存的只有CAPI函数,那么写下一个定制版new和delete(很可能是placement版本,见条款52),你便得以为C API穿上一件C+外套。你也可以写一个自定的operator delete,在其中将所有归还内存内容覆盖为0,藉此增加应用程序的数据安全性。
  12. 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。

51. 编写new和delete时需固守常规

  1. 让我们从operator new开始。实现一致性operator new必得返回正确的值,内存不足时必得调用new-handling函数(见条款49),必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new一虽然这比较偏近class的接口要求而非实现要求。正常形式的new描述于条款52。
  2. operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就遵循条款49描述的规则,并抛出一个bad alloc异常。
  3. 然而其实也不是非常单纯,因为operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operatornew才会抛出异常。
  4. 奇怪的是C++规定,即使客户要求0 bytes,.operator new也得返回一个合法指针。纹种看以格异的行为甘中是为了简化语言其他部分。下面是个non-member operator new伪码(pseudocode):

  1. 你也可能带着怀疑的眼光斜睨这份伪码(pseudocode),因为其中将new-handling函数指针设为null而后又立刻恢复原样。那是因为我们很不幸地没有任何办法可以直接取得new-handling函数指针,所以必须调用set new handler找出它来。拙劣,但有效一至少对单线程程序而言。若在多线程环境中你或许需要某种机锁(lock)以便安全处置new-handling函数背后的(global)数据结构。
  2. operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。.它也应该有能力处理0 bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
  3. operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。

52. 写了placement new还要写placement delete

  1. 这个版本的new已被纳入C++标准程序库,你只要#include就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement new版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placement new有多重定义。当人们谈到placement new,大多数时候他们谈的是此一特定版本,也就是“唯一额外实参是个void*”,少数时候才是指接受任意额外实参之operator new。上下文语境往往也能够使意义不明确的含糊话语清晰起来,但了解这一点相当重要:一般性术语"placement
  2. placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对着一个指针(例如上述的pw)施行delete绝不会导致调用placement delete。不,绝对不会。
  3. 当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
  4. 当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。

8. 定制new和delete
http://binbo-zappy.github.io/2024/11/27/effective-cpp/8-定制new和delete/
作者
Binbo
发布于
2024年11月27日
许可协议