C++项目 -- 高并发内存池(二)Thread Cache

C++项目 – 高并发内存池(二)Thread Cache


一、高并发内存池整体框架设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题。

  1. 性能问题。
  2. 多线程环境下,锁竞争问题
  3. 内存碎片问题。

concurrent memory pool主要由以下3个部分构成:
在这里插入图片描述

  1. thread cache线程缓存每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
  2. central cache中心缓存所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈
  3. page cache页缓存在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

二、thread cache设计

1.整体设计

thread cache是哈希桶结构每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。

  • 类比定长内存池,一大块内存用来分配空间,freeList用于管理已经分配好的定长内存块
  • thread cache的freeList可以设计多个大小的定长list,如下图,8字节、16字节…;对象小于等于8字节的由8字节的freelist管理,9到16字节的由16字节的freeList管理;
    在这里插入图片描述
  • 但这样设计会造成空间浪费,比如一个对象大小为6字节,为它分配了8字节的空间,那么这剩下的2字节就会成为碎片,这叫做内碎片
  • thread cache整体是一个哈希桶结构,将对象的大小映射到对应大小的freeList中进行管理;

申请内存:

  • 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
  • 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。
  • 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。

释放内存:

  • 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
  • 当链表的长度过长,则回收一部分内存对象到central cache。

2.thread cache哈希桶映射规则

  • thread cache最大支持一个对象申请256KB的内存空间,则对象的大小范围是1 ~ 256KB,如果以8字节为对齐数,指定freeList,就是1 ~ 8字节大小的对象按照8字节对齐,分配8字节空间,连接到第一个freeList后面;9 ~ 16字节大小的对象分配16字节空间,连接到第二个freeList后面,这就是内存对齐
  • 每一个对齐的freeList都是一个哈希桶,这就是哈希映射
    在这里插入图片描述
  • 如果将所有的哈希桶对齐规则都为8字节,则一共需要32768个哈希桶,数量太多;

因此我们需要制定一个对齐规则:
在这里插入图片描述
分段安排对齐数的大小;

  • 对象大小在1 ~ 128字节之间的,按照8字节对齐,即每个哈希桶大小增长8字节;
  • 那么8字节对齐的哈希桶就有16个,即对应freeLists[0] - freeList[15];
    第一个桶链接对象大小在1 ~ 8字节之间的,第二个链接9 ~ 16字节之间,依次类推;
  • 对象大小在129 - 1024之间的,按照16字节对齐,即每个哈希桶大小增长16字节,以此类推;
  • 最终分配下来,8字节对齐的哈希桶共16个,16字节对齐的哈希桶共56个,128字节对齐的哈希桶共56个,1024字节对齐的哈希桶共56个,8KB字节对齐的哈希桶共56个,所有的哈希桶加起来一共208个,也就是说一共有208个freeList;
  • 根据上面的对齐规则可以将对象的size按照对齐数进行对齐,对齐的逻辑:
    在这里插入图片描述
    如果size不是对齐数alignNum的倍数,就需要根据对齐数调整最终分配空间的大小,否则size就是最终分配空间的大小;
  • 上面的计算过程可以使用下面的位运算进行代替,因为这段代码会被频繁调用,位运算的效率更高:
    在这里插入图片描述

这样分配的好处:

  • 能够减少哈希桶的数量
  • 能够将内碎片浪费控制在10%左右
    • 以16字节对齐为例,16字节对齐数能浪费的空间最大为15字节;如果一个对象分配到了129字节的内存,其对应的对齐数是16,则最终系统会为该对象分配145字节的空间,那么就有15字节的空间浪费,则内碎片为15字节,浪费率 = 15 / 145 = 0.1034 ,后面128字节等的对齐规则类似
    • 前面8字节对齐的部分可能不止10%,但是从16字节开始就能够控制在10%左右;

在制定好字节对齐规则后,还需要制订哈希映射规则,将不同大小的对象映射到对应的freeList中:

  • 根据字节对齐规则,1 ~ 128字节大小的对象,映射在8字节对齐的哈希桶,其映射逻辑如下:
    在这里插入图片描述
    129 ~ 1024字节大小的对象也是这样的规则;
  • 上面的代码可以使用位运算代替,因为这段代码会被频繁调用,位运算的效率更高:
    在这里插入图片描述

3.TLS无锁访问

在多线程环境下,ThreadCache的创建和访问会涉及到锁的问题,我们希望每个线程都有独立的ThreadCache,并且访问自己的ThreadCache都无须加锁,这样就需要使用TLS;
线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。

Linux下TLS
win下TLS

TLS —— thread local storage:线程本地存储,我们这里使用静态的TLS:
声明以下代码:

_declspec(thread) DWORD data=0;

声明了_declspec(thread)的变量,会为每一个线程创建一个单独的拷贝。

  • 静态TLS的原理
    在×86CPU上,将为每次引用的静态TLS变量生成3个辅助机器指令。如果在进程中创建了另一个线程,那么系统就要将它捕获并且自动分配另一个内存块,以便存放新线程的静态TLS变量。新线程只拥有对它自己的静态TLS变量的访问权,不能访问属于其他线程的TLS变量。

ThreadCache.h
在这里插入图片描述

  • 在该头文件下定义一个全局的ThreadCache*类型的静态指针pTLSThreadCache使用_declspec(thread)声明后,每一个线程都会为该指针创建一个单独的拷贝,本线程在访问pTLSThreadCache指针时是能够全局访问的,但是其他线程不能访问本线程的pTLSThreadCache指针,这样就实现了多线程环境下的无锁访问;
  • 其实就是每一个线程都有一个独立的pTLSThreadCache,线程之间访问互不干扰

ConcurrentAlloc.h
在这里插入图片描述

  • 上面的代码只是声明了TLS的指针,并没有指向实际的ThreadCache对象,实际上每个线程在运行的时候,都需要将TLS指针指向自己的ThreadCache对象;
  • 这个头文件是对多线程环境下ThreadCache申请和释放内存的功能进行了封装,保证每个线程的ThreadCache都是本线程独有的,
  • ConcurrentAlloc函数会检测pTLSThreadCache是否为空,如果为空,证明初次调用,就需要构建一个新的ThreadCache对象,并将pTLSThreadCache指针指向该对象,这样本线程独有的ThreadCache对象就创建好了,再通过pTLSThreadCache指针去调用Allocate函数开辟空间;

4.thread cache代码

Common.h

  • 公共的头文件,共有部分的代码可以写在这里面;
  • NextObj函数用于获取当前obj对象指向的下一个对象的指针:
  • 将自由链表定义为一个类FreeList,实现链表的基本操作
  • 定义一个管理字节对齐和哈希映射规则的类SizeClass
    • 类中的所有成员函数都定义为静态的内联函数,方便外部直接调用;
    • RoundUp是用来计算当前对象size字节对齐之后对应的size,先判断对象的size在哪个对齐区间,再根据对齐数来计算对齐后的size(调用子函数_RoundUp
    • Index函数用来计算对象size映射到哪一个哈希桶(freelist),根据对象size所属的对齐区间和对齐数,调用_Index函数计算该对象映射到的哈希桶下标;
      注意从第二个对齐区间开始,由于前面部分的对齐数是不同的,因此在计算下标的时候,先要用size减去前面不同对齐数的区间,带入_Index函数计算该对象在当前区间内的相对下标,最后再加上前面所有的哈希桶个数,得到最终的下标;
      例如:如果映射到的是16字节对齐的区域,先要用分配空间减去128,因为前面的128字节是8字节对齐的,要减去,剩下的按照16字节对齐计算,再加上前面减去的桶的数量,以此类推
#pragma once
//公共头文件

#include <iostream>
#include <vector>
#include <assert.h>
#include <thread>
using std::cout;
using std::endl;
using std::vector;

static const size_t MAX_BYTES = 256 * 1024; //ThreadCache能分配对象的最大字节数
static const size_t NFREELIST = 208; // 最大的哈希桶数量

// 访问obj的前4 / 8字节地址空间
static void*& NextObj(void* obj) {
	return *(void**)obj;
}

//自由链表类,用于管理切分好的小内存块
class FreeList {
public:
	void Push(void* obj) {
		assert(obj);
		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;
	}

	void* Pop() {
		assert(_freeList);

		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);

		return obj;
	}

	bool Empty() {
		return _freeList == nullptr;
	}
private:
	void* _freeList = nullptr;
};


// 管理对齐和哈希映射规则的类
class SizeClass {
public:
	//对齐规则
	// 整体控制在最多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)

	//RoundUp的子函数,根据对象大小和对齐数,返回对象对齐后的大小
	static inline size_t _RoundUp(size_t size, size_t align) {
		//if (size % align == 0) {
		//	return size;
		//}
		//else {
		//	return (size / align + 1) * align;
		//}

		//使用位运算能够得到一样的结果,但是位运算的效率很高
		return ((size + align - 1) & ~(align - 1));
	}

	//计算当前对象size字节对齐之后对应的size
	static inline size_t RoundUp(size_t size) {
		assert(size <= MAX_BYTES);

		if (size <= 128) {
			//8字节对齐
			_RoundUp(size, 8);
		}
		else if (size <= 1024) {
			//16字节对齐
			_RoundUp(size, 16);
		}
		else if (size <= 8 * 1024) {
			//128字节对齐
			_RoundUp(size, 128);
		}
		else if (size <= 64 * 1024) {
			//1024字节对齐
			_RoundUp(size, 1024);
		}
		else if (size <= 256 * 1024) {
			//8KB字节对齐
			_RoundUp(size, 8 * 1024);
		}
		else {
			assert(false);
		}
		return -1;
	}

	//Index的子函数,用于计算映射的哈希桶下标
	static inline size_t _Index(size_t size, size_t alignShift) {
		//if (size % align == 0) {
		//	return size / align - 1;
		//}
		//else {
		//	return size / align;
		//}

		//使用位运算能够得到一样的结果,但是位运算的效率很高
		//使用位运算需要将输入参数由对齐数改为对齐数是2的几次幂、

		return ((size + (1 << alignShift) - 1) >> alignShift) - 1;
	}

	//计算对象size映射到哪一个哈希桶(freelist)
	static inline size_t Index(size_t size) {
		assert(size <= MAX_BYTES);

		//每个区间有多少个哈希桶
		static int groupArray[4] = { 16, 56, 56, 56 };
		if (size <= 128) {
			return _Index(size, 3);
		}
		else if (size <= 1024) {
			//由于前128字节不是16字节对齐,因此需要减去该部分,单独计算16字节对齐的下标
			//再在最终结果加上全部的8字节对齐哈希桶个数
			return _Index(size - 128, 4) + groupArray[0];
		}
		else if (size <= 8 * 1024) {
			return _Index(size - 1024, 7) + groupArray[0] + groupArray[1];
		}
		else if (size <= 64 * 1024) {
			return _Index(size - 8 * 1024, 10) + groupArray[0] + groupArray[1] + groupArray[2];
		}
		else if (size <= 256 * 1024) {
			return _Index(size - 64 * 1024, 13) + groupArray[0] + groupArray[1] + groupArray[2] + groupArray[3];
		}
		else {
			assert(false);
		}
		return -1;
	}
};

ThreadCache.h

  • 用于声明ThreadCache类的头文件
  • ThreadCache类包括一个FreeList类型的数组,这就是哈希桶的数组,还有完成ThreadCache功能的成员函数的声明;
  • 定义了由_declspec(thread)声明的TLS指针,用于实现无锁访问
#pragma once
#include "Common.h"

class ThreadCache {
public:
	//申请和释放对象内存
	void* Allocate(size_t size);
	void Deallocate(void* obj, size_t size);

	//从中心缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t alignSize);

private:
	FreeList _freeLists[];
};

//声明_declspec(thread)后,会为每一个线程创建一个单独的拷贝
//使用_declspec(thread)声明了ThreadCache*指针变量,则该指针在该线程中会创建一份单独的拷贝
//pTLSThreadCache指向的对象在本线程内是能够全局访问的,但是无法被其他线程访问到,这就做到了多线程情景下的无锁访问
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

ThreadCache.cpp

  • 这个.cpp文件中来实现ThreadCache中的成员函数:
  • Allocate函数为对象申请内存空间
    • 先获取对齐后的size和对应的哈希桶下标
    • 如果该哈希桶的freeList不为空,就Pop一个内存块给该对象,如果为空就需要向CentralCache申请空间
  • Deallocate函数用于归还对象的内存空间
    • 先获取对象对应的freeList的下标
    • 直接将该内存块插入对应的freeList中
  • FetchFromCentralCache用于从中心缓存获取对象空间
#include "ThreadCache.h"

void* ThreadCache::Allocate(size_t size) {
	assert(size <= MAX_BYTES);
	//获取对齐后的大小及对应的哈希桶下标
	size_t alignSize = SizeClass::RoundUp(size);
	size_t index = SizeClass::Index(size);

	if (!_freeLists[index].Empty()) {
		//若对应的freeList桶不为空,直接pop一个内存块给该对象
		return _freeLists[index].Pop();
	}
	else {
		//否则需要从CentralCache获取内存空间
		return ThreadCache::FetchFromCentralCache(index, alignSize);
	}
}

void ThreadCache::Deallocate(void* obj, size_t size) {
	assert(obj);
	assert(size <= MAX_BYTES);

	//找该对象对应的freeList的桶,直接插入
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(obj);
}

void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignSize) {
	return nullptr;
}

ConcurrentAlloc.h

  • 该头文件用于进一步封装ThreadCache的功能,进而使其能够实现多线程情况下的无锁访问
#pragma once
#include "Common.h"
#include "ThreadCache.h"

static void* ConcurrentAlloc(size_t size) {
	if (pTLSThreadCache == nullptr) {
		//如果pTLSThreadCache指针是空的,就构造一个ThreadCache对象,并指向它
		//则这个ThreadCache对象就是本线程专属的ThreadCache对象
		pTLSThreadCache = new ThreadCache;
	}
	
	//使用pTLSThreadCache访问本线程专属的ThreadCache对象来开辟空间
	return pTLSThreadCache->Allocate(size);
}

static void ConcurrentFree(void* obj, size_t size) {
	assert(pTLSThreadCache);

	pTLSThreadCache->Deallocate(obj, size);
}

UnitTest.cpp

  • 该cpp文件用于测试ThreadCache的功能,重点测试TLS无锁访问的功能
  • 创建两个线程,分别使用ThreadCache申请空间,使用并行监视窗口监视每个进程的内容;
  • c++11的多线程,是将一个线程封装成一个对象
    在这里插入图片描述
    构造thread对象时,传入该线程执行的函数的指针以及参数;
#include "ObjectPool.h"
#include "ConcurrentAlloc.h"
#include "ThreadCache.h"

void Alloc1() {
	for (int i = 0; i < 5; i++) {
		void* obj = ConcurrentAlloc(5);
	}
}

void Alloc2() {
	for (int i = 0; i < 5; i++) {
		void* obj = ConcurrentAlloc(8);
	}
}

void TestTLS() {
	std::thread t1(Alloc1);
	std::thread t2(Alloc2);

	t1.join();
	t2.join();
}


int main() {
	TestTLS();
	return 0;
}

测试结果:
在这里插入图片描述

  • 两个不同的线程都获取和ThreadCache对象,但是通过TLS获取到的是两个不同的ThreadCache,每个线程各一个,两个线程通过pTLSThreadCache指针访问各自的ThreadCache对象

  • 也可以通过输出线程id和pTLSThreadCache指针来观察验证
    在这里插入图片描述
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值