C++高并发内存池:内存池调优与测试

前面我们已经完成了三种Cache的设计。本期我们就来调整一下内存池相关的设计问题

相关代码在我的个人gitee:高并发内存池: 个人学习的项目------高并发内存池

目录

对于大于256KB的内存申请释放

释放对象优化

配备内存池申请变量

多线程下与malloc的性能测试对比


对于大于256KB的内存申请释放

前面我们已经讲解对于小于256KB的内存池而言,它会通过我们前面设计的三重Cache申请资源。但是实际中并不总是申请小于256KB的内存。那么对于大于256KB的内存我们怎么处理呢?

前面我们以8KB为一页,那么我们就来分类讨论一下,设申请size

32页<size<=256页的时候,可以 去找PageCache申请

size>256页的时候,就要去系统堆申请

Common.h

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)
		{
			(void)end; // 标记未使用参数,消除警告
			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 (size_t 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:
		//方式二:位运算
		//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
		}
	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
			{
				return _RoundUP(size, 1 << PAGE_SHIFT);
			}
		}
		//计算映射到哪个桶内的自由链表
		//这里除法计算的都是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("申请内存过大");
			}
		}
		// 一次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;	//对象数量
	bool isLargeMemory = false; // 标记是否为大内存块(>256KB)
	void* systemMemoryPtr = nullptr; // 系统分配的内存指针(仅用于大内存块)
};

//带头双向循环链表
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 自动释放,无需手动处理
	}
};

MemoryAlloc.h

cpp 复制代码
// File: MemoryAllocator.h
#pragma once
#include <cstddef>
#include <stdexcept>
#include <new>

#ifdef _WIN32
    #include <windows.h>
#else
    #include <sys/mman.h>
    #include <unistd.h>
#endif

// 默认页大小 4KB
static const size_t PAGE_SIZE = 4096;  // 1<<12

// 按页申请内存(kpage:页数)
inline static void* SystemAlloc(size_t kpage)
{
    size_t size = kpage * PAGE_SIZE;
#ifdef _WIN32
    void* ptr = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
        ptr = nullptr;
#endif
    if (!ptr)
        throw std::bad_alloc();
    return ptr;
}

inline static void SystemFree(void* ptr)
{
    if (!ptr) return;
#ifdef _WIN32
    if (!VirtualFree(ptr, 0, MEM_RELEASE)) {
        throw std::runtime_error("VirtualFree failed");
    }
#else
    // 在Linux下,我们需要知道内存大小才能正确释放
    // 这里使用一个简化的实现:假设ptr是通过mmap分配的
    // 更健壮的实现需要记录分配的大小信息
    if (munmap(ptr, 0) == -1) {
        throw std::runtime_error("munmap failed");
    }
#endif
}

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);
}

释放对象优化

Common.h

cpp 复制代码
//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;	//对象数量
	bool isLargeMemory = false; // 标记是否为大内存块(>256KB)
	void* systemMemoryPtr = nullptr; // 系统分配的内存指针(仅用于大内存块)
};

PageCache.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"PageCache.h"
#include <iostream>
PageCache PageCache::sInstan_;
Span* PageCache::MapObjectToSpan(void* object)
{
	PageID id = ((PageID)object >> PAGE_SHIFT);
	std::unique_lock<std::mutex> lock(pagemtx_);
	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);
	//大于128页的直接向堆申请内存,不进行切分
	if (K > NPAGES-1)
	{
		void* ptr = SystemAlloc(K);
		Span* span = new Span;
		span->PageNum = (PageID)ptr >> PAGE_SHIFT;
		span->n_ = K;
		IdSpanMap_[span->PageNum] = span;
		IdSpanMap_[span->PageNum + K - 1] = span;
		return span;
	}
	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)
{
	// 大于128 page的直接还给堆
	if (span->n_ > NPAGES-1)
	{
		void* ptr = (void*)(span->PageNum << PAGE_SHIFT);
		SystemFree(ptr);
		delete span;

		return;
	}
	// 如果是大内存块,直接返回,不进行合并操作
	if (span->isLargeMemory) {
		return;
	}
	
	// 对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;
}

// 注册大内存块
Span* PageCache::RegisterLargeSpan(void* ptr, size_t kpage)
{
	if (!ptr) {
		throw std::invalid_argument("Invalid pointer for large memory registration");
	}
	
	// 检查是否已经注册
	if (largeMemoryMap_.find(ptr) != largeMemoryMap_.end()) {
		throw std::runtime_error("Large memory block already registered");
	}
	
	// 创建新的Span来管理这个大内存块
	Span* span = new Span();
	span->PageNum = reinterpret_cast<PageID>(ptr) >> PAGE_SHIFT;
	span->n_ = kpage;
	span->isLargeMemory = true;
	span->systemMemoryPtr = ptr;
	span->IsUse = true;
	
	// 注册到映射表
	largeMemoryMap_[ptr] = span;
	
	// 注册到页映射表(用于MapObjectToSpan查找)
	for (PageID i = 0; i < span->n_; ++i) {
		IdSpanMap_[span->PageNum + i] = span;
	}
	
	return span;
}

// 注销大内存块
void PageCache::UnregisterLargeSpan(Span* span)
{
	if (!span || !span->isLargeMemory) {
		throw std::invalid_argument("Invalid span for large memory unregistration");
	}
	
	// 从大内存映射表中移除
	auto it = largeMemoryMap_.find(span->systemMemoryPtr);
	if (it != largeMemoryMap_.end()) {
		largeMemoryMap_.erase(it);
	}
	
	// 从页映射表中移除
	for (PageID i = 0; i < span->n_; ++i) {
		IdSpanMap_.erase(span->PageNum + i);
	}
	
	// 删除Span对象
	delete span;
}

// 检查是否为大内存块
bool PageCache::IsLargeMemory(void* ptr) const
{
	return largeMemoryMap_.find(ptr) != largeMemoryMap_.end();
}

ConcurrentAlloc.h

cpp 复制代码
#pragma once
#include"Common.h"
#include"ThreadCache.h"
#include"TLSManager.h"
#include"PageCache.h"
// 并发申请内存
static void* ConcurrrentAlloc(size_t size)
{
	if (size == 0) return nullptr;
	
	if (size > MAX_SIZE)
	{
		// 大内存分配:直接使用系统分配器
		try 
		{
			size_t alignsize = Alignment::RoundUP(size);
			size_t kpage = (alignsize + PAGE_SIZE - 1) >> PAGE_SHIFT;
			
			// 使用系统分配器分配大内存
			void* ptr = SystemAlloc(kpage);
			if (!ptr) 
			{
				throw std::bad_alloc();
			}
			
			// 记录大内存块信息到PageCache
			PageCache::Instance()->pagemtx_.lock();
			PageCache::Instance()->RegisterLargeSpan(ptr, kpage);
			PageCache::Instance()->pagemtx_.unlock();
			
			return ptr;
		}
		catch (const std::exception& e) 
		{
			std::cerr << "Large memory allocation failed: " << e.what() << std::endl;
			throw;
		}
	}
	else
	{
		// 小内存分配:通过TLSManager获取当前线程的ThreadCache实例
		// RAII模式自动管理生命周期,线程结束时自动清理
		try 
		{
			return TLSManager::GetInstance().Allocator(size);
		}
		catch (const std::exception& e)
		{
			std::cerr << "Small memory allocation failed: " << e.what() << std::endl;
			throw;
		}
	}
}

// 并发释放内存
static void ConcurrrentDealloc(void* ptr)
{
	if (!ptr) return;
	Span*span=PageCache::Instance()->MapObjectToSpan(ptr);
	size_t size=span->ObjectSize;
	Span* span = PageCache::Instance()->MapObjectToSpan(ptr);
	if (size > MAX_SIZE)
	{
		// 大内存释放
		try {
			PageCache::Instance()->pagemtx_.lock();
			
			if (!span) 
			{
				PageCache::Instance()->pagemtx_.unlock();
				throw std::runtime_error("Invalid pointer for large memory deallocation");
			}
			PageCache::Instance()->ReleaseSpanToPageCache(span);
			// 从PageCache中注销大内存块
			PageCache::Instance()->UnregisterLargeSpan(span);
			PageCache::Instance()->pagemtx_.unlock();
			
			// 使用系统分配器释放内存
			SystemFree(ptr);
		}
		catch (const std::exception& e) 
		{
			std::cerr << "Large memory deallocation failed: " << e.what() << std::endl;
			throw;
		}
	}
	else
	{
		// 小内存释放:通过TLSManager获取当前线程的ThreadCache实例
		try 
		{
			TLSManager::GetInstance().Deallocator(ptr, size);
		}
		catch (const std::exception& e) 
		{
			std::cerr << "Small memory deallocation failed: " << e.what() << std::endl;
			throw;
		}
	}
}

配备内存池申请变量

这里我们就要与之前的内存池相连了,首先在PageCache.h中添加声明:

cpp 复制代码
#pragma once
#include"Common.h"
#include "ConcurrentMemoryPool.h"
#include<unordered_map>
class PageCache
{
private:
	SpanList spanlist_[NFRESSLISTS];
	std::unordered_map<PageID, Span*>IdSpanMap_;
	static PageCache sInstan_;
	ConcurrentMemoryPool<Span> spanPool_;//添加声明
	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_;

// 获取一个K页的span
Span* PageCache::GetSpan(size_t k)
{
	assert(k > 0);

	// 大于128 page的直接向堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		//Span* span = new Span;
		Span* span = spanPool_.New();

		span->PageNum = (PageID)ptr >> PAGE_SHIFT;
		span->n_ = k;

		IdSpanMap_[span->PageNum] = span;
		return span;
	}

	// 先检查第k个桶里面有没有span
	if (!spanlist_[k].Empty())
	{
		Span* kSpan = spanlist_[k].PopFront();

		// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
		for (size_t i = 0; i < kSpan->n_; ++i)
		{
			IdSpanMap_[kSpan->PageNum + i] = kSpan;
		}

		return kSpan;
	}

	// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
	for (size_t i = k + 1; i < NPAGES; ++i)
	{
		if (!spanlist_[i].Empty())
		{
			Span* nSpan = spanlist_[i].PopFront();
			//Span* kSpan = new Span;
			Span* kSpan = spanPool_.New();

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

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

			spanlist_[nSpan->n_].PushFront(nSpan);
			// 存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时
			// 进行的合并查找
			IdSpanMap_[nSpan->PageNum] = nSpan;
			IdSpanMap_[nSpan->PageNum + nSpan->n_ - 1] = nSpan;


			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PageID i = 0; i < kSpan->n_; ++i)
			{
				IdSpanMap_[kSpan->PageNum + i] = kSpan;
			}

			return kSpan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	//Span* bigSpan = new Span;
	Span* bigSpan = spanPool_.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->PageNum = (PageID)ptr >> PAGE_SHIFT;
	bigSpan->n_ = NPAGES - 1;

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

	return GetSpan(k);
}

Span* PageCache::MapObjectToSpan(void* obj)
{
	PageID id = ((PageID)obj >> PAGE_SHIFT);

	std::unique_lock<std::mutex> lock(pagemtx_);

	auto ret = IdSpanMap_.find(id);
	if (ret != IdSpanMap_.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 大于128 page的直接还给堆
	if (span->n_ > NPAGES - 1)
	{
		void* ptr = (void*)(span->PageNum << PAGE_SHIFT);
		SystemFree(ptr);
		//delete span;
		spanPool_.Delete(span);

		return;
	}

	// 对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;
		spanPool_.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;
		spanPool_.Delete(nextSpan);
	}

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

多线程下与malloc的性能测试对比

多线程下与malloc的性能对比

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
//多线程下与malloc的性能测试
#include"ConcurrentAlloc.h"
#include<thread>
#include<vector>
#include<atomic>
#include<ctime>
#include<cstdio>
// ntimes 一轮申请和释放内存的次数
// rounds 轮次
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;

	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&, k]()
			{
				std::vector<void*> v;
				v.reserve(ntimes);

				for (size_t j = 0; j < rounds; ++j)
				{
					size_t begin1 = clock();
					for (size_t i = 0; i < ntimes; i++)
					{
						v.push_back(malloc(16));
						//v.push_back(malloc((16 + i) % 8192 + 1));
					}
					size_t end1 = clock();

					size_t begin2 = clock();
					for (size_t i = 0; i < ntimes; i++)
					{
						free(v[i]);
					}
					size_t end2 = clock();
					v.clear();

					malloc_costtime += (end1 - begin1);
					free_costtime += (end2 - begin2);
				}
			});
	}

	for (auto& t : vthread)
	{
		t.join();
	}

	printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime.load());

	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, free_costtime.load());

	printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
		nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}


// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;

	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&]() {
			std::vector<void*> v;
			v.reserve(ntimes);

			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(ConcurrentAlloc(16));
					//v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
				}
				size_t end1 = clock();

				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{ 
					ConcurrentFree(v[i]);
				}
				size_t end2 = clock();
				v.clear();

				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
			});
	}

	for (auto& t : vthread)
	{
		t.join();
	}

	printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime.load());

	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, free_costtime.load());

	printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
		nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}

int main()
{
	size_t n = 1000;
	cout << "==========================================================" << endl;
	BenchmarkConcurrentMalloc(n, 10, 10);
	cout << endl << endl;

	BenchmarkMalloc(n, 10, 10);
	cout << "==========================================================" << endl;

	return 0;
}

运行结果如下,显然我们的性能问题异常严重,但是这个性能分析和优化我们下期更新

本期内容就到这里了。后续我们还会针对内存池进行进一步的性能优化

封面图自取:

相关推荐
短剑重铸之日2 小时前
《ShardingSphere解读》16 改写引擎:如何理解装饰器模式下的 SQL 改写实现机制?
java·数据库·后端·sql·shardingsphere·分库分表·装饰器模式
q5431470872 小时前
VScode 开发 Springboot 程序
java·spring boot·后端
小白自救计划2 小时前
力扣知识点杂集
算法·leetcode·哈希算法
雾隐潇湘2 小时前
C++——第三篇 继承与多态
开发语言·c++
阿贵---2 小时前
分布式系统C++实现
开发语言·c++·算法
不染尘.2 小时前
最短路径之Bellman-Ford算法
开发语言·数据结构·c++·算法·图论
Reisentyan2 小时前
[Refactor]CPP Learn Data Day 1
c++·重构
xiaoye-duck2 小时前
C++ STL set 系列深度解析:从底层原理、核心接口到实战场景
开发语言·c++·stl
m0_662577972 小时前
C++中的RAII技术深入
开发语言·c++·算法