0
点赞
收藏
分享

微信扫一扫

ThreadCache 自由链表哈希桶与对象大小映射的实现方式

自由链表的哈希桶跟对象大小的映射关系

背景

如果每个256KB的threadcache中用于分配的内存块全用8字节的,那就会产生32768个内存块,需要32768个桶/自由链表(一个用于分配8字节给目标对象的桶,一个2*8字节的,一个3*8字节...... 共32768个),管理起来麻烦且效率低。

通过一定的映射规则,减少桶/内存块的个数,提高管理效率,更合理的管理用户申请的和实际分配的内存块


映射规则:

ThreadCache 自由链表哈希桶与对象大小映射的实现方式_字节对齐

不是每个内存块大小一个桶,而是一段范围大小一个桶;

使用者申请一个对象大小的空间,1.是给他发一个大块空间,2.还是多个小块空间。

把/让对象根据给他分配的单个块大小(不同种类)进行内存对齐,哪种内碎片最小,就用哪种分配方案

// 管理对齐和映射等关系
class SizeClass
{
public:
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 				8byte对齐 		freelist[0,16) 多个桶即自由链表,在整个threadcache的hash中的下标范围,
// [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)

//最多10%左右的内碎片浪费,证明:
//128正好能被16整除,那么给129会分配129+15=144字节,会浪费15字节(最多也就只能浪费15字节,因为16就对齐了)
//15/144 ~ 0.10; 分子最大15,分母越大,内碎片浪费的比例越少

// freelist[0,16) 表示 按8字节对齐的桶即自由链表,在整个threadcache的hash中的下标范围;有16个按8字节对齐的桶,
// 如果准备给用户的对象分配1个字节,那么使用对应8字节桶的下标0/第1个桶(自由链表)中的内存块;若2个字节,使用8字节桶的下标1的自由链表中的内存块;......
// 每增多一个对齐数大小,就使用下一个对应对齐数的桶中的内存块;按8字节对齐的桶只有16个,即如果需要分配的大小超过了8*16字节,就向后找更大对齐数对应的桶(自由链表)

// 给对象分配空间的示例:
// 用户给一个对象申请空间,底层根据这个对象大小在哪个范围,然后按照对应的对齐数对齐,需要几个内存块,就到第几个桶中申请内存块(这个对齐数对应范围的桶)
// 比如:9字节大小的对象申请空间,就把8字节对齐数范围中的第二个桶中内的内存块分配给它
// 这样做的好处是保证每个桶(自由链表) 每次分配/回收 的内存块都是固定且连续的,更加方便/合理的管理内存块;比如:8字节对齐数的第二个桶,每次分配/回收 连续2个内存块(共16字节)

重点---理解映射规则

freelist[0,16) 表示 按8字节对齐的桶即自由链表,在整个threadcache的hash中的下标范围;有16个按8字节对齐的桶,

如果准备给用户的对象分配1个字节,那么使用对应8字节桶的下标0/第1个桶(自由链表)中的内存块;若2个字节,使用8字节桶的下标1的自由链表中的内存块;......

每增多一个对齐数大小,就使用下一个对应对齐数的桶中的内存块;按8字节对齐的桶只有16个,即如果需要分配的大小超过了8*16字节,就向后找更大对齐数对应的桶(自由链表)

给对象分配空间的示例:

用户给一个对象申请空间,底层根据这个对象大小在哪个范围,然后按照对应的对齐数对齐,需要几个内存块,就到第几个桶中申请内存块(这个对齐数对应范围的桶)

比如:9字节大小的对象申请空间,就把8字节对齐数范围中的第二个桶中内的内存块分配给它

这样做的好处是保证每个桶(自由链表) 每次分配/回收 的内存块都是固定且地址连续的,更加方便/合理的管理内存块;比如:8字节对齐数的第二个桶,每次分配/回收 连续2个内存块(共16字节)


class SizeClass 申请 -->计算 实际分配大小

小细节

ThreadCache 自由链表哈希桶与对象大小映射的实现方式_位运算_02

//判断申请size大小,使用对齐数alignNum,实际需要分配多少字节
	//方法一:
	/*size_t _RoundUp(size_t size, size_t alignNum)//alignNum对齐数
	{
		size_t alignSize;
		if (size % alignNum != 0)
		{
			alignSize = (size / alignNum + 1)*alignNum;//(size / alignNum + 1)几个对齐数
			//或者
			//alignSize = (size+(alignNum-1)/alignNum) * alignNum;
		}
		else //能被对齐数整除
		{
			alignSize = size;
		}

		return alignSize;
	}*/
	// 1-8 //方法二:
	static inline size_t _RoundUp(size_t bytes, size_t alignNum)
	{
		//(bytes + alignNum - 1) 申请size大小,+ (alignNum - 1)  / alignNum 得到实际分配多少个alignNum大小,余数舍弃,再*alignNum=实际分配字节
		//即alignSize = (size+(alignNum-1)/alignNum) * alignNum; 就是下面位运算要表达的效果
		return ((bytes + alignNum - 1) & ~(alignNum - 1));//位运算效率高
	}

SizeClass

// 申请 ——> 实际分配大小
class SizeClass	//不需要实际的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)
	
	//判断申请size大小,使用对齐数alignNum,实际需要分配多少字节
	//方法一:
	/*size_t _RoundUp(size_t size, size_t alignNum)//alignNum对齐数
	{
		size_t alignSize;
		if (size % alignNum != 0)
		{
			alignSize = (size / alignNum + 1)*alignNum;//(size / alignNum + 1)几个对齐数
			//或者
			//alignSize = (size+(alignNum-1)/alignNum) * alignNum;
		}
		else //能被对齐数整除
		{
			alignSize = size;
		}

		return alignSize;
	}*/
	// 1-8 //方法二:
	static inline size_t _RoundUp(size_t bytes, size_t alignNum)
	{
		//(bytes + alignNum - 1) 申请size大小,+ (alignNum - 1)  / alignNum 得到实际分配多少个alignNum大小,余数舍弃,再*alignNum=实际分配字节
		//即alignSize = (size+(alignNum-1)/alignNum) * alignNum; 就是下面位运算要表达的效果
		return ((bytes + alignNum - 1) & ~(alignNum - 1));//位运算效率高
	}
	//综述: 申请size大小,返回实际需要分配多少字节
	static inline size_t RoundUp(size_t size)//inline内联函数,直接用类名调用,不需要创建对象
	{
		if (size <= 128)
		{
			return _RoundUp(size, 8);
		}
		else if (size <= 1024)
		{
			return _RoundUp(size, 16);
		}
		else if (size <= 8*1024)
		{
			return _RoundUp(size, 128);
		}
		else if (size <= 64*1024)
		{
			return _RoundUp(size, 1024);
		}
		else if (size <= 256 * 1024)
		{
			return _RoundUp(size, 8*1024);
		}
		else
		{
			return _RoundUp(size, 1<<PAGE_SHIFT);
		}
	}

    //

	/*size_t _Index(size_t bytes, size_t alignNum)
	{
        if (bytes % alignNum == 0)
        {
            return bytes / alignNum - 1;
        }
        else
        {
            return bytes / alignNum;
        }
	}*/

	// 1 + 7  8
	// 2      9
	// ...
	// 8      15
	
	// 9 + 7 16
	// 10
	// ...
	// 16    23
	static inline size_t _Index(size_t bytes, size_t align_shift)//2^align_shift = bytes
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;//-1得下标,否则得第几个
	}

	// 计算映射的哪一个自由链表桶---下标
	static inline size_t Index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);

		// 每个区间有多少个链
		static int group_array[4] = { 16, 56, 56, 56 };//8字节对齐的桶/链有16个,16字节对齐的....
		if (bytes <= 128){
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024){
			return _Index(bytes - 128, 4) + group_array[0];
		}
		else if (bytes <= 8 * 1024){
			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (bytes <= 64 * 1024){
			return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
		}
		else if (bytes <= 256 * 1024){
			return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
		}
		else{
			assert(false);
		}

		return -1;
	}

	// 一次thread cache从中心缓存获取多少个
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);

		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		// 小对象一次批量上限高
		// 小对象一次批量上限低
		int num = MAX_BYTES / 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;
	}
};


总结:

如果全用8字节对齐,则需要258*1024/8个节点,太多了,管理效率低;

需要的空间小时,就用小字节对齐;大时,用大字节对齐

举报

相关推荐

0 条评论