详析内存池

内存池

一.什么是内存池

对于C/C++使用者,在进行内存分配时,可能会频繁调用 new / delete / free / malloc 等标准库内存分配函数,这些函数会造成大量不必要的内存开销。
当我们频繁地动态申请和释放内存时,使用这些系统调用而造成性能损耗的在一定程度上是可观的。

二.内存池的思想

内存池首先申请一块大内存,并将内存分为较小的部分。
每次请求时都会返回其中一个小块(放在应用层上执行,减少系统调用的开销)。

三.内存池的优缺点

优点:

(1) 快于malloc new等系统调用

(2) 由于每个对象的大小都是事先已知的(即无需存储分配元数据(metadata:可以包括:内存块的大小,是否已经被分配或释放,边界地址,用于分配管理![]()的数据结构等))

(3) 几乎没有内存碎片(外部碎片:零散分配而无法满足连续大块的需求(主要在堆内存中);内部碎片:分配的内存块中有一部分浪费(与内存的对齐方式有关))

(4) 无需逐个释放对象。分配器将在调用析构函数后将释放所分配的内存(仅在对象有默认析构函数时才有效)

缺点:

(1) 对象具有固定的大小且必须事先知道(通常不是问题)

(2) 对特定的应用程序需要进行微调(通过使用模板类可以解决)

四.先导知识

4.1 allocator / allocator_traits

"Allocators are classes that define memory models to be used by some parts of the Standard Library, and most specifically, by STL containers."
上述allcator定义来自于cpluscplus reference中的定义,说明allocator是一种定义内存模型的类,用于STL容器。


"This template supplies a uniform interface for allocator types."
而对于 allocator_traits上述语句说明其是一种为allocator type提供统一接口的模板。

4.2 new / operator new / placement new / delete / operator new

(1) new:可分为以下几步:
    1) 调用 operator new 分配一块内存(如果分配失败,会抛出bad_alloc异常)
    2) 调用对象构造函数进行构造

(2) delete: 可分为以下几步:
    1) 调用 operator delete 释放内存
    2) 自动调用析构函数进行清理

(3) operator new: 内存分配函数(形如:void* operator new(size_t size, std::nothrow_t&);):operator new -> malloc -> sbrk/brk/mmap

(4) operator delete: 内存释放函数(形如:void* operator delete(void* ptr);):operator delete -> free -> sbrk/brk/mmap

(5) placement new: operator new的重载版本,允许在指定的内存地址上构造对象(已分配内存,形如:void* operator new(size_t size, void* ptr) noexcept;)

五.关键成员变量

5.1 内存单元(slot_)

1 union slot_ 
2 {
3    value_type element; // 用于存储实际对象
4    slot_* next;        // 用于指向下一个空闲 slot
5 };

为什么使用 union :

        1)union 的所有成员共享同一块内存。

        2)在内存池中,一个 slot 在某个时刻要么存储分配的对象(element),要么是空闲链表的一部分(next),因此可以使用 union 节省内存开销。

5.2 空闲(内存单元)链表(free_slots_)

        链表数据结构;当需要分配内存时,优先从 free_slots_中获取

1 if (freeSlots_) {
2     Slot* result = freeSlots_;
3     freeSlots_ = freeSlots_->next;
4     return reinterpret_cast<pointer>(result);
5 }

5.3 当前分配的内存块 (current_block_)

       指向当前分配的内存块的起始位置,每次调用 allocate_block() 分配新内存块,该指针指向新的内存起始块

1 Slot* newBlock = reinterpret_cast<Slot*>(::operator new(blockSize));
2 newBlock->next = currentBlock_;
3 currentBlock_ = newBlock;

六.关键成员函数

6.1 allocate

  • 设计思路:
    • 优先从空闲链表(freeSlots_)中分配。
    • 如果空闲链表为空且当前内存块已满,则分配新的内存块。
  • 实现细节
    • if (freeSlots_) {
          Slot* result = freeSlots_;
          freeSlots_ = freeSlots_->next;
          return reinterpret_cast<pointer>(result);
      }
      
      if (!currentBlock_ || currentSlot_ >= lastSlot_) {
          allocateBlock();
      }
      
      return reinterpret_cast<pointer>(currentSlot_++);

6.2 deallocate

  • 设计思路:
    • 通过 Slot 的 next 指针将释放的块链接到空闲链表中。
  • 实现细节:
    Slot* slot = reinterpret_cast<Slot*>(p);
    slot->next = freeSlots_;
    freeSlots_ = slot;

6.3 construct

  • 作用:
    • 在分配的内存上构造对象。
  • 实现细节:
    new (p) U(std::forward<Args>(args)...);

6.4 destroy

  • 作用:
    • 调用对象的析构函数。
  • 实现细节:
    p->~U();

6.5 allocate_block

  • 设计思路:
    • 使用 operator new 分配新内存块。
    • 将内存块划分为多个 slot,并链接成空闲链表。
  • 实现细节
    • size_type blockSize = alignUp(BlockSize, alignof(Slot));
      Slot* newBlock = reinterpret_cast<Slot*>(::operator new(blockSize));
      newBlock->next = currentBlock_;
      currentBlock_ = newBlock;
      
      char* body = reinterpret_cast<char*>(newBlock) + sizeof(Slot);
      size_type bodyPadding = alignUp(reinterpret_cast<size_type>(body), alignof(Slot)) - reinterpret_cast<size_type>(body);
      freeSlots_ = reinterpret_cast<Slot*>(body + bodyPadding);
      
      Slot* lastSlot = reinterpret_cast<Slot*>(reinterpret_cast<char*>(newBlock) 
      + blockSize - sizeof(Slot));
      for (Slot* slot = freeSlots_; slot < lastSlot; ++slot) 
      {
          slot->next = slot + 1;
      }
      lastSlot->next = nullptr;

七.在不同编译环境下的优化情况

7.1 Windows(MSVC, visual studio 2022)

1 #include"MemoryPool.h"
2 #include<memory>
3 #include<ctime>
4 #include<iostream>
5
6 using namespace my_memory_pool;
7 const int ELEMS1 = 500;
8 const int ELEMS2 = 100000;
9 int main()
10 {
11    clock_t start;
12
13    memory_pool<size_t> pool;
14    start = clock();
15    for (int i = 0; i < ELEMS1; ++i)
16    {
17        for (int j = 0; j < ELEMS2; ++j)
18        {
19            // 创建元素
20            size_t* x = pool.allocate();
21
22            // 释放元素
23            pool.deallocate(x);
24        }
25    }
26    std::cout << "my_memory_pool 内存池耗时: ";
27    std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";
28
29
30    start = clock();
31    for (int i = 0; i < ELEMS1; ++i)
32    {
33        for (int j = 0; j < ELEMS2; ++j)
34        {
35            size_t* x = new size_t;
36
37            delete x;
38        }
39    }
40    std::cout << "new/delete动态内存分配耗时: ";
41    std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << "\n\n";
42
43    return 0;
44 }

7.2 Ubuntu 22.04.5(g++ 11.4.0)

使用命令 g++ main.cpp -o main

使用 -O2 或 -O3时,new / delete效果更好

-O2:

-O3:

参考:cacay/MemoryPool: An easy to use and efficient memory pool allocator written in C++.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值