项目:高并发内存池(1)定长内存池

  1. 什么是高并发内存池

本项目是实现一个高并发内存池,原型是google的一个开源项目tcmalloc,全称Thread-Caching Malloc,线性缓存的malloc,实现了高效多线程内存管理,用于替代malloc,free这样的系统分配的相关内存函数

  1. 项目所需技术

C/C++,数据结构(链表,哈希桶),操作系统的内存管理,单例模式,多线程,互斥锁等基础知识

  1. 内存池的介绍

  • 1.池化技术

“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过 量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快 捷,大大提高程序运行效率。 避免频繁创建和销毁 在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务 器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端 的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠 状态。

  • 2.内存池

内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接 向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操 作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放

  • 3.内存池主要解决的问题

  1. 每次申请资源都要频繁向操作系统申请,低效(C标准库提供的接口malloc/free去进程地址空间,堆上申请)

  1. 内存碎片

  1. 内碎片

内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。

  1. 外碎片

外部碎片是一些空闲的 连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请 需求。内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。

  • 4.malloc

C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获 取内存的, 而malloc就是一个内存池。malloc() 相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程 序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方 式有很多种,一般不同编译器平台用的都是不同的

进程调用malloc C语言接口 ——>malloc底层调用brk()系统接口 ——> 操作系统在内核执行brk()

——>brk()返回,malloc找到空闲内存块 ——>malloc返回

本次项目的原型tcmalloc在多线程环境下会比malloc更快

  1. 设计一个定长的内存池

通常申请内存,我们使用的是malloc,malloc适用范围广,可用在任何场景也就决定了他不会有很高的性能,为了解决这一点,我们可以先尝试设计一个定长的内存池

定长内存池需要提前申请一块很大的内存

大小固定内存申请释放,可以让性能发挥到极致,也不需要考虑内存碎片问题

char* _memery指向一块定长的内存空间,这是我么能使用事先申请的空间

_freeList指向我们所释放的之前从内存池申请过来的资源,后续也可以被我们继续使用

要设计一个定长内存池,那么我们一次需要开多少空间呢

  1. 我们可以使用非类型模板参数 template<size_t N> 可以代表一次向内存池申请的资源大小

  1. 也可以直接使用template<class T>这里的模板参数就代表将来我们从内存池一次所能申请的资源就是大小为T

  1. 设计成员变量

1)一个指向大小固定的内存的指针 char* _memery

2)一个指向被释放的资源的指针 char* _freeList 由于申请是一个一个申请的,所以将来释放的时候也是一个一个释放的,我们可以用单链表的形式将所释放的资源组织起来

3)由于我们并不知道内存池中还剩下多少资源可以继续被我们申请,所以我们需要使用一个成员变量保存当前内存池char* _memery还能指向多大字节数的空间

  1. 为了解决我们的代码的跨平台问题,也就是32位环境下指针大小为4byte,而64位环境下是8byte,我们想要申请我们的freeList所指向的链表结构的每一个节点需要保存下一个内存块的的地址,我们可以设计节点的头4/8个字节是存储指针的区域,该如何用统一的方式设计代码呢?

如果是32位环境我们可以用这样的代码

*(int*)obj=nullptr;//但在64下要用*(long long*)解引用访问8byte
//ObjectPool.h
#pragma once
#include<iostream>
#include<Windows.h>

inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
    void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    // linux下brk mmap等
#endif
    if (ptr == nullptr)
        throw std::bad_alloc();
    return ptr;
}

template<class T>
class ObjectPool
{
public:
    //申请一块T大小的空间
    T* New() 
    {
        T* obj = nullptr;//定义obj的指针用于指向本次申请的空间资源
        // 优先使用还回来内存块对象 再次重复利用
        if (_freeList) //如果指针为空说明目前没有还回来的
        {
            void* next = *((void**)_freeList);//让next记录freeList的头 指针大小 字节数,里面记录了下一个结点的地址
            obj = (T*)_freeList;//把freelist的第一个节点分配给obj
            _freeList = next;//让freelist指向下一个节点
        }
        //使用剩余的_memery所指向的空间
        else
        {
            // 剩余内存不够一个对象大小时,则重新开大块空间
            if (_remainBytes < sizeof(T))
            {
                _remainBytes = 128 * 1024;//开大小为128*1024byte的大块空间
                //使用c库提供的接口,底层也是系统调用
                //_memory = (char*)malloc(_remainBytes);
                _memory = (char*)SystemAlloc(_remainBytes >> 13);//因为是windows平台,所以采用windows提供的接口

                if (_memory == nullptr)//这里说明上面开空间失败了
                {
                    throw std::bad_alloc();
                }
            }
            obj = (T*)_memory;//将来*obj可以拿到T大小的内存空间
            //考虑我要申请的一个调用内存池的申请空间大小如果小于该平台下指针大小,那就申请还是申请指针大小的空间,不然freelist每个节点存不下一个指针
            size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
            _memory += objSize;//一次性申请要让指针往后移相应的距离
            _remainBytes -= objSize;//剩余的空间相应减少
        }
        // 定位new,显示调用T的构造函数初始化
        new(obj)T;
        return obj;
    }
    void Delete(T* obj)
    {
        // 显示调用析构函数清理对象
        obj->~T();

        // 头插
        *(void**)obj = _freeList;//释放的资源插入freelist的链表,obj的指针域存放freelist所指向的第一个结点
        _freeList = obj;
    }
private:
    char* _memory = nullptr; // 指向大块内存的指针
    size_t _remainBytes = 0; // 大块内存在切分过程中剩余字节数
    void* _freeList = nullptr; // 还回来过程中链接的自由链表的头指针
};
  1. 测试性能

#include"ObjectPool.h"
#include<vector>
#include<time.h>
using std::cout;
using std::cin;
using std::endl;

struct TreeNode
{
    int _val;
    TreeNode* _left;
    TreeNode* _right;

    TreeNode()
        :_val(0)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

void TestObjectPool()
{
    // 申请释放的轮次
    const size_t Rounds = 5;

    // 每轮申请释放多少次
    const size_t N = 100000;

//使用标准库中的new/delete
    std::vector<TreeNode*> v1;
    v1.reserve(N);

    
    size_t begin1 = clock();
    for (size_t j = 0; j < Rounds; ++j)
    {
        for (int i = 0; i < N; ++i)
        {
            v1.push_back(new TreeNode);
        }
        for (int i = 0; i < N; ++i)
        {
            delete v1[i];
        }
        v1.clear();
    }

    size_t end1 = clock();


    //使用我们写的定长内存池
    std::vector<TreeNode*> v2;
    v2.reserve(N);

    ObjectPool<TreeNode> TNPool;
    size_t begin2 = clock();
    for (size_t j = 0; j < Rounds; ++j)
    {
        for (int i = 0; i < N; ++i)
        {
            v2.push_back(TNPool.New());
        }
        for (int i = 0; i < N; ++i)
        {
            TNPool.Delete(v2[i]);
        }
        v2.clear();
    }
    size_t end2 = clock();

    //比较效率
    cout << "new cost time:" << end1 - begin1 << endl;
    cout << "object pool cost time:" << end2 - begin2 << endl;
}
int main()
{
    TestObjectPool();
    return 0;
}

由此看来,我们的这种设计方案确实效率更高,接下来就要进一步学习高并发内存池

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值