operator new、operator delete补充讲义

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.了解newdelete的合理替换时机

Understand when it makes sense to replace new and delete.
替换标准库的operator newoperator 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 newoperator delete采取的是中庸之道,需要对每个任务都适度友好,但不对特定任何人有最佳表现。自定义的operator newoperator delete性能胜过缺省版本,定制版的速度比较快,有时候会快很多,而且所需内存比较少,最高可节省50%。对于有些程序而言将标准库的operator newoperator delete替换为自定义的版本是获得重大效能提升的办法之一。

2.3 为了收集使用上的统计数据

在使用自定义的operator newoperator delete之前应该了解开发的软件如何使用动态内存。比如,分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配和归还?它们的运用形态是否随时间改变,即软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所需的最大动态分配量是多少?使用自定义operator newoperator 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就有意义,使用newdeleteplacement版本就可以使得这些数据被成簇集中在尽可能少的内存页上。

2.10 为了获得非传统行为

有时候希望operator newoperator delete做编译器默认版没做的事情。例如自定义的operator delete将归还的内容覆盖为0。

3.编写newdelete时需固守常规

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

  1. Scott Meyers. Effective C++ 改善程序与设计的55个具体做法(第三版), 电子工业出版社,2011.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值