现在项目已经可以正常运行,但相比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位:

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

Release

4.打包静态库

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

被折叠的 条评论
为什么被折叠?



