tcmalloc效率低的原因分析
上文中tcmalloc的效率相比ptmalloc,除了大内存(大于40KB)申请效率较高,其他情况都远低于ptmalloc,本文采用代码跟踪方式查找效率低下的原因所在。
当应用层调用malloc时,实际调用的是tc_malloc函数
void* tc_malloc(size_t size) PERFTOOLS_NOTHROW {
return malloc_fast_path<tcmalloc::malloc_oom>(size);
}
static void * malloc_fast_path(size_t size) {
if (PREDICT_FALSE(!base::internal::new_hooks_.empty())) {
return tcmalloc::dispatch_allocate_full<OOMHandler>(size);
}
ThreadCache *cache = ThreadCache::GetFastPathCache();
if (PREDICT_FALSE(cache == NULL)) {
return tcmalloc::dispatch_allocate_full<OOMHandler>(size);
}
uint32 cl;
if (PREDICT_FALSE(!Static::sizemap()->GetSizeClass(size, &cl))) {
return tcmalloc::dispatch_allocate_full<OOMHandler>(size);
}
size_t allocated_size = Static::sizemap()->ByteSizeForClass(cl);
if (PREDICT_FALSE(!cache->TryRecordAllocationFast(allocated_size))) {
return tcmalloc::dispatch_allocate_full<OOMHandler>(size);
}
return CheckedMallocResult(cache->Allocate(allocated_size, cl, OOMHandler));
}
经过调试发现,最大时间延迟出现在malloc_fast_path函数中ThreadCache *cache = ThreadCache::GetFastPathCache(),调用此函数会产生5us的延时,接着跟踪此函数
inline ThreadCache* ThreadCache::GetFastPathCache() {
#ifndef HAVE_TLS
return GetCacheIfPresent();
#else
return threadlocal_data_.fast_path_heap;
#endif
}
HAVE_TLS在config.h头文件已经定义,GetFastPathCache函数只是返回threadlocal_data_.fast_path_heap这样一个变量,从表面看返回一个变量不应该耗时5us
struct ThreadLocalData {
ThreadCache* fast_path_heap;
ThreadCache* heap;
bool use_emergency_malloc;
};
static __thread ThreadLocalData threadlocal_data_
CACHELINE_ALIGNED ATTR_INITIAL_EXEC;
从上面代码看出threadlocal_data_.fast_path_heap确实只是结构体中的一个变量,但定义threadlocal_data_时的一句修饰语句在这里起到了决定性作用。
__thread的修饰符:表示每一个线程有一份独立的实体,每一个线程都不会干扰。这句修饰也是tcmalloc的精髓,既看似全局变量,实际是线程私有数据,__thread在底层实现上依赖于pthread的key机制,所以实际上使用__thread和使用pthread的key机制实现是一样的,在tcmalloc的源码中也体现了这一点,当去掉HAVE_TLS宏定义,其底层确实使用了pthread的key机制
inline ThreadCache* ThreadCache::GetThreadHeap() {
#ifdef HAVE_TLS
return threadlocal_data_.heap;
#else
return reinterpret_cast<ThreadCache *>(
perftools_pthread_getspecific(heap_key_));
#endif
}
因每一次的malloc和free都需要通过pthread_getspecific获取本地的ThreadCache,到这里可以确定效率低的问题出在pthread_getspecific函数中,继续跟踪内核的源码实现,以下代码删除了函数中部分无关内容
void *pthread_getspecific (pthread_key_t key)
{
void *pvalue = LW_NULL;
__pthreadDataGet(key, &pvalue);
return (pvalue);
}
static INT __pthreadDataGet (long lId, void **ppvData)
{
iHash = __PX_KEY_THREAD_HASH(ulMe);
__PX_KEY_LOCK(pkeyn);
for (plineTemp = pkeyn->PKEYN_plineKeyHeader[iHash];
plineTemp != LW_NULL;
plineTemp = _list_line_get_next(plineTemp)) {
pkeyd = (__PX_KEY_DATA *)plineTemp;
if (pkeyd->PKEYD_ulOwner == ulMe) {
*ppvData = pkeyd->PKEYD_pvData;
break;
}
}
__PX_KEY_UNLOCK(pkeyn);
return (ERROR_NONE);
}
#define __PX_KEY_LOCK(pkeyn) API_SemaphoreMPend(pkeyn->PKEYN_ulMutex, LW_OPTION_WAIT_INFINITE)
#define __PX_KEY_UNLOCK(pkeyn) API_SemaphoreMPost(pkeyn->PKEYN_ulMutex)
pthread_getspecific函数在内核中最终调用__pthreadDataGet,该函数的流程:
1,内核使用Hash散列方式分散了链表数据以提高查找速度,先根据线程ID找到key链表头。
2,因为各个线程的本地私有数据都保存在key链表中,因此需要调用__PX_KEY_LOCK加锁,实际该宏使用互斥信号量加锁。
3,从链表中遍历查找和当前thread ID匹配的线程本地私有数据。
4,调用__PX_KEY_UNLOCK释放互斥信号量。
到此发现,内核的pthread key的实现机制,和tcmalloc原始设计思想是有冲突的,tcmalloc想通过一个线程本地私有数据的访问不影响其他线程来提高效率,但是该本地私有数据在内核中是通过一个共享链表来实现。因此,效率低的最主要原因是内核中对链表的加解锁操作,其次是对链表的遍历,后面的tcmalloc优化也将重点解决此问题。待续…