开篇:最近研究了一下TBB的内存分配算法,发现设计的非常优雅,故和大家分析一下它的算法思想。
既然是开篇,那么我们就先从最基本的入手来看一下TBB基于Cache对齐的内存管理算法。首先来明确几个概念。
cache: 现代的cpu都引入的cache的概念,即cpu在参与运算时并非每次都去内存中取得数据的,而是将内存中的一部分数据直接存储在cpu的cache中,cpu中的cache采用的静态随机存储(SRAM)而内存采用的是动态随机存储(DRAM),SRAM的读写速度远大于DRAM。大目前的Intel家族的Cpu中普遍采用的cache行大小为64B,也就是说cpu每次将内存中64B大小的内存块一次性读入缓存,缓存块的地址64*K ~ 64*(K+1) - 1。
既然是开篇,那么我们就先从最基本的入手来看一下TBB基于Cache对齐的内存管理算法。首先来明确几个概念。
cache: 现代的cpu都引入的cache的概念,即cpu在参与运算时并非每次都去内存中取得数据的,而是将内存中的一部分数据直接存储在cpu的cache中,cpu中的cache采用的静态随机存储(SRAM)而内存采用的是动态随机存储(DRAM),SRAM的读写速度远大于DRAM。大目前的Intel家族的Cpu中普遍采用的cache行大小为64B,也就是说cpu每次将内存中64B大小的内存块一次性读入缓存,缓存块的地址64*K ~ 64*(K+1) - 1。
下面来看一下TBB内存分配和回收源代码
/*TBB采用128B来对齐,虽然现在intel的处理cache采用64B来对齐,这里可能是为了以后的兼容性。也就是说分配的内存块的起始地址一定是128的倍数, NFS_LineSize一定的2的幂, 之所以要使用128B来对齐是为了用户请求的两个内存不可能被同一行cache给缓存,防止出现伪共享*/
static size_t NFS_LineSize = 128;
size_t NFS_GetLineSize() {return NFS_LineSize;
}
//这是进行内存分配的函数。
// @param : n 请求分配的内存块数量
// @param : element_size,用户请求的每块的内存大小
// @return: 返回给用户的内存地址。
void* NFS_Allocate( size_t n, size_t element_size, void* /*hint*/ ) {size_t m = NFS_LineSize;//两个assert是判断NFS_LineSize是否是2的幂__TBB_ASSERT( m<=NFS_MaxLineSize, "illegal value for NFS_LineSize" );__TBB_ASSERT( (m & (m-1))==0, "must be power of two" );//计算用户总共需要的内存大小size_t bytes = n*element_size;//判断请求的内存大小是否合法if (bytes<n || bytes+m<bytes) {// Overflowthrow_exception(eid_bad_alloc);}// scalable_aligned_malloc considers zero size request an error, and returns NULLif (bytes==0) bytes = 1;//padded_allocate_handler是一个函数指针,这里指向 padded_allocatevoid* result = (*padded_allocate_handler)( bytes, m );//如果分配失败则抛出异常if (!result)throw_exception(eid_bad_alloc);//判断返回用户的内存地址块是否是NFS_LineSize对齐的,NFS_LineSize是2的幂__TBB_ASSERT( ((size_t)result&(m-1)) == 0, "The address returned isn't aligned to cache line size" );return result;
}
//释放内存
void NFS_Free( void* p ) {(*padded_free_handler)( p );
}
//进行内存分配
// @param : bytes 用户实际需要的内存大小
// @param : alignment 分配算法采用的对齐大小, 传入的值一般是NFS_LineSize。
static void* padded_allocate( size_t bytes, size_t alignment ) { unsigned char* result = NULL;//向系统请求分配alignment+bytes的内存块,注意要对请求alignment大小的内存,是为了下面进行内存对齐,unsigned char* base = (unsigned char*)malloc(alignment+bytes);if( base ) { // Round up to the next line// 这里是进行内存对齐,这里采用了一个非常巧妙的方法,在后方对其进行详细解释result = (unsigned char*)((uintptr_t)(base+alignment)&-alignment);// Record where block actually starts.// 这里将malloc分配的起始地址写入result前的4个字节中,由于alignment > 4且malloc分配的地址是4字节对齐\// 故而result前一定含有至少4个空字节,记录base的起始是为了释放时使用((uintptr_t*)result)[-1] = uintptr_t(base);}return result;
}
// 释放内存
// @param : p 函数NFS_Allocate返回给用户的内存地址
static void padded_free( void* p ) {if( p ) {// 0x4096以下的内存空间由系统使用,不对外分配,故而不能释放__TBB_ASSERT( (uintptr_t)p>=0x4096, "attempt to free block not obtained from cache_aligned_allocator" );// Recover where block actually starts// 取出NFS_Allocate分配的该内存块的真实起始地址,该地址是由malloc返回的unsigned char* base = ((unsigned char**)p)[-1];// 进行真实块地址的合法性判断,对应于NFS_Allocate采用的字节对齐__TBB_ASSERT( (void*)((uintptr_t)(base+NFS_LineSize)&-NFS_LineSize)==p, "not allocated by NFS_Allocate?" );free(base);}
}
result = (unsigned char*)((uintptr_t)(base+alignment)&-alignment);
这个NFS_LineSize的对齐就是由上面的公式计算出来的。由于base是4的倍数, alignment也是2的幂,假设alignment=2^k。
我们将上式进行简写一下:
r = (n + m)& (-m).
下面使用二进制表示,X代表0或者1
n = XX...XX00
m = 0...010...0, 1后总共m个0
-m= 1...110...0, 补码表示
故而(n + m) & (-m) 一定是XX...XXX0..0, 即最后含有至少m个0, 所以r是m的倍数。
综合上面的分析,其算法流程可以归纳如下:
1. 用户申请n块大小为m的内存空间。
2. 计算用户申请的内存空间大小s = m * n, 使用malloc函数申请 s + NFS_LineSize的内存空间,起始内存地址为base
3. 计算从base开始(不包含base)的第一个NFS_LineSize的倍数地址result。
4. 将base写入result前的4个字节中。
5. 将result返回给用户。