TBB(intel threading building blocks)内存分配算法(一)

开篇:最近研究了一下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内存分配和回收源代码


/*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) {
        // Overflow
        throw_exception(eid_bad_alloc);
    }
    // scalable_aligned_malloc considers zero size request an error, and returns NULL
    if (bytes==0) bytes = 1;
    
	//padded_allocate_handler是一个函数指针,这里指向 padded_allocate
    void* 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返回给用户。



你可能感兴趣的:(算法,exception,cache,null,存储,alignment)