C++内存池两种方案解析

目录
  • C++内存池
    • 1、C++内存池分析
    • 2、多此一举方案
    • 3、分时复用改进方案
    • 4、其他的思考

C++内存池

前言:

使用new expression为类的多个实例分配动态内存时,cookie导致内存利用率可能不高,此时我们通过实现类的内存池来降低overhead。从不成熟到巧妙优化的内存池,得益于union的分时复用特性,内存利用率得到了提高。

1、C++内存池分析

在实例化某个类的对象时(在heap而不是stack中),若不使用array new,则每次实例化时都要调用一次内存分配函数,类的每个实例在内存中都有上下两个cookie,从而降低了内存的利用率。然而,array new也有先天的缺陷,即只能调用默认无参构造函数,这对于很多没有提供无参构造函数的类来说是不合适的。

因此,我们可以对于一个没有实例化的类第一次实例化时,先分配一大块内存(内存池),这一大块内存记录在类中,只有上下两个cookie,能够容纳多个实例。后续实例化时,若内存池中还有剩余内存,则不必申请内存分配,只在内存池中分配。内存回收时,将实例所占用的内存回收到内存池中。若内存池中无内存,则再申请分配大块内存。

2、多此一举方案

我们以链表的形式组织内存池,内存池中链表的每个结点是一个小桶,这个桶中装我们实例化的对象。

内存池链表的头结点记录在类中,即以class staic变量的形式存储。组织形式如下:

实现代码如下:

#include <iostream>
using namespace std;
class DemoClass{
public:
    DemoClass() = default;
    DemoClass(int i):data(i){}
    static void* operator new(size_t size);
    static void operator delete(void *);
    virtual ~DemoClass(){}
private:
    DemoClass *next;
    int data;
    static DemoClass *freeMemHeader;
    static const size_t POOL_SIZE;
};
DemoClass * DemoClass::freeMemHeader = nullptr;
const size_t DemoClass::POOL_SIZE = 24;//设定内存池能容纳24个DemoClass对象
void* DemoClass::operator new(size_t size){
    DemoClass* p;
    if(!freeMemHeader){//freeMemHeader为空,内存池中无空间,分配内存
        size_t pool_mem_bytes = size * POOL_SIZE;//内存池的字节大小 = 每个实例的大小(字节数)* 内存池中能容纳的最大实例数
        freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes个字节,因为每个char占用1个字节
        cout << "Info:向操作系统申请了" << pool_mem_bytes << "字节的内存。" << endl;
        for(int i = 0;i < POOL_SIZE - 1; ++i){//将内存池中POOL_SIZE个小块内存,串起来。
            freeMemHeader[i].next = &freeMemHeader[i + 1];
        }
        freeMemHeader[POOL_SIZE - 1].next = nullptr;
    }
    p = freeMemHeader;//取内存池(链表)的头部,分配给要实例化的对象
    cout << "Info:从内存池中取了" << size << "字节的内存。" << endl;
    freeMemHeader = freeMemHeader -> next;//从内存池中删去取出的那一小块地址,即更新内存池
    p -> next = nullptr;
    return p;
}
void DemoClass::operator delete(void* p){
    DemoClass* tmp = (DemoClass*) p;
    tmp -> next = freeMemHeader;
    freeMemHeader = tmp;
}

测试代码如下:

int main(int argc, char* argv[]){
    cout << "sizeof(DemoClass):" << sizeof(DemoClass) << endl;
    size_t N = 32;
    DemoClass* demos[N];
    for(int i = 0; i < N; ++i){
        demos[i] = new DemoClass(i);
        cout << "address of the ith demo:" << demos[i] << endl;
        cout << endl;
    }
    return 0;
}

其结果如下:

可以看到每个DemoClass的实例大小为24字节,内存池一次从操作系统中申请了576个字节的内存,这些内存可以容纳24个实例。上面显示出了每个实例的内存地址,内存池中相邻实例的内存首地址之差为24,即实例的大小,证明了一个内存池的实例之间确实没有cookie。

当内存池中内存用完后,又向操作系统申请了576个字节的内存。

由此,只有每个内存池两侧有cookie,而内存池中的实例不存在cookie,相比于每次调用new expression实例化对象都有cookie,内存池的组织形式确实在形式上提高了内存利用率

那么,有什么问题么

sizeof(DemoClass)等于24

  • int data数据域占4个字节
  • 两个构造函数一个析构函数各占4字节,共12字节
  • 额外的指针DemoClass*,在64位机器上,占8个字节

这样一个DemoClass的大小确实是24字节。wait,what?

我们为了解决cookie带来的内存浪费,引入了指针next,但却又引入了8个字节的overhead,脱裤子放屁,多此一举

这样看来确实没有达到要求,但至少为我们提供了一种思路,不是么?

3、分时复用改进方案

首先我们先回忆下c++ 中的Union:

在任意时刻,联合中只能有一个数据成员可以有值。当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义状态了。

结合我们之前不成熟的内存池,我们发现,当内存池中的桶还没有被分配给实例时,只有next域有用,而当桶被分配给实例后,next域就没什么用了;当桶被回收时,数据域变无用而next指针又需要用到。这不正是union的特性么?

看一下代码实现:

#include <iostream>
using namespace std;
class DemoClass{
public:
    DemoClass() = default;
    DemoClass(int i, double p){
        data.num = i;
        data.price = p;
    }
    static void* operator new(size_t size);
    static void operator delete(void *);
    virtual ~DemoClass(){}
private:
    struct DemoData{
        int num;
        double price;
    };
private:
    static DemoClass *freeMemHeader;
    static const size_t POOL_SIZE;
    union {
        DemoClass *next;
        DemoData data;
    };

};
DemoClass * DemoClass::freeMemHeader = nullptr;
const size_t DemoClass::POOL_SIZE = 24;//设定内存池能容纳24个DemoClass对象
void* DemoClass::operator new(size_t size){
    DemoClass* p;
    if(!freeMemHeader){//freeMemHeader为空,内存池中无空间,分配内存
        size_t pool_mem_bytes = size * POOL_SIZE;//内存池的字节大小 = 每个实例的大小(字节数)* 内存池中能容纳的最大实例数
        freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes个字节,因为每个char占用1个字节
        cout << "Info:向操作系统申请了" << pool_mem_bytes << "字节的内存。" << endl;
        for(int i = 0;i < POOL_SIZE - 1; ++i){//将内存池中POOL_SIZE个小块内存,串起来。
            freeMemHeader[i].next = &freeMemHeader[i + 1];
        }
        freeMemHeader[POOL_SIZE - 1].next = nullptr;
    }
    p = freeMemHeader;//取内存池(链表)的头部,分配给要实例化的对象
    cout << "Info:从内存池中取了" << size << "字节的内存。" << endl;
    freeMemHeader = freeMemHeader -> next;//从内存池中删去取出的那一小块地址,即更新内存池
    p -> next = nullptr;
    return p;
}
void DemoClass::operator delete(void* p){
    DemoClass* tmp = (DemoClass*) p;
    tmp -> next = freeMemHeader;
    freeMemHeader = tmp;
}

对比前一种实现代码,只是构造函数、数据域和指针域的组织形式发生了变化:

  • 由于数据域增加了price项,构造函数中也增加了对应的参数
  • 数据域被集成定义成一个类自定义struct类型
  • 数据域和指针域被组织为union

测试代码依旧:

int main(int argc, char* argv[]){
    cout << "sizeof(DemoClass):" << sizeof(DemoClass) << endl;
    size_t N = 32;
    DemoClass* demos[N];
    for(int i = 0; i < N; ++i){
        demos[i] = new DemoClass(i, i * i);
        cout << "address of the " << i << "th demo:" << demos[i] << endl;
        cout << endl;
    }
    return 0;
}

结果:

可以看到每个DemoClass的实例大小为24字节,一个内存池的实例之间没有cookie。

分析一下sizeof(DemoClass)等于24的缘由:

data数据域占12个字节(int 4字节、double 8字节)。
两个构造函数一个析构函数各占4字节,共12字节。
指针DemoClass,在64位机器上,占8个字节,但由于和数据域使用了union,data数据域12个字节中的前8个字节在适当的时机被看作DemoClass,而不占用额外空间,消除了overhead。
这样一个DemoClass的大小确实是24字节。利用union的分时复用特性,我们消除了初步方案中指针带来的脱裤子放屁效果。

4、其他的思考

细心的读者可能会发现,前面的那两种方案都有共同的小缺陷,即当程序一直实例化而不析构时,内存池会向操作系统申请多次大块内存,而当这些对象一起回收时,内存池中的剩余桶数会远大于设定的POOL_SIZE的大小,这个峰值多大取决于类实例化和回收的时机。

另外,内存池中的内存暂时不会回收给操作系统,峰值很大可能会对内存分配带来一些影响,不过这却不属于内存泄漏。在以后的文章中,我们可能会讨论一些性能更好的内存分配方案。

以上就是C++内存池两种方案对比的详细内容,更多关于C++内存池的资料请关注我们其它相关文章!望大家以后多多支持我们!

(0)

相关推荐

  • C++ 实现高性能HTTP客户端

    目录 一.什么是Http Client 二.请求的过程 1. 创建Http任务 2. 填写header并发出 3. 处理返回结果 三.高性能的基本保证 1. 异步调度模式 2. 连接复用 3. 解锁其他功能 一.什么是Http Client Http协议,是全互联网共同的语言,而Http Client,可以说是我们需要从互联网世界获取数据的最基本方法,它本质上是一个URL到一个网页的转换过程.而有了基本的Http客户端功能,再搭配上我们想要的规则和策略,上至内容检索下至数据分析都可以实现了. 继

  • C++stack与queue模拟实现详解

    目录 stack与queue模拟实现 stack queue 为什么选择deque作为stack和queue的底层默认容器 总结 stack与queue模拟实现 在stl中,stack(栈)与queue(队列)都是容器适配器. 什么是容器适配器呢? 适配器(adaptor)是标准库中通用的概念,包括容器适配器.迭代器适配器和函数适配器.本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制.容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现.简单来说其实就是利用现有的容

  • C++高并发内存池的整体设计和实现思路

    目录 一.整体设计 1.需求分析 2.总体设计思路 3.申请内存流程图 二.详细设计 1.各个模块内部结构详细剖析 2.设计细节 三.测试 一.整体设计 1.需求分析 池化技术是计算机中的一种设计模式,内存池是常见的池化技术之一,它能够有效的提高内存的申请和释放效率以及内存碎片等问题,但是传统的内存池也存在一定的缺陷,高并发内存池相对于普通的内存池它有自己的独特之处,解决了传统内存池存在的一些问题. 附:实现一个内存池管理的类方法 1)直接使用new/delete.malloc/free存在的问

  • C++关于类结构体大小和构造顺序,析构顺序的测试详解

    目录 总结 #include <iostream> using namespace std; /** 1. c++的类中成员若不加修饰符的话,默认是private 2. 调用构造函数时,先递归调用最顶级的父类构造函数,再依次到子类的构造函数. 3. 调用析构函数时相反,先调用最底层的子类析构函数,再依次到父类的构造函数. 4. 空类的sizeof(A)大小为1,多个空类继承后的子类大小也是1 */ class A{ public: A() { cout<<"A const

  • C++中priority_queue模拟实现的代码示例

    目录 priority_queue概述 priority_queue定义 priority_queue特点 构造函数 修改相关函数 push pop 容量相关函数 size empty 元素访问相关函数 top 总结 priority_queue概述 priority_queue定义 优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最高优先权的元素. priority_queue特点 优先队列是一种容器适配器,首先要包含头文件 #include<queue>, 他和queu

  • C++项目基于HuffmanTree实现文件的压缩与解压缩功能

    目录 前言 一.文件压缩 1.文件压缩的概念 2.为什么需要压缩 3.压缩的分类 4.压缩的方法 二.HuffmanTree文件压缩与解压缩 1.HuffmanTree的概念 2.HuffmanTree的构建 3.文件压缩 4.文件解压缩 三.HuffmanTree压缩解压缩碰到的问题 1.创建优先级队列要使用自己写的仿函数 2.自定义类型结构体类型相加和仿函数要重载operator+和operator> 3.剔除在HuffmanTree出现0次的字符,不用统计出现0次的字符 4.如果在解压缩时

  • 超详细讲解Linux C++多线程同步的方式

    目录 一.互斥锁 1.互斥锁的初始化 2.互斥锁的相关属性及分类 3,测试加锁函数 二.条件变量 1.条件变量的相关函数 1)初始化的销毁读写锁 2)以写的方式获取锁,以读的方式获取锁,释放读写锁 四.信号量 1)信号量初始化 2)信号量值的加减 3)对信号量进行清理 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> #include<stdio.h&

  • C++内存池的简单实现

    目录 一.内存池基础知识 1.什么是内存池 1.1 池化技术 1.2 内存池 2.内存池的作用 2.1 效率问题 2.2 内存碎片 3.内存池技术的演进 二.简易内存池原理 1.整体设计 1.1 内存池结构 1.2 申请内存 1.3 释放内存 2.详细剖析 2.1 blockNode结构 2.2 单个对象的大小 3.性能比较 三.简易内存池完整源码 一.内存池基础知识 1.什么是内存池 1.1 池化技术 池化技术是计算机中的一种设计模式,主要是指:将程序中经常要使用的计算机资源预先申请出来,由程

  • C++ const关键字分析详解

    目录 C语言中修饰变量 C语言中修饰指针变量 C语言中修饰函数的参数 C语言中修饰函数的返回值 C++中修饰变量 C++中修饰函数的参数 C++中修饰函数的返回值 C++中修饰类的成员函数 C++中修饰类的成员变量 总结 C语言中修饰变量 在C语言中,被const修饰的是一个不能被修改的变量. C语言中修饰指针变量 #include <stdio.h> //代码1 void test1() { int n = 10; int m = 20; int *p = &n; *p = 20;/

  • C++手写内存池的案例详解

    引言 使用new expression为类的多个实例分配动态内存时,cookie导致内存利用率可能不高,此时我们通过实现类的内存池来降低overhead.从不成熟到巧妙优化的内存池,得益于union的分时复用特性,内存利用率得到了提高. 原因 在实例化某个类的对象时(在heap而不是stack中),若不使用array new,则每次实例化时都要调用一次内存分配函数,类的每个实例在内存中都有上下两个cookie,从而降低了内存的利用率.然而,array new也有先天的缺陷,即只能调用默认无参构造

随机推荐