C++高性能并发内存池:三种Cache的设计及其内存申请释放

前面我们已经学习写了一个简单地定长内存池。并且设计出来了高性能并发内存池的框架,接下来我们就来写代码

本期相关的代码已经上传到作者的个人gitee:高并发内存池: 个人学习的项目------高并发内存池喜欢请点个赞谢谢

目录

Common

ThreadCache

设计

内存对齐规则

TLS无锁线程访问

源代码

CentralCache

设计

源代码

PageCache

设计

源代码


Common

共同需要的代码

cpp 复制代码
#pragma once
#include"MemoryAllocator.h"
#include <iostream>
#include <vector>
#include<algorithm>
#include <cstdlib>  
#include<stdexcept>
#include<cassert>
#include<thread>
#include<mutex>
#include<memory>
#include <stddef.h>  // 跨平台定义 size_t,必须包含

#ifdef _WIN32
// Windows 平台:VirtualAlloc 需要 windows.h
// 同时定义 NOMINMAX 避免与 std::min 冲突(若后续使用)
	#ifndef NOMINMAX
		#define NOMINMAX
	#endif
	#include <windows.h>
#else
	// Linux 平台:mmap、sysconf 需要以下头文件
	#include <sys/mman.h>   // mmap, MAP_FAILED, PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANONYMOUS
	#include <unistd.h>     // sysconf, _SC_PAGESIZE
#endif

using std::cout;
using std::endl;
using std::vector;
//内存池可申请的最大的内存------256KB
static const int MAX_SIZE = 256 * 1024;
static const int NFRESSLISTS = 208;
static const size_t NPAGES = 129;
static const size_t PAGE_SHIFT = 13;


// 跨平台类型定义:PageID_
// 适用:Windows (32/64位) + Linux (32/64位)

// 1. 64 位 Windows 系统
#if defined(_WIN64)
	typedef unsigned long long PageID;
// 2. 32 位 Windows 系统
#elif defined(_WIN32)
	typedef size_t PageID;
// 3. Linux 系统(自动适配 32/64 位)
#elif defined(__linux__)
	typedef size_t PageID;
// 4. 不支持的平台(报错提示)
#else
	#error "当前仅支持 Windows 和 Linux 系统!"
#endif




//给一个对象取前4/8字节
static inline void*& NextObject(void* object)
{
	return *(void**)object;
}
//管理好切分的小对象的链表
class FreeList
{
	private:
		void* freelist_=nullptr;
		size_t maxSize_=1;
		size_t size_ = 0;
	public:
		void Push(void* object)
		{
			//头插
			/*if (object == nullptr)
			{
				throw "申请的对象内存为空";
			}*/
			assert(object);
			NextObject(object) = freelist_;
			freelist_ = object;
			++size_;
		}
		void PushRange(void*start,void* end,size_t n)
		{
			NextObject(start) = freelist_;
			freelist_ = start;
			size_ += n;
		}
		void* Pop()
		{
			//头删
			/*if (freelist_ == nullptr)
			{
				throw "内存链表为空";
			}*/
			assert(freelist_);
			void* object = freelist_;
			freelist_ = NextObject(object);
			--size_;
			return object;
		}
		void PopRange(void*& start, void*& end, size_t n)
		{
			assert(n>=size_);
			start = freelist_;
			end = start;
			for (int i = 0; i < n - 1; ++i)
			{
				end = NextObject(end);
			}
			freelist_ = NextObject(end);
			NextObject(end) = nullptr;
			size_ -= n;
		}
		//判断是否为空
		bool Empty()
		{
			return freelist_ == nullptr;
		}
		size_t& MaxSize()
		{
			return maxSize_;
		}
		size_t size()
		{
			return size_;
		}
};


//对齐映射规则
//以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上
class Alignment
{
		// 整体控制在最多10%左右的内碎片浪费
		// [1,128]    8byte对齐    freelist[0,16)
		// [128+1,1024]  16byte对齐    freelist[16,72)
		// [1024+1,8*1024] 128byte对齐   freelist[72,128)
		// [8*1024+1,64*1024] 1024byte对齐   freelist[128,184)
		// [64*1024+1,256*1024] 8*1024byte对齐   freelist[184,208)
	private:
		/*size_t _RoundUP(size_t size, size_t  Align)
		{
			//方式一:普通运算
			size_t alignsize = size;
			if (size %8!=0)
			{
				return (size / Align + 1) * Align;
			}
			else
			{
				return alignsize;
			}
			return alignsize;
		}*/
		//方式二:位运算
		//static防止多次定义 constexpr编译期计算 inline建议编译器编译期内联
		static constexpr inline size_t _RoundUP(size_t size, size_t  Align)
		{
			return (size + Align - 1) & ~(Align - 1);
			//比如size=5,则二进制为00000101,Align为8,则Align - 1为00000111
			// size + Align - 1为00001100,即12
			//~为按位取反, ~(Align - 1)为11111000
			//&为按位与,当对应位数均为1时方为1
			//00001100&11111000=>
			//00001100
			//11111000
			//=>00001000,为8
		}

		//配合Index的子函数
		/*static inline size_t _Index(size_t bytes, size_t align_shift)
		{
			return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
		}*/
	public:
		//频繁调用的小函数常见写法
		static constexpr inline size_t RoundUP(size_t size)
		{
			if (size <= 128)//128B
			{
				return _RoundUP(size, 8);
			}
			else if (size < 1024)//1KB
			{
				return _RoundUP(size, 16);
			}
			else if (size <= 8 * 1024)//8KB
			{
				return _RoundUP(size, 128);
			}
			else if (size <= 64 * 1024)//64KB
			{
				return _RoundUP(size, 1024);
			}
			else if (size <= 256 * 1024)//256KB
			{
				return _RoundUP(size, 8 * 1024);
			}
			else
			{
				throw std::runtime_error("申请内存过大");
				return 0;
			}
		}
		//计算映射到哪个桶内的自由链表
		//这里除法计算的都是2的幂数,现代主流编译器(MSVC、GCC、Clang会将其转化为位运算)
		static constexpr inline size_t Index(size_t size)
		{
			size_t aligned = RoundUP(size);  // 先对齐,可能抛出异常

			if (aligned <= 128) // 8字节对齐区间 [8, 128]
			{
				// 桶索引:8→0, 16→1, ..., 128→15
				return aligned / 8 - 1;
			}
			else if (aligned <= 1024) // 16字节对齐区间 [144, 1024]
			{
				// 起始144对应桶16,步长16
				return (aligned - 144) / 16 + 16;
			}
			else if (aligned <= 8 * 1024)  // 128字节对齐区间 [1152, 8192]
			{
				// 起始1152对应桶72,步长128
				return (aligned - 1152) / 128 + 72;
			}
			else if (aligned <= 64 * 1024) // 1024字节对齐区间 [9216, 65536]
			{
				// 起始9216对应桶128,步长1024
				return (aligned - 9216) / 1024 + 128;
			}
			else if (aligned <= 256 * 1024)  // 8*1024字节对齐区间 [73728, 262144]
			{
				// 起始73728对应桶184,步长8192
				return (aligned - 73728) / (8 * 1024) + 184;
			}
			else // 理论上不会到达这里,因为 RoundUP 已经对过大 size 抛异常
			{
				throw std::runtime_error("申请内存过大");
			}
		}
		//第一个版本,可能效率更高,但是可读性略差
		//static constexpr inline size_t Index(size_t size) 
		//{
		//	size_t aligned = RoundUP(size);
		//	if (aligned <= 128)
		//		return _Index(aligned, 3);                     // 8字节对齐
		//	else if (aligned <= 1024)
		//		return _Index(aligned - 128, 4) + 16;          // 16字节对齐
		//	else if (aligned <= 8 * 1024)
		//		return _Index(aligned - 1024, 7) + 72;         // 128字节对齐
		//	else if (aligned <= 64 * 1024)
		//		return _Index(aligned - 8 * 1024, 10) + 128;   // 1024字节对齐
		//	else if (aligned <= 256 * 1024)
		//		return _Index(aligned - 64 * 1024, 13) + 184;  // 8192字节对齐
		//	else
		//		throw std::runtime_error("申请内存过大");
		//}
	
		// 一次thread cache从中心缓存获取多少个
		static size_t NumMoveSize(size_t size)
		{
			assert(size >0);

			// [2, 512], 一次批量移动多少个对象的(慢启动)上限值
			// 小对象一次批量上限高
			// 小对象一次批量上限低
			int num = MAX_SIZE / size;
			if (num < 2)
				num = 2;

			if (num > 512)
				num = 512;

			return num;
		}
		// 计算一次向系统获取几个页
		// 单个对象 8byte
		// ...
		// 单个对象 256KB
		static size_t NumMovePage(size_t size)
		{
			size_t num = NumMoveSize(size);
			size_t npage = num * size;

			npage >>= PAGE_SHIFT;
			if (npage == 0)
				npage = 1;

			return npage;
		}
};

//Span用来管理多个连续页的大块内存跨度调度结构
//CentralCache和PageCache都需要调用
struct Span
{
	PageID PageNum=0;		//页数
	size_t n_=0;			//多个大块内存的起始页的页号
	//双向链表
	Span* prev_=nullptr;
	Span* next_=nullptr;

	void* freeList_=nullptr;//切出来小内存的自由链表
	size_t IsUseCount_=0;	//被切出去的内存,分配给ThreadCache计数
	bool IsUse = false;		//是否被使用
	size_t ObjectSize=0;	//对象数量
};

//带头双向循环链表
class SpanList
{
private:
	std::unique_ptr<Span> head_;  // 头节点由 unique_ptr 独占管理
	//多线程访问同一个桶会形成竞争导致变慢,访问各自的桶即使是加锁也不会变慢

public:
	std::mutex mtx_;
	SpanList()
		: head_(std::make_unique<Span>())
	{
		head_->prev_ = head_.get();
		head_->next_ = head_.get();
	}
	Span* Begin()
	{
		return head_->next_;
	}
	Span* End()
	{
		return head_->prev_;
	}
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}
	Span* PopFront()
	{
		Span* target = head_->next_;
		Erase(target);
		return target;
	}
	bool Empty()
	{
		return head_->next_ == head_.get();
	}
	// 插入:将 newSpan 插入到 pos 之前(pos 不能为 nullptr)
	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);
		// 连接前后节点
		Span* prev = pos->prev_;
		prev->next_ = newSpan;
		newSpan->prev_ = prev;
		newSpan->next_ = pos;
		pos->prev_ = newSpan;
		// newSpan 的所有权已转移给链表(由链表负责析构时释放)
	}

	// 删除:从链表中移除 pos 指向的节点(不能是头节点)
	void Erase(Span* pos)
	{
		assert(pos);
		// 确保不删除头节点
		if (pos == head_.get())
		{
			return;  // 头节点由 unique_ptr 管理,不能删除
		}

		// 从链表中摘除
		pos->prev_->next_ = pos->next_;
		pos->next_->prev_ = pos->prev_;

		// 重置指针,避免悬空指针
		pos->prev_ = nullptr;
		pos->next_ = nullptr;
	}

	// 析构函数:释放所有非头节点
	~SpanList()
	{
		Span* cur = head_->next_;
		while (cur != head_.get())
		{
			Span* next = cur->next_;
			delete cur;      // 释放每个普通节点
			cur = next;
		}
		// head_ 由 unique_ptr 自动释放,无需手动处理
	}
};

ThreadCache

设计

我们之前设计的定长内存池应对单线程没有问题。但是对于多线程需要不同内存的资源来说,这样显然是不够的。我们也可以这样设计

针对于8kb以下的可以申请8kb内存,针对16kb以下8kb以上的申请16kb内存

但是这样会导致内存资源浪费。申请的一大块内存因为内存对齐的需求,多余的没有利用内存,导致内存碎片问题,这就是内碎片。而外碎片是这段空间被切成碎片,部分还回来了但是并不连续,导致了内存碎片问题。

为了解决这个问题,我们就设计了ThreadCache

ThreadCache 是一个哈希桶,需要申请的内存通过计算来映射其所在的是哪个桶,每个桶内都拥有一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的

Common.h

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <cstdlib>  
#include<stdexcept>
#include<cassert>
using std::cout;
using std::endl;
using std::vector;
//内存池可申请的最大的内存------256KB
static const int MAX_SIZE=256 * 1024;
static const int NFRESSLISTS = 208;

//给一个对象取前4/8字节
void*& NextObject(void* object)
{
	return *(void**)object;
}
//管理好切分的小对象的链表
class FreeList
{
	private:
		void* freelist_;
	public:
		void Push(void* object)
		{
			//头插
			if (object == nullptr)
			{
				throw "申请的对象内存为空";
			}
			NextObject(object) = freelist_;
			freelist_ = object;
		}
		void* Pop()
		{	
			//头删
			if (freelist_ == nullptr)
			{
				throw "内存链表为空";
			}
			void* object = freelist_;
			freelist_ = NextObject(object);

		}
		//判断是否为空
		bool Empty()
		{
			return freelist_ == nullptr;
		}
};

ThreadCache.h

cpp 复制代码
#pragma once
#include"Common.h"
class ThreadCache
{
	public:
		//申请释放资源
		void *Allocator(size_t size)
		{

		}
		void Deallocator(size_t size ,void*ptr)
		{

		}
	private:

};

内存对齐规则

我们将以以下这种方式映射

1,128\] 8byte对齐 freelist\[0,16) \[128+1,1024\] 16byte对齐 freelist\[16,72) \[1024+1,8\*1024\] 128byte对齐 freelist\[72,128) \[8\*1024+1,64\*1024\] 1024byte对齐 freelist\[128,184) \[64\*1024+1,256\*1024\] 8\*1024byte对齐 freelist\[184,208)

因此我们最大的自由链表桶数为208

cpp 复制代码
//最大的自由链表桶数
static const int NFRESSLISTS = 208;

这样就可以改ThreadCache.h

cpp 复制代码
#pragma once
#include"Common.h"
class ThreadCache
{
	public:
		//申请释放资源
		void* Allocator(size_t size);
		void Deallocator(size_t size, void* ptr);
	private:
		FreeList freelist_[NFRESSLISTS];

};

尽管这个数字仍然不能避免内存碎片和浪费,但是已经做出了取舍。如果桶太少(比如只分几十个),则每个桶覆盖的范围过大,内部碎片会增加;如果桶太多(比如上千个),则管理数组本身的内存开销增大,且查找桶时可能需更多时间

类似的,在谷歌的项目------tcmalloc中桶的数量大约也是200多个,这是一个经历了工程考研的经验数值。

内存映射以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上

我们可能写出初版:

cpp 复制代码
//对齐映射规则
//以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上
class Alignment
{
	private:
		size_t _RoundUP(size_t size, size_t  Align)
		{
			//方式一:普通运算
			size_t alignsize = size;
			if (size %8!=0)
			{
				return (size / Align + 1) * Align;
			}
			else
			{
				return alignsize;
			}
			return alignsize;
		}
	public:
		//频繁调用的小函数常见写法
		static constexpr inline size_t RoundUP(size_t size)
		{
			if (size <= 128)//128B
			{
				return _RoundUP(size,8);
			}
			else if (size<1024)//1KB
			{
				return _RoundUP(size, 16);
			}
			else if (size<=8*1024)//8KB
			{
				return _RoundUP(size, 128);
			}
			else if (size<=64*1024)//64KB
			{
				return _RoundUP(size, 1024);
			}
			else if (size<=256*1024)//256KB
			{
				return _RoundUP(size, 8*1024);
			}
			else
			{
				throw std::runtime_error("申请内存过大");
				return 0;
			}
		}

但是这种取模、除法运算对于CPU来说比较慢,还有更快的方式:

cpp 复制代码
//方式二:位运算
//static防止多次定义 constexpr编译期计算 inline建议编译器编译期内联
static constexpr inline size_t _RoundUP(size_t size, size_t  Align)
{
	return (size + Align - 1) & ~(Align - 1);
}

以上这个写法第一眼可能比较懵,没关系我们来以一个例子解释:

当size为5时,则二进制为00000101;Align为8,则Align - 1为00000111

size + Align - 1为00001100,即12

~为按位取反 , ~(Align - 1)为11111000

&为按位与,当对应位数均为1时方为1

因此,(size + Align - 1) & ~(Align - 1)=>

00001100&11111000

=>00001100

11111000

=>00001000

结果为8

接下来我们还要计算申请到的内存被挂载到哪个桶上,代码如下

cpp 复制代码
//计算映射到哪个桶内的自由链表
//这里除法计算的都是2的幂数,现代主流编译器(MSVC、GCC、Clang会将其转化为位运算)
static constexpr inline size_t Index(size_t size) 
{
	size_t aligned = RoundUP(size);  // 先对齐,可能抛出异常

	if (aligned <= 128) // 8字节对齐区间 [8, 128]
	{  
		// 桶索引:8→0, 16→1, ..., 128→15
		return aligned / 8 - 1;
	}
	else if (aligned <= 1024) // 16字节对齐区间 [144, 1024]
	{  
		// 起始144对应桶16,步长16
		return (aligned - 144) / 16 + 16;
	}
	else if (aligned <= 8 * 1024)  // 128字节对齐区间 [1152, 8192]
	{ 
		// 起始1152对应桶72,步长128
		return (aligned - 1152) / 128 + 72;
	}
	else if (aligned <= 64 * 1024) // 1024字节对齐区间 [9216, 65536]
	{  
		// 起始9216对应桶128,步长1024
		return (aligned - 9216) / 1024 + 128;
	}
	else if (aligned <= 256 * 1024)  // 8*1024字节对齐区间 [73728, 262144]
	{ 
		// 起始73728对应桶184,步长8192
		return (aligned - 73728) / (8 * 1024) + 184;
	}
	else // 理论上不会到达这里,因为 RoundUP 已经对过大 size 抛异常
	{
		throw std::runtime_error("申请内存过大");
	}
}

这里不需要进行位运算,因为现代编译器对2的幂的除法编译的过程中会自行转化为位运算,实际效果并不会比显式写位运算差

接着我们就可以写ThreadCache内存池申请资源了

cpp 复制代码
void*ThreadCache::Allocator(size_t size)
{
	assert(size <= MAX_SIZE);
	//对申请的内存对齐
	size_t AlignSize = Alignment::RoundUP(size);
	//计算映射的桶
	size_t index = Alignment::Index(size);
	if (!freelist_[index].Empty())//链表内存不为空
		return freelist_[index].Pop();
	else//链表内存为空,则自行申请内存
		FetchFromCentralCache(index, AlignSize);
	
}

TLS无锁线程访问

一个进程有多个线程,如果每个线程都需要访问,并发条件下,用锁会导致串行,这样在高并发下很有可能效率更低,还会有CPU上下文转换的性能开销。这样我们就不想要锁,那么没有锁的化,我们该怎么办呢?

我们可以利用TLS技术------Thread Local Storage,即线程本地存储。

Thread Local Storage(TLS,线程本地存储)是一种线程隔离的存储机制 ,它允许为每个线程创建并维护变量的独立副本,确保线程之间的数据互不干扰。核心机制:TLS的本质是为每个线程分配一块独立的内存空间,用于存储该线程专属的变量实例。当线程访问这些变量时,会自动访问自己的副本,而不会影响其他线程的同名变量。

在C++11后,拥有单独的关键字实现

cpp 复制代码
thread_local int thread_id; // 每个线程有独立的thread_id副本

而历史角度上,也有两种非跨平台的实现方式:

Windows :通过 TlsAllocTlsSetValueTlsGetValue 等 API 分配和使用 TLS 槽,编译器支持 __declspec(thread) 语法。

Linux/Unix :早期用 __thread 关键字(GCC扩展)

TLS的优缺点

优点

  1. 线程安全:变量副本独立,无需互斥锁,避免了线程竞争和死锁风险。

  2. 性能提升:无锁操作减少了同步开销,适合高频访问的场景。

  3. 简化代码:无需手动管理线程间的数据隔离,代码更简洁。

缺点

  1. 内存开销:每个线程都有变量副本,可能增加内存使用(特别是线程数量多时)。

  2. 初始化开销:线程首次访问TLS变量时需要初始化,可能影响首次访问性能。

  3. 生命周期管理:TLS变量的构造和析构时机与线程生命周期相关,需注意资源释放。

源代码

TLSManager.h

cpp 复制代码
#pragma once
#include "ThreadCache.h"
#include <memory>

/**
 * @brief TLS生命周期管理器,使用RAII模式自动管理ThreadCache实例
 * @details 每个线程拥有独立的ThreadCache实例,线程结束时自动清理资源
 */
class TLSManager 
{
public:
    /**
     * @brief 获取当前线程的ThreadCache实例
     * @return ThreadCache& 当前线程的ThreadCache引用
     * @note 线程首次访问时自动创建实例,线程结束时自动销毁
     */
    static ThreadCache& GetInstance() 
    {
        if (!instance_) 
        {
            instance_ = std::make_unique<ThreadCache>();
        }
        return *instance_;
    }

    /**
     * @brief 清理当前线程的ThreadCache资源
     * @note 通常不需要手动调用,线程结束时会自动调用
     */
    static void Cleanup()
    {
        instance_.reset();
    }

    /**
     * @brief 检查当前线程是否有ThreadCache实例
     * @return bool 如果有实例返回true,否则false
     */
    static bool HasInstance() 
{
        return instance_ != nullptr;
    }

private:
    // 每个线程独立的ThreadCache实例
    static thread_local std::unique_ptr<ThreadCache> instance_;
};

// 定义thread_local变量
thread_local std::unique_ptr<ThreadCache> TLSManager::instance_ = nullptr;

ConcurrentAlloc.h

cpp 复制代码
#pragma once
#include"Common.h"
#include"ThreadCache.h"
#include"TLSManager.h"

//并发申请释放内存
static void* ConcurrrentAlloc(size_t size)
{
	//通过TLSManager获取当前线程的ThreadCache实例
	//RAII模式自动管理生命周期,线程结束时自动清理
	return TLSManager::GetInstance().Allocator(size);
}

static void ConcurrrentDealloc(void* ptr, size_t size)
{
	//通过TLSManager获取当前线程的ThreadCache实例
	TLSManager::GetInstance().Deallocator(ptr, size);
}

ThreadCache.h

cpp 复制代码
#pragma once
#include "Common.h"

class ThreadCache
{
public:
    //申请释放资源
    void* Allocator(size_t size);
    void Deallocator(void* ptr, size_t size);
    //从中心缓存获取
    void* FetchFromCentralCache(size_t index, size_t size);
    // 释放对象时,链表过长时,回收内存回到中心缓存
    void ListTooLong(FreeList& list, size_t size);
private:
    FreeList freelist_[NFRESSLISTS];
};

// 声明为 extern thread_local,全局只有一个 TLS 变量
extern thread_local ThreadCache* pTLSThreadCache;

ThreadCache.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "ThreadCache.h"
#include"CentralCache.h"

// 跨平台min函数包装器,避免Windows.h的宏冲突
namespace PlatformUtils
{
    template<typename T>
    inline const T& Min(const T& a, const T& b)
    {
        return (a < b) ? a : b;
    }
}

// 定义 thread_local 变量(每个线程独立)
thread_local ThreadCache* pTLSThreadCache = nullptr;
//申请资源
void* ThreadCache::Allocator(size_t size)
{
    assert(size <= MAX_SIZE);
    size_t AlignSize = Alignment::RoundUP(size);
    size_t index = Alignment::Index(size);
    if (!freelist_[index].Empty())
        return freelist_[index].Pop();
    else
        return FetchFromCentralCache(index, AlignSize);
}
//释放资源
void ThreadCache::Deallocator(void* ptr, size_t size)
{
    assert(ptr);
    assert(size <= MAX_SIZE);
    size_t index = Alignment::Index(size);
    freelist_[index].Push(ptr);
    //链表长度大于一次批量申请的内存
    if (freelist_[index].size() >= freelist_[index].MaxSize())
    {
        ListTooLong(freelist_[index], size);
    }
}
//从CentralCache申请资源
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
    //满开始反馈调节算法
    // 1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完
    // 2、如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
    // 3、size越大,一次要的BatchNum越小
    // 4、size越小,一次要得BatchNum越大
    size_t BatchNum = PlatformUtils::Min(freelist_[index].MaxSize(), Alignment::NumMoveSize(size));
    void* start = nullptr;
    void* end = nullptr;
    size_t actualNum = CentralCache::Instance()->FetchRangeObj(start, end, BatchNum, size);
    assert(actualNum > 0);
    if (actualNum == 1)
    {
        assert(start == end);
        return start;
    }
    else
    {
        freelist_[index].PushRange(NextObject(start), end, actualNum-1);
        return start;
    }

    if (BatchNum == freelist_[index].MaxSize())
    {
        freelist_[index].MaxSize() += 1;
    }
    return nullptr;
}

void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
    void* start = nullptr;
    void* end = nullptr;
    list.PopRange(start, end, list.MaxSize());

    CentralCache::Instance()->ReleaseListToSpans(start, size);
}

CentralCache

设计

当ThreadCache没有内存的时候,就回去CentralCache申请内存。CentralCache也是一个哈希桶结构,不同的是每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。

源代码

CentralCache.h

cpp 复制代码
#pragma once
#include"Common.h"
//单例模式
class CentralCache
{
	private:
		// 将指针数组改为对象数组,避免未初始化指针和成员访问错误
		SpanList spanlist_[NFRESSLISTS];
		static CentralCache sInst_;//只声明不定义

		CentralCache() = default;//不希望别人创建对象
		CentralCache(const CentralCache&) = delete;//禁止拷贝构造
	public:
		static CentralCache* Instance()
		{
			return &sInst_;
		}
		// 从中心缓存获取一定数量的对象给thread cache
		size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
		Span* GetOneSpan(SpanList& list, size_t size);

		// 将一定数量的对象释放到span跨度
		void ReleaseListToSpans(void* start, size_t byte_size);
		
};

CentralCache.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"CentralCache.h"
#include"PageCache.h"
CentralCache CentralCache::sInst_;

// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
    //查看当前是否有没有被分配的span
    Span* it = list.Begin();
    while (it != list.End())
    {
        if (it->freeList_ != nullptr)
        {
            return it;
        }
        else
        {
            it = it->next_;
        }
    }
    //先解除CentralCache的桶锁,这样其他线程释放资源的时候就不会阻塞
    list.mtx_.unlock();
    //走到这里证明没有没有被分配的span,只能去PageCache取申请
    PageCache::Instance()->pagemtx_.lock();
    Span*span=PageCache::Instance()->GetSpan(Alignment::NumMovePage(size));
    span->IsUse = true;
    PageCache::Instance()->pagemtx_.unlock();
    //不需要加锁,因为这样可能会导致切分后其他线程无法访问该span
    
    //计算span的起始地址
    char* start = (char*)(span->PageNum << PAGE_SHIFT);

    size_t bytesize = span->n_<< PAGE_SHIFT;//得知页号
    char* end = start + bytesize;
    //span切为小块挂在自由链表上挂起
    span->freeList_ = start;
    start += size;
    void* tail = span->freeList_;
    while (start < end)
    {
        NextObject(tail)=start;
        tail = NextObject(tail);
        start += size;
    }
    //切好后挂到桶内再加锁
    list.mtx_.lock();
    list.PushFront(span);

    return span;
    
}

// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
    size_t index = Alignment::Index(size);
    spanlist_[index].mtx_.lock();

    Span* span = GetOneSpan(spanlist_[index],size);
    assert(span);
    assert(span->freeList_);
    start = span->freeList_;
    end = start;
    size_t i = 0;
    size_t actualNum = 1;
    while (i< batchNum -1&&NextObject(end)!=nullptr)
    {
        end = NextObject(end);
        ++i;
    }
    span->freeList_ = NextObject(end);
    NextObject(end) = nullptr;
    spanlist_[index].mtx_.unlock();
    return actualNum;
}

void CentralCache::ReleaseListToSpans(void* start, size_t byte_size)
{
    size_t index = Alignment::Index(byte_size);
    spanlist_[index].mtx_.lock();
    while (start)
    {
        void* next = NextObject(start);
        Span* span = PageCache::Instance()->MapObjectToSpan(start);
        NextObject(start)=span->freeList_;
        span->freeList_ = start;
        span->IsUseCount_--;
        // 说明span的切分出去的所有小块内存都回来了
        // 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
        if (span->IsUseCount_ == 0)
        {
            spanlist_[index].Erase(span);
            span->freeList_ = nullptr;
            span->next_ = nullptr;
            span->prev_ = nullptr;
            // 释放span给page cache时,使用page cache的锁就可以了
            // 这时把桶锁解开掉
            spanlist_[index].mtx_.unlock();

            PageCache::Instance()->pagemtx_.lock();
            PageCache::Instance()->ReleaseSpanToPageCache(span);
            PageCache::Instance()->pagemtx_.unlock();

            spanlist_[index].mtx_.lock();
        }
        start = next;
    }
    spanlist_[index].mtx_.unlock();
}

PageCache

设计

申请内存:

  1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page span分裂为一个4页page span和一个6页page span。

  2. 如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。

  3. 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
    释放内存:

  4. 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

不过这里我们就涉及到加锁的问题。

对于一个线程来说,是不需要锁的。因为一个线程申请自己的内存,互相之之间不存在竞争,因此ThreadCache是不需要加锁的。

对于CentralCache来说,不同的线程对应不同的桶的时候是没有竞争的,也就不需要上锁,只有当多个线程竞争同一个锁的时候才会竞争,因此只需要对桶加锁就够了。

对于PageCaChe来说,需要整个锁。但是不能单纯用互斥锁,因为会死锁。我们可以采用两种方式解决:

1、用recursive_mutex。递归进行锁的时候会进行检查防止死锁。

2、分离子函数和主函数。子函数实现业务逻辑,主函数加锁控制

但是我们可以这样在CentralCache上加锁(见上面的源码)

源代码

PageCache.h

cpp 复制代码
#pragma once
#include"Common.h"
#include<unordered_map>
class PageCache
{
private:
	SpanList spanlist_[NFRESSLISTS];
	std::unordered_map<PageID, Span*>IdSpanMap_;
	static PageCache sInstan_;
	PageCache() = default;
	PageCache(const PageCache&) = delete;
public:
	static PageCache* Instance()
	{
		return &sInstan_;
	}
	// 获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);
	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);
	std::mutex pagemtx_;
	//获取K页的span
	Span* GetSpan(size_t K);
};

PageCache.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"PageCache.h"
PageCache PageCache::sInstan_;
Span* PageCache::MapObjectToSpan(void* object)
{
	PageID id = ((PageID)object >> PAGE_SHIFT);
	auto ret = IdSpanMap_.find(id);
	if (ret != IdSpanMap_.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}
Span* PageCache::GetSpan(size_t K)
{
	assert(K > 0 && K < NPAGES);
	if (!spanlist_[K].Empty())
	{
		return spanlist_[K].PopFront();
	}
	// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
	for (size_t i = K + 1; i < NPAGES; ++i)
	{
		if (!spanlist_[i].Empty())
		{
			Span* nSpan = spanlist_[i].PopFront();
			Span* kSpan = new Span;

			// 在nSpan的头部切一个k页下来
			// k页span返回
			// nSpan再挂到对应映射的位置
			kSpan->PageNum = nSpan->PageNum;
			kSpan->n_ = K;

			nSpan->PageNum += K;
			nSpan->n_ -= K;

			spanlist_[nSpan->n_].PushFront(nSpan);
			//存储span页号与span的映射,方便回收时合并查找
			IdSpanMap_[nSpan->PageNum] = nSpan;

			// 1000 5
			IdSpanMap_[nSpan->PageNum + nSpan->n_ - 1] = nSpan;

			//建立ID与span映射,方便CentralCache回收时查找对应的span
			for (PageID i = 0; i < kSpan->n_; ++i)
			{
				IdSpanMap_[kSpan->PageNum + i] = kSpan;
			}
			return kSpan;
		}
	}
	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->PageNum = (PageID)ptr >> PAGE_SHIFT;
	bigSpan->n_ = NPAGES - 1;

	spanlist_[bigSpan->n_].PushFront(bigSpan);

	return GetSpan(K);
	
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 对span前后的页,尝试进行合并,缓解内存碎片问题
	//向前合并
	while (1)
	{
		PageID prevId = span->PageNum - 1;
		auto ret = IdSpanMap_.find(prevId);
		// 前面的页号没有,不合并了
		if (ret == IdSpanMap_.end())
		{
			break;
		}
		// 前面相邻页的span在使用,不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->IsUse == true)
		{
			break;
		}

		// 合并出超过128页的span没办法管理,不合并了
		if (prevSpan->n_ + span->n_ > NPAGES - 1)
		{
			break;
		}
		span->PageNum = prevSpan->PageNum;
		span->n_ += prevSpan->n_;
		spanlist_[prevSpan->n_].Erase(prevSpan);
		delete prevSpan;
	}
	// 向后合并
	while (1)
	{
		PageID nextId = span->PageNum + span->n_;
		auto ret = IdSpanMap_.find(nextId);
		if (ret == IdSpanMap_.end())
		{
			break;
		}
		Span* nextSpan = ret->second;
		if (nextSpan->IsUse == true)
		{
			break;
		}
		if (nextSpan->n_ + span->n_ > NPAGES - 1)
		{
			break;
		}
		span->n_ = nextSpan->n_;
		spanlist_[nextSpan->n_].Erase(nextSpan);

		delete nextSpan;
	}
	spanlist_[span->n_].PushFront(span);
	span->IsUse = false;
	IdSpanMap_[span->PageNum] = span;
	IdSpanMap_[span->PageNum + span->n_ - 1] = span;
}

本篇内容到这里就结束了。内存池的三大Cache已经完成,后续我们还会对其进行调优测试。敬请期待

封面图自取:

相关推荐
暮冬-  Gentle°2 小时前
设计模式在C++中的实现
开发语言·c++·算法
短剑重铸之日2 小时前
《ShardingSphere解读》12 解析引擎:SQL 解析流程应该包括哪些核心阶段?(下)
数据库·后端·sql·架构·shardingsphere·分库分表
2501_908329852 小时前
实时音频处理C++实现
开发语言·c++·算法
dapeng28702 小时前
移动语义与完美转发详解
开发语言·c++·算法
计算机学姐2 小时前
基于SpringBoot的网吧管理系统
java·spring boot·后端·spring·tomcat·intellij-idea·mybatis
摸鱼的春哥2 小时前
Agent教程21:知识图谱🕸,让AI🤖学会联想
前端·javascript·后端
!停2 小时前
C++基础入门(缺省参数,函数重载,引用)
开发语言·c++·算法
我不是秋秋2 小时前
软件开发项目各角色关系解析:产品/前后端/测试如何高效协作?
java·算法·面试·职场和发展·哈希算法