C++_项目高并发内存池结项——性能瓶颈分析与基数树进行性能突破,打包静态库

本文分析了内存管理中PageCache的性能瓶颈,提出使用基数树替代哈希映射以优化内存分配和释放。基数树通过预分配空间避免了加锁操作,提高了多线程环境下的性能。在32位系统中采用二层基数树,而在64位系统中使用三层基数树。通过确保所需页号的空间已分配,基数树能提供高效的映射访问,且在释放内存时保持线程安全。
摘要由CSDN通过智能技术生成

现在项目已经可以正常运行,但相比malloc来讲,在申请次数较少时性能比较差

每轮申请10000次内存
在这里插入图片描述

每轮申请100次

在这里插入图片描述

1. 性能分析

VS2022下性能分析
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其次是:

在这里插入图片描述
根据上图可知,消耗最大的是释放空间时合并Span到PageCache时锁消耗时间比较多,其次是通过页号找到对应的Span的哈希映射,加锁耗时比较大。

2. 使用基数树进行优化分析

为了解决PageCache访问页号与Span*的映射加锁耗费太多的时间问题,引出了基数树这种映射方法。

基数树的定义

直接定址法的基数树:

一层基数树映射保存页号和Span*

template <int BITS>
class TCMalloc_PageMap1 {
private:
	static const int LENGTH = 1 << BITS;
	void** array_;
}

BITS是保存了记录所有页所需要的比特位

假设一页8K,32位平台
2 ^ 32 / 2 ^ 13= 2 ^ 19
所以需要19位可以表示所有的页
BITS=19

在这里插入图片描述
如图数组大小仅为2MB所以对于32位可以直接将映射数组开辟出来。

如果是64位机器,不能之一这种方式,因为要开辟的数组太大。这个时候需要高层映射(基数树)

二层基数树

在这里插入图片描述
拿到一个页号,因为页号最大到19位,前面13位都是0可以不用管
在这里插入图片描述
eg:
给一个页号,先分离出后19为[A],通过[A]前5位来找在第一层位置,再通过后14位来找第二层数组的位置。

template <int BITS>
class TCMalloc_PageMap2 {
private:
	// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
	static const int ROOT_BITS = 5;
	static const int ROOT_LENGTH = 1 << ROOT_BITS;//第一层数组的大小
	static const int LEAF_BITS = BITS - ROOT_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;//第二层数组的大小
	// Leaf node
	struct Leaf {
			void* values[LEAF_LENGTH];
	};
	Leaf* root_[ROOT_LENGTH];//第一层数组的每个数组元素是一个数组
}

同理三层模型与二层基数树类型,这里不在赘述。

三层基数树

64位采用为了减少开辟数组的大小,选用三层的基数树。需要访问那个位置,再开辟数组,可以避免开始就开辟特别大的数组

// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
 // How many bits should we consume at each interior level
 static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
 static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;
 // How many bits should we consume at leaf level
 static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
 static const int LEAF_LENGTH = 1 << LEAF_BITS;
 // Interior node
 struct Node {
 Node* ptrs[INTERIOR_LENGTH];
 };
 // Leaf node
 struct Leaf {
 void* values[LEAF_LENGTH];
 };
 Node* root_;                          // Root of radix tree

3. C++使用二层基数树优化

这里不讨论64位(需要三层基数树),只看32位使用二层基数树。将PageCache中需要使用页号和Span*的哈希映射全部转化为基数树的映射即可。

一二三层基数树C++实现

#pragma once

#include"Common.h"
#include"ObjectPool.h"

// Single-level array
template <int BITS>
class TCMalloc_PageMap1 {
private:
	static const int LENGTH = 1 << BITS;
	void** array_;
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap1() {
		size_t size = sizeof(void*) << BITS;
		size_t AlignSize = SizeClass::_RoundUp(size, 1 << PAGESIZE);
		array_ = (void**)SystemAlloc(AlignSize >> PAGESIZE);//计算页数
		memset(array_, 0, sizeof(void*) << BITS);
	}
	// Return the current value for KEY. Returns NULL if not yet set,
	// or if k is out of range.
	void* get(Number k) const {
		if ((k >> BITS) > 0) {
			return NULL;
		}
		return array_[k];
	}
	// REQUIRES "k" is in range "[0,2^BITS-1]".
	// REQUIRES "k" has been ensured before.
	//
	// Sets the value 'v' for key 'k'.
	void set(Number k, void* v) {
		array_[k] = v;
	}
};
// Two-level radix tree
template <int BITS>
class TCMalloc_PageMap2 {
private:
	// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
	static const int ROOT_BITS = 5;
	static const int ROOT_LENGTH = 1 << ROOT_BITS;
	static const int LEAF_BITS = BITS - ROOT_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;
	// Leaf node
	struct Leaf {
			void* values[LEAF_LENGTH];
	};
	Leaf* root_[ROOT_LENGTH];
	void* (*allocator_)(size_t);
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap2() {
		
		memset(root_, 0, sizeof(root_));
	}
	void* get(Number k) const {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 || root_[i1] == NULL) {
			return NULL;
		}
		return root_[i1]->values[i2];
	}
	void set(Number k, void* v) {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		ASSERT(i1 < ROOT_LENGTH);
		root_[i1]->values[i2] = v;
	}
	bool Ensure(Number start, size_t n) { //确保n页对应的每层数组的空间大小已经开辟完毕
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> LEAF_BITS;
			// Check for overflow
			if (i1 >= ROOT_LENGTH)
				return false;
			// Make 2nd level node if necessary
			if (root_[i1] == NULL) {
				static ObjectPool<Leaf>LeafPool;//定长内存池
				Leaf* leaf = (Leaf*)LeafPool.New();
				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}
			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}
	void PreallocateMoreMemory() {
		// Allocate enough to keep track of all possible pages
		Ensure(0, 1 << BITS);
	}
};

//#################################################################################三层基数树,适用64位,不讨论

// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
	// How many bits should we consume at each interior level
	static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;
	// How many bits should we consume at leaf level
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;
	// Interior node
	struct Node {
		Node* ptrs[INTERIOR_LENGTH];
	};
	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};
	Node* root_;                           // Root of radix tree
	void* (*allocator_)(size_t);           // Memory allocator
	Node* NewNode() {
		Node* result = reinterpret_cast<Node*>((*allocator_)(sizeof(Node)));
		if (result != NULL) {
			memset(result, 0, sizeof(*result));
		}
		return result;
	}
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap3(void* (*allocator)(size_t)) {
		allocator_ = allocator;
		root_ = NewNode();
	}
	void* get(Number k) const {
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 ||
			root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL) {
			return NULL;
		}
		return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3];
	}
	void set(Number k, void* v) {
		ASSERT(k >> BITS == 0);
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
	}
	bool Ensure(Number start, size_t n) {  //确保n页对应的每层数组的空间大小已经开辟完毕
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS);
			const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
			// Check for overflow
			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
				return false;
			// Make 2nd level node if necessary
			if (root_->ptrs[i1] == NULL) {
				Node* n = NewNode();
				if (n == NULL) return false;
				root_->ptrs[i1] = n;
			}
			// Make leaf node if necessary
			if (root_->ptrs[i1]->ptrs[i2] == NULL) {
				Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_) (sizeof(Leaf)));
				if (leaf == NULL) return false;
				memset(leaf, 0, sizeof(*leaf));
				root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
			}
			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}
	void PreallocateMoreMemory() {
		//
	}
};

之前使用哈希表需要加锁的原因:

  • 线程在释放内存时,PageCache需要合并Span,修改映射。其他线程还可能插入映射或删除映射。这样会导致哈希结构底层的红黑树发生旋转等操作。所以为了保证线程安全,必须要加锁。

使用基数树映射不需要加锁的原因:

  • 基数树在需要建立映射的时候会提前根据你的页号创建出空间(无论是一层基数树还是三层基数树Ensure函数),开好后的空间无论是删除映射还是添加映射,这个空间不会像树结构那样发生旋转。结构不会改变
  • 修改基数树映射的时刻是,从系统申请的大空间切分挂到PageCache桶上时要添加映射。释放空间,小空间合并为大空间时需要添加映射
    在这里插入图片描述

在这里插入图片描述
而这两个位置在上层的时候就已经加锁了(同时一个是申请空间,一个是释放空间,根本不可能发生对同一块内存操作的问题),其他的操作都是访问映射,并不涉及修改操作,所以是线程安全的。在获取页号映射的函数中不需要加锁。效率提高。

注意:这里要使用一层基数树只能在32位下运行,64位需要三层基数树。
VS2022默认64位,如果你是VS2022下测试注意改为32位:

在这里插入图片描述

代码优化因为篇幅原因,放到链接下

Github
Gitee

优化后测试

在这里插入图片描述
Release
在这里插入图片描述

4.打包静态库

在这里插入图片描述

原tcmalloc项目链接

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUC_Dodamce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值