1. 了解new-handler
当operator new
无法满足某一内存分配需求时,会抛出异常(当然可以使用nothrow
版本),当operator new
抛出异常以反映一个未获满足的内存需求之前,它会先调用一个错误处理函数new-handler
(当然必须要先调用set_new_handler
。
例如:
#include <new>
void outOfMem() {
std::cerr << "Unable to satifsy request for memory\n";
std::abort();
}
int main() {
std::set_new_handler(outOfMem);
int *pBigDataArray = new int[100000000L];
}
更多信息请查阅参考文献1的条款49:了解new-handler的行为(Understand the behavior of the new-handler)
P240。
2.了解new
和delete
的合理替换时机
Understand when it makes sense to replace new and delete.
替换标准库的operator new
、operator delete
常见的理由:
2.1 用来检测运用上的错误。
a. 将new
所得内存delete
时失败会导致内存泄漏(memory leaks);
b. 在new
所得内存进行多次delete
会导致不确定行为;
c. 由于各种编程错误导致数据”overruns“(写入点在分配区块之后)或”underruns“(写入点在分配区块起点之前)。【如果自定义一个operator new
,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的byte patterns】。operator delete
便得以检查上述签名是否原封不动,若否就表示在分配区的某个时间点发生了overrunns或underruns,operator delete
可以log事实以及发生问题的指针(个人理解,可以在operator delete
时进行校验超额空间放置的特定的byte patterns是否发生了改变)。
2.2 为了强化效能
编译器(标准库)提供的operator new
和operator delete
采取的是中庸之道,需要对每个任务都适度友好,但不对特定任何人有最佳表现。自定义的operator new
和operator delete
性能胜过缺省版本,定制版的速度比较快,有时候会快很多,而且所需内存比较少,最高可节省50%。对于有些程序而言将标准库的operator new
和operator delete
替换为自定义的版本是获得重大效能提升的办法之一。
2.3 为了收集使用上的统计数据
在使用自定义的operator new
和operator delete
之前应该了解开发的软件如何使用动态内存。比如,分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配和归还?它们的运用形态是否随时间改变,即软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所需的最大动态分配量是多少?使用自定义operator new
和operator delete
便于轻松搜集这些信息。
#pragma once
#include <new>
#include <cstdlib>
#include <stdexcept>
#include <iostream>
static const int kSignature = 0xDEADBEEF;
void* operator new(size_t size) {
if(void* mem = malloc(size + 2 * sizeof(int))) {
*((int*)mem) = kSignature;
*((int*)((unsigned char*)mem + sizeof(int) + sizeof(unsigned char)*size)) = kSignature;
std::cout << "malloc successfully" << std::endl;
return (unsigned char*)mem + sizeof(int);
}
else {
throw std::bad_alloc();
}
}
void operator delete(void *mem) noexcept {
std::cout << "Entering self-define operator delete...\n";
if(mem) {
/* 注意,正常情况下operator delete里没法校验overuns,因为不知道内存大小就没法加偏移量
也许你会说加一个额外参数,例如,void operator delete(void *mem, int extra) noexcept
但这种形式只有在构造函数发生异常时才会调用 */
// 当然有其它方法校验overuns,比如在delete时根据申请内存的大小进行偏移进行主动校验
if((*(int*)((unsigned char*)mem - sizeof(int)) == kSignature)) {
free((unsigned char*)mem - sizeof(int));
std::cout << "free successfully" << std::endl;
}
else{
std::cerr << "memory underruns!!!" << std::endl;
}
}
}
auto test_operator_new_main() -> void {
std::cout << "testing operator new & operator delete for overruns&underruns..." << std::endl;
int *ptr = new int;
*((unsigned char*)ptr - 2) = 'P';
delete ptr;
std::cout << "------------------------------" << std::endl;
}
编译器cmake -G "Visual Studio 15 2017" -A x64 ..
的结果(此运行结果符合设计预期):
testing operator new & operator delete for overruns&underruns...
malloc successfully
Entering self-define operator delete...
memory underruns!!!
------------------------------
编译器cmake -G "MinGW Makefiles" ..
的结果(此运行结果不符合设计预期):
testing operator new & operator delete for overruns&underruns...
malloc successfully
由结果可知在不同编译器下结果有差异,"MinGW Makefiles"时应该时调用标准库的operator delete
,并未使用自定义的版本。
2.4 为了检测运用错误
如2.3节描述。
2.5 为了收集动态分配内存之使用统计信息
如2.3节描述。
2.6 为了增加分配和归还的速度
(标准库提供的)泛用型分配器往往(虽然并不总是)比自定义分配器慢,特别是当自定义分配器专门针对某特定类型对象而设计时。
2.7 为了降低缺省内存管理器带来的空间额外开销
泛用型内存管理器往往(虽然并不总是)不仅比自定义分配器慢,而且往往还使用更多内存,因为它们常常在每一个分配区块上有一些额外开销。
2.8 为了弥补缺省分配器中的非最佳齐位
在x86架构上double如果是8-byte对齐则访问速度最快,但是编译器自带的operator new
并不保证动态分配的double采取8字节对齐。此时,将缺省的operator new
替换为一个8字节版本可使得程序效率大幅提升。
2.9 为了将相关对象成簇集中
如果有些数据往往一起使用,而你有希望处理这些数据时缺页异常的频率降至最低,那么创建一个heap就有意义,使用new
和delete
的placement
版本就可以使得这些数据被成簇集中在尽可能少的内存页上。
2.10 为了获得非传统行为
有时候希望operator new
和operator delete
做编译器默认版没做的事情。例如自定义的operator delete
将归还的内容覆盖为0。
3.编写new
和delete
时需固守常规
3.1 处理0 bytes的内存申请
C++规定,即使客户要求0 bytes,operator new
也得返回一个合法指针,因此重载operator new
时应当使其有能力处理0 byte申请。
在应对0 bytes的内存申请之前需要先介绍EBO,Empty Base Optimization空白基类最优化。即基类如果是空类,则其子类的大小是子类成员的大小。
#include <iostream>
#include <typeinfo>
#include <stdio.h>
namespace test_ebo {
class Empty{};
class Empty2{};
class Derived1 : public Empty {
};
class Derived2 : public Empty {
private:
char c_;
};
class Derived3 : public Empty {
private:
int i_;
};
class MultipleDerived : public Derived1 {
};
class DerivedMultiBase : public Empty, public Empty2 {
};
auto main() -> int {
std::cout << "testing test_ebo......\n" << std::endl;
std::cout << "sizeof(Empty): " << sizeof(Empty) << std::endl; // 1
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << std::endl; // 1
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << std::endl; // 1
std::cout << "sizeof(Derived3): " << sizeof(Derived3) << std::endl; // 4
std::cout << "sizeof(MultipleDerived): " << sizeof(MultipleDerived) << std::endl; // 1
std::cout << "sizeof(DerivedMultiBase): " << sizeof(DerivedMultiBase) << std::endl; // 1
std::cout << typeid(MultipleDerived).name() << std::endl;
std::cout << typeid(DerivedMultiBase).name() << std::endl;
return 0;
}
}
cmake -G "MinGW Makefiles" ..
编译,输出:
sizeof(Empty): 1
sizeof(Derived1): 1
sizeof(Derived2): 1
sizeof(Derived3): 4
sizeof(MultipleDerived): 1
sizeof(DerivedMultiBase): 1
N8test_ebo15MultipleDerivedE
N8test_ebo16DerivedMultiBaseE
处理申请0 bytes的示例:
void* operator new(std::size_t size) {
if(0 == size) {
size = 1; // 将0-byte视为1-byte申请
}
while(true) {
// 尝试分配size bytes
if(分配成功) {
return(指向分配得到内存的指针)
}
// 分配失败:找出目前的new-handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)(*globalHandler)();
else throw std::bad_alloc();
}
}
3.2注意operator new
会被子类继承
class Base {
public:
static void* operator new(size_t size);
};
class Derived: public Base {}; // 假设Derived未声明operator new
Derived* p = new Derived; // 此处调用的是Base::operator new
如果Base
专属的operator new
并非设计用于Base
的子类(实际上往往如此),那么应该在申请子类对象内存的时候使用标准库的operator new
,像下面这样:
void* Base::operator new(size_t size) {
if(size != sizeof(Base)) { // 如果大小不是基类大小则使用标准库的operator new
return ::operator new(size);
}
// ...... // 否则在这处理
}
3.3更多operator new
细节请移步Reference及上手实验
Reference
- Scott Meyers. Effective C++ 改善程序与设计的55个具体做法(第三版), 电子工业出版社,2011.