内存池(C++)v1
文章目录
- 项目背景介绍
- 什么是内存池?
- 为什么要做内存池?
- 内存池的应用场景:
- 内存池在代码中的应用
- 内存池的缺点
- 项目前置知识
- std::allocator
- 内存碎片问题
- 自旋锁的概念
- 自旋锁 vs. 互斥锁
- 内存池版本1项目框架
- 项目代码
- MemoryPool.h
- MemoryPool.cpp
- 实验结果
- 为什么内存池需要为不同类型的元素分配内存,但MemoryPool类和HashBucket类却不需要写成类模板?
- 为什么lastSlot_ 的值等于newBlock + BlockSize - sizeof(slot_type_) + 1 而不是newBlock + BlockSize - sizeof(slot_type_) ?
- 无锁数据结构
- 内存对齐的意义是什么?
项目背景介绍
什么是内存池?
为什么要做内存池?
内存池的应用场景:
内存池在代码中的应用
内存池的缺点
项目前置知识
std::allocator
https://www.cnblogs.com/wpcockroach/archive/2012/05/10/2493564.html
https://blog.csdn.net/justaipanda/article/details/7790355
https://cplusplus.com/reference/memory/allocator/
https://cplusplus.com/reference/memory/allocator_traits/
内存碎片问题
该内存池项目哈希桶的思想借鉴了STL allocator:
https://blog.csdn.net/LF_2016/article/details/53511648
具体每个小内存池的实现参照了GitHub上的项目: 项目:https://github.com/cacay/MemoryPool(学习本文项目之前建议先看一下该项目,更基础) 对应文档:https://github.com/AngryHacker/articles/blob/master/src/code_reading/memorypool.md
自旋锁的概念
自旋锁 vs. 互斥锁
内存池版本1项目框架
主要框架如上图所示,主要就是维护一个哈希桶 Hash Bucket,里面每项对应一个内存池 MemoryPool,哈希桶中每个内存池的块大小 BlockSize 是相同的(4096字节,当然也可以设置为不同的),但是每个内存池里每个块分割的大小(槽大小)SlotSize是不同的,依次为8,16,32,…,512字节(需要的内存超过512字节就用new/malloc),用户申请不同大小的内存就通过哈希桶的映射找到相应(槽大小) SlotSize 的内存池向其申请,比如用户分别申请8字节和12字节的内存,则经过哈希函数的计算找到槽大小为8字节的内存池和槽大小为16字节(哈希函数向上取整,因为分配给用户的内存块只能大不能小)的内存池分别分配内存给用户,这样设置的好处是可以保证内存碎片在可控范围内。
注:到这里大家可能想问为什么用户申请超过512字节的内存就直接调用 new/malloc 等系统调用?
答:因为内存池主要解决的是 小内存带来的内存碎片问题 和 小内存频繁申请释放带来的性能问题。但如果是用户申请较大块的内存依然选择通过内存池分配内存则会导致内存池频繁因为管理的内存不足而向系统申请内存,这样既没有起到性能优化的作用同时增加的程序的复杂度。(ps:频繁申请小内存带来的问题可参考:https://blog.csdn.net/LF_2016/article/details/53511648) 介绍完大致框架之后再来看一下每个内存池的内部结构图:
上述两个图已经大致介绍了该项目的结构,下面我将以一个整体流程图解读的方式,给大家介绍该内存池项目:
项目代码
完整代码看我github仓库
MemoryPool.h
#pragma once #include <atomic>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <memory>
#include <mutex>namespace MemoryPoolv1 {#define MEMORY_POOL_NUM 64
#define SLOT_BASE_SIZE 8
#define MAX_SLOT_SIZE 512/* 具体内存池的槽大小没法确定,因为每个内存池的槽大小不同(8的倍数)
所以这个槽结构体的sizeof 不是实际的槽大小 */struct Slot {// 原子指针// std::atomic 类型通过原子操作来避免这一问题,确保同一时间只有一个线程能够对指针执行修改操作。std::atomic<Slot*> next;
}class MemoryPool {
public:MemoryPool(size_t BlockSize = 4096);~MemoryPool();void init(size_t);void* allocate();void deallocate(void*);private:void allocateNewBlock();size_t padPointer(char* p, size_t align);// 使用CAS操作进行无锁入队和出队bool pushFreeList(Slot* slot);Slot* popFreeList();private:// 内存块大小int BlockSize_;// 槽大小int SlotSize_;// 指向内存池管理的首个实际内存块Slot* firstBlock_;// 指向当前未被使用过的槽Slot* curSlot_;// 指向空闲的槽(被使用过后又被释放的槽)std::atomic<Slot*> freeList_;// 作为当前内存块中最后能够存放元素的位置标识(超过该位置需申请新的内存块)Slot* lastSlot_;//std::mutex mutexForFreeList_; // 保证freeList_在多线程中操作的原子性// 保证多线程情况下避免不必要的重复开辟内存导致的浪费行为std::mutex mutexForBlock_;
};class HashBucket {
public:static void initMemoryPool();static MemoryPool& getMemoryPool(int index);static void* useMemory(size_t size) {if(size <= 0) {return nullptr;} // 大于512字节的内存,则使用newif(size > MAX_SLOT_SIZE) {return operator new(size);}// 相当于size / 8 向上取整(因为分配内存只能大不能小return getMemoryPool(((size + 7) / SLOT_BASE_SIZE) - 1).allocate();}static void freeMemory(void* ptr, size_t size) {if(!ptr) {return;}if(size > MAX_SLOT_SIZE) {operator delete(ptr);return;}getMemoryPool(((size + 7) / SLOT_BASE_SIZE) - 1).deallocate(ptr);}template <typename T, typename... Args>friend T* newElement(Args&&... args);template <typename T>friend void deleteElement(T* p);
};template <typename T, typename... Args>
T* newElement(Args&&... args) {T* p = nullptr;// 根据元素大小选取合适的内存池分配内存if((p = reinterpret_cast<T*>(HashBucket::useMemory(sizeof(T)))) != nullptr) {// 在分配的内存上构造对象new (p) T(std::forward<Args>(args)...);}return p;
}template <typename T>
void deleteElement(T* p) {// 对象析构if(p) {p->~T();// 内存回收HashBucket::freeMemory(reinterpret_cast<void*>(p), sizeof(T));}
}
} // namespace MemoryPoolv1
MemoryPool.cpp
#include "MemoryPool.h"namespace MemoryPoolv1 {
MemoryPool::MemoryPool(size_t BlockSize): BlockSize_(BlockSize), SlotSize_(0), firstBlock_(nullptr), curSlot_(nullptr), freeList_(nullptr), lastSlot_(nullptr) {}MemoryPool::~MemoryPool() {// 把连续的block删除Slot* cur = firstBlock_;while(cur) {Slot* next = cur->next;// 等同于 free(reinterpret_cast<void*>(firstBlock_));// 转化为 void 指针,因为 void 类型不需要调用析构函数,只释放空间// 在 C++ 中,operator delete 需要的是一个 void* 类型的指针,而不是特定类型的指针。因此,我们需要将 cur 从 Slot* 类型转换为 void* 类型,这样才能传递给 operator delete。// operator delete 是一个低级别的内存释放操作符,通常与 operator new 配对使用。// delete 是一个更高级的操作符,通常与 new 配对使用。它不仅释放内存,还会调用对象的析构函数。operator delete(reinterpret_cast<void*>(cur));cur = next;}
}void MemoryPool::init(size_t size) {assert(size > 0);SlotSize_ = size;firstBlock_ = nullptr;curSlot_ = nullptr;freeList_ = nullptr;lastSlot_ = nullptr;
}void* MemoryPool::allocate() {// 优先使用空闲链表中的内存槽Slot* slot = popFreeList();if(slot != nullptr) {return slot;}Slot* temp;{std::lock_guard<std::mutex> lock(mutexForBlock_);if(curSlot_ >= lastSlot_) {// 当前内存块已无内存槽可用,开辟一块新的内存allocateNewBlock();}temp = curSlot_;// 这里不能直接 curSlot_ += SlotSize_ 因为curSlot_是Slot*类型,所以需要除以SlotSize_再加1curSlot_ += SlotSize_ / sizeof(Slot);}return temp;
}void MemoryPool::deallocate(void* ptr) {if(!ptr) {return;}Slot* slot = reinterpret_cast<Slot*>(ptr);pushFreeList(slot);
}void MemoryPool::allocateNewBlock() {//std::cout << "申请一块内存块,SlotSize: " << SlotSize_ << std::endl;// 头插法插入新的内存块void* newBlock = operator new(BlockSize_);reinterpret_cast<Slot*>(newBlock)->next = firstBlock_;firstBlock_ = reinterpret_cast<Slot*>(newBlock);char* body = reinterpret_cast<char*>(newBlock) + sizeof(Slot*);// 计算对齐需要填充内存的大小size_t paddingSize = padPointer(body, SlotSize_);curSlot_ = reinterpret_cast<Slot*>(body + paddingSize);// 超过该标记位置,则说明该内存块已无内存槽可用,需向系统申请新的内存块lastSlot_ = reinterpret_cast<Slot*>(reinterpret_cast<size_t>(newBlock) + BlockSize_ - SlotSize_ + 1);
}// 让指针对齐到槽大小的倍数位置
size_t MemoryPool::padPointer(char* p, size_t align) {// uintptr_t类型// 是 C++ 标准库(定义于<cstdint>头文件)提供的一个无符号整型类型。// 特点:专门设计用于存放指针值,以整数方式操作内存地址。// 优点:保证能安全容纳任何指针地址,无论是32位还是64位架构下,都能正确表示。// reinterpret_cast的特性:// 最底层的强制类型转换符,直接按二进制值转换,不考虑语义。// 把内存地址从指针形式(地址)转换成整数形式,以便进行数学计算。// align 是槽大小uintptr_t result = reinterpret_cast<uintptr_t>(p);return (align - (result % align)) % align;
}// 实现无锁入队操作
bool MemoryPool::pushFreeList(Slot* slot) {while(true) {// 获取当前头节点// load() 方法用于从一个原子变量读取当前值(线程安全的读取)。// 只保证安全读,但暂不要求内存同步其他数据状态。// 因为后续真正关键的同步是在CAS中。// oldHead// |// v// freeList_ -> SlotA -> SlotB -> nullptrSlot* oldHead = freeList_.load(std::memory_order_relaxed);// 将新节点的 next 指向当前头节点// 相当于 slot->next = oldHead; // 普通链表// slot -> oldHead -> SlotA -> SlotB -> nullptrslot->next.store(oldHead, std::memory_order_relaxed);// 尝试将新节点设置为头节点// 比较当前链表头(freeList_)是否等于之前读到的oldHead// std::memory_order_release表示若CAS成功,之前的所有操作(尤其是slot->next.store)对其他线程变为可见(内存同步完成)。// 相当于发布一个“同步成功”的信号。// memory_order_relaxed(失败情况):// 若失败,暂不要求严格的内存同步,因为失败后立即重试。if(freeList_.compare_exchange_weak(oldHead, slot, std::memory_order_release, std::memory_order_relaxed)) {return true;}// 失败:说明另一个线程可能已经修改了 freeList_// CAS 失败则重试}
}// 实现无锁出队操作
Slot* MemoryPool::popFreeList() {while(true) {// 生产者线程用release发布数据;// 消费者线程用acquire获取最新发布的数据。// 用memory_order_acquire确保了线程间正确的数据同步Slot* oldHead = freeList_.load(std::memory_order_acquire);if(oldHead == nullptr) {// 队列为空return nullptr;}// 在访问 newHead 之前再次验证 oldHead 的有效性Slot* newHead = nullptr;try {newHead = oldHead->next.load(std::memory_order_relaxed);} catch(...) {// 如果返回失败,则continue重新尝试申请内存continue;}// 尝试更新头结点// 原子性地尝试将 freeList_ 从 oldHead 更新为 newHead// bool compare_exchange_weak(T& expected, T desired, // memory_order success_order,// memory_order failure_order);// 比较原子变量当前值与expected是否相等:// 如果相等:// 原子地将当前值修改为desired(期望值)。// 返回true表示CAS成功。// success_order CAS成功时的内存顺序 memory_order_acquire// failure_order CAS失败时的内存顺序 memory_order_relaxedif(freeList_.compare_exchange_weak(oldHead, newHead, std::memory_order_acquire, std::memory_order_relaxed)) {return oldHead;}// 失败:说明另一个线程可能已经修改了 freeList_// CAS 失败则重试}
}void HashBucket::initMemoryPool() {for(int i = 0; i < MEMORY_POOL_NUM; ++i) {getMemoryPool(i).init((i + 1) * SLOT_BASE_SIZE);}
}// 单例模式
MemoryPool& HashBucket::getMemoryPool(int index) {// static 关键字确保该数组仅第一次调用时初始化一次,整个程序生命周期中只存在一份。// 也就是说,所有对getMemoryPool函数的调用,都会访问同一个内存池数组。static MemoryPool MemoryPool[MEMORY_POOL_NUM];return MemoryPool[index];
}}
实验结果
这个实验结果比new delete还差,我猜应该是这样的:
跟并发开销大于收益:
内存池实现中使用了较复杂的 无锁队列(CAS) 机制,每次操作可能涉及多次原子操作和循环重试,这在单线程环境下是绝对的额外负担,且即使在多线程环境下,如果争用频繁,也会带来性能损耗。
注意 “无锁队列(CAS)”前后加了一个空格,避免跟上一段语法粘连。
为什么内存池需要为不同类型的元素分配内存,但MemoryPool类和HashBucket类却不需要写成类模板?
为什么lastSlot_ 的值等于newBlock + BlockSize - sizeof(slot_type_) + 1 而不是newBlock + BlockSize - sizeof(slot_type_) ?
无锁数据结构
这里主要实现一个基于原子操作的无锁队列来替代原有使用互斥锁的实现。主要修改freeList_的管理方式。属于一种常用的无锁并发编程技术,通常使用于内存池或对象池管理,以避免使用锁,从而提高并发性能
内存对齐的意义是什么?
- 1 提高访问效率:
缓存对齐:cpu的缓存通常以固定大小的块(如64字节或128字节)为单位,如果内存对齐,数据可以完全放入一个缓冲块中,提高缓存命中率。 内存总线对齐:现代内存通常是按块传输(如4、8、16字节)。未对齐的访问可能导致额外的内存访问,降低效率 - 2 满足硬件要求:某些硬件架构要求特定数据类型对齐,不对齐可能导致硬件异常,即使不严格要求对齐的平台,不对齐也可能导致性能下降或未定义行为。
- 3 保证兼容性:
数据结构标准化:C++标准库的数据结构通常要求对齐,例如std::allocator分配的内存默认按最大对齐要求对齐。 跨平台兼容:对齐内存可以确保代码在不同架构下正确运行,避免潜在的问题。
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!