自由链表的哈希桶跟对象大小的映射关系
背景
如果每个256KB的threadcache中用于分配的内存块全用8字节的,那就会产生32768个内存块,需要32768个桶/自由链表(一个用于分配8字节给目标对象的桶,一个2*8字节的,一个3*8字节...... 共32768个),管理起来麻烦且效率低。
通过一定的映射规则,减少桶/内存块的个数,提高管理效率,更合理的管理用户申请的和实际分配的内存块
映射规则:
不是每个内存块大小一个桶,而是一段范围大小一个桶;
使用者申请一个对象大小的空间,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 申请 -->计算 实际分配大小
小细节
//判断申请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个节点,太多了,管理效率低;
需要的空间小时,就用小字节对齐;大时,用大字节对齐