文章目录
ConcurrentMemoryPool原码链接:https://gitee.com/yangyangqingxuan/c-learning-code/tree/b5a78cbe774189769d9a4951c9e48d93f80d026e/ConcurrentMemoryPool2
1.需求分析
C/C++下内存管理是件很头疼的事,分配足够的内存、追踪内存的分配、在不需要的时候释放内存—这个任务很复杂。如果直接使用系统调用malloc/free、new/delete进行内存分配和释放,则会有以下弊端:
1.调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销;
2.频繁使用时会产生大量内存碎片,从而降低程序运行效率;
3.容易造成内存泄漏;
内存池(memory pool)是代替直接调用malloc/free、new/delete进行内存管理的常用方法,当我们申请内存空间时,首先到我们的内存池中查找合适的内存块,而不是直接向操作系统申请,优势在于:
1.比malloc/free进行内存申请/释放的方式快;
2.不会产生或很少产生堆碎片;
3.可避免内存泄漏;
2.高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀了,但是我们的项目原型tcmalloc在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池要考虑如下几个方面。
1.性能问题
2.多线程环境下,内存申请和内存释放所引起的锁竞争问题
3.内存碎片问题
Concurrent Memory pool主要由以下3个部分构成: |
1.Thread Cache:
线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个Cache,这也就是这个并发线程池高效的地方。(这里使用了TLS(thread local storage)线程本地存储,是每个线程独有的全局变量)
2.Central Cache:
通过单例模式实现了程序中只有一个中心缓存,被多个线程所共享。thread cache 是按需从Central Cache中获取的对象。Central Cache合适的时机回收Thread Cache中的对象,避免一个线程占用了太多的内存,而其它线程的内存吃紧,达到了内存分配在多个线程中更均衡的按需调度的目的。
Central Cache是存在竞争的,所以从这里取内存对象时需要加锁。首先这里用的是桶锁,其次只有Thread Cache的没有内存对象时才会找Central Cache,所以这里竞争不会很激烈。
3.Page Cache:
页缓存是Central Cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,Central Cache没有内存对象时,从Page Cache分配出一定数量的page,并切割成定长大小的小块内存,分配给Central Cache。当一个span的几个跨度页的对象都回收以后,Page Cache会回收Central Cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片(外碎片)的问题。
怎么实现每个线程都拥有自己唯一的线程缓存呢?
静态详解链接:https://blog.csdn.net/evilswords/article/details/8191230
动态详解链接:https://blog.csdn.net/yusiguyuan/article/details/22938671
3.内部细节构成介绍
3.1高并发内存池-Thread Cache
Thread Cache.h
#pragma once
#include "Common.h"
class ThreadCache
{
public:
// 申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELIST];
};
// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
Thread Cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个Thread Cache对象,这样每个线程在这里获取对象和释放对象时是无锁的,大大提高了内存池的效率。
不分段的话若全是按8字节分,则会需要32768个桶,但但是每个桶都是16Byte则会差生较多的内存碎片。因此这里采用了不同段的内存使用不同的内存对齐规则,既控制了桶的数量不会太多,又整体将内存碎片浪费控制在10%左右
管理对齐和映射的关系如下
[1,128] 8byte对齐 _freelist[0,16)
[129,1024] 16byte对齐 _freelist[16,72)
[1025,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)
内碎片的计算方法:
对于7/8;15/(128+16);127/(1024+128);1023/(8*1024+1024);
对于进行哈希映射的数组的大小是208
208 = (128/8) + (1024-128)/16 + (8*1024-1024)/128 + (64*1024-8*1024)/1024 + (256*1024-64*1024)/(8*1024)
内存碎片浪费率分别为:7/8,15/144,127/1152,1023/8 * 1024 + 1024均在10%左右(除了第一块按照8字节对齐的浪费率)。
对齐映射规则
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return ((bytes + alignNum - 1) & ~(alignNum - 1));
}
//内联函数:调用频繁,因此写成内联函数
//向上对齐
static inline size_t RoundUp(size_t size)
{
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//size > 256 * 1024 byte
{
return _RoundUp(size, 1<<PAGE_SHIFT);
}
}
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
//计算映射的哪一个自由链表桶中
static inline size_t Index(size_t bytes)
{
assert(bytes <= MAX_BYTES);
static int group_array[4] = { 16, 56, 56, 56 };
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[0] + group_array[1];
}
else if (bytes <= 64 * 1024)
{
return _Index(bytes - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
else if (bytes <= 256 * 1024)
{
return _Index(bytes - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
}
else
{
assert(false);
return -1;
}
}
Thread Cache,h
#pragma once
#include"Common.h"
class ThreadCache
{
public:
// 申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);//给定size,计算出在哪个桶
//从中心获取对象
void* FetchFromCentralCache(size_t index, size_t size);
//释放对象时,链表过长,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELISTS];
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
申请内存:
①:当申请内存size <=256k时,在Thread Cache中申请内存,通过size计算出自由链表中的桶的位置,如果自由链表对应的桶中有内存,则直接从Free List[i]的首部取出一块内存,因为这里使用的是哈希映射,并且没有锁竞争,因此时间复杂度位O(1)。
②③:当Free List中没有对象时,则会批量的从Central Cache中获取一定数量的内存,返回一个内存并将之前批量申请的剩余的内存挂到对应free list桶中。
释放内存:
当FreeList[i]中没有对象时,则批量从Central cache中获取一定数量的对象,剩余的n-1个对象插入到自由链表并返回一 个对象。
3.2高并发内存池-Central Cache
① Central Cache本质是由一个哈希映射的span对象自由双向链表构成
②为了保证全局只有唯一的Central Cache,这个类因此可以被设计称单例模式(这里使用的是饿汉模式)
饿汉模式:构造函数私有,对象设为静态私有。拷贝构造和赋值重载设为delete(防拷贝)
Central Cache.h
#pragma once
#include"Common.h"
//单例模式 -》饿汉模式
// 采用饿汉模式,在main函数之前单例对象已经被创建
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t size);
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
//将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t size);
private:
SpanList _spanLists[NFREELISTS];
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
};
span对象:一个由多个页组成的内存块,每页大小是8K。
Span对象
//管理多个连续页的大块内存跨度结构
struct Span
{
PAGE_ID _pageId = 0;//大块内存起始页的页号
size_t _n = 0;//页的数量
Span* _next = nullptr;//双向链表的结构
Span* _prev = nullptr;
size_t _objSize = 0;//切好的小对象的大小
size_t _usecount = 0;//切好的小块内存,被分配给thread cache的计数
void* _freeList = nullptr;//切好的小块内存的自由链表
bool _isUse = false;//是否在被使用
};
SpanList
:一个双向链表,插入删除效率较高
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
Span* Begin()
{
return _head->_next;
}
Span* End()
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
void PushFront(Span* span)
{
Insert(Begin(), span);
}
Span* PopFront()
{
Span* front = _head->_next;
Erase(front);
return front;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
public:
std::mutex _mtx;//桶锁
private:
Span* _head;
};
Central Cache申请内存:
1.当Thread Cache中没有内存时,就会批量向Central Cache申请一些内存对象,这里的批量获取对象的数量采用了类似网络tcp协议中拥塞控制的满开始算法;Central Cache也有一个哈希映射spanlist,spanlist中挂着span对象,从span中取出对象给ThreadCache,这个过程是需要加锁的,这里使用的是一个桶锁,尽可能提高效率(桶锁限制的只是当两个线程取相同size的内存块时才会发生线程锁竞争)。
2.Central Cache映射的spanlist中所有span对象都没有内存以后,则需要向PageCache申请一个新的span对象,拿到span对象之后,将span管理的内存块按大小切好作为自由链表连接到一起,然后从span中取对象返回给ThreadCache.
3.Central Cache的中挂的span中use_count记录了该span借给ThreadCache多少个对象出去,每借出一个use_count++。当这个span的使用计数为0,说明这个span所有的内存对象都是空闲的,然后将它交给Page Cache合并成更大的页,减少内存碎片(外碎片)。起到承上启下的作用。
Central Cache释放内存:
当thread cache过长或者线程销毁,则会将内存释放回Central cache中,每释放一个内存对象,检查该内存所在的span使用计数是否为空,释放回来一个时–use_count。
当use_count减到0时则表示所有对象都回到了span,则将span释放回Page cache,在Page cache中会对前后相邻的空闲页进行合并。
如何将Thread Cache中的内存对象回收到Central Cache中呢?
实际上每当Page Cache借出一定数量页的span时,就会对该span中的页号和该span一 一建立映射,并且将剩余的span对象重新挂到其对应的的桶上,并将起始页和尾页与剩余的span建立映射,以确保往后回收的时候,可以进行前后页合并。达到了内存分配在多个线程中更均衡的按需调度的目的。
3.3高并发内存池-Page Cache
页缓存是在Central Cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,Central Cache没有内存对象时,从Page Cache分配出一定数量的Page,并切割定长大小的小块内存,分配给Central
Page Cache是一个以页为单位的span自由链表。
为了保证全局只有唯一的Page Cache,这个类被设计成了单例模式。采用了饿汉模式。
#pragma once
#include"Common.h"
#include"ObjectPool.h"
#include"PageMap.h"
//采用饿汉模式,在main函数之前单例对象已经被创建
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
//释放空闲span回到PageCache,合并相邻的span
void ReleaseSpanToPageCache(Span* span);
在SpanList中获取一个K页的Span对象,如果没有或者申请内存大于128页,则直接去系统申请
Span* NewSpan(size_t k);
// 为了锁住SpanList,可能会存在多个线程同时来PageCache申请span
std::mutex _pageMtx;
private:
PageCache() {};
ObjectPool<Span> _spanPool;
SpanList _spanList[NPAGES];
//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
};
PageCache没有采用桶锁而使用了整体的大锁的原因:
频繁调度锁资源,会引起线程的频繁切换,这会使得上下文数据被频繁的保存,严重占用CPU资源。
Page Cache申请流程:
1.当CentralCache向PageCache申请内存时,page Cache先检查的是对应位置有没有span,若没有则向更大的页寻找一个span,例如申请的是4页,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page的span分裂成一个4页的page span 和一个6页page span。
2.若找到_spanList[128]都没有合适的span,则向系统使用VirtualAlloc或者mmap,brk等方式申请128页page span关在自由链表中,在重复第一点中的步骤,对其进行切分。
brk,mmap,malloc底层详解:https://blog.csdn.net/gfgdsg/article/details/42709943
3.Central Cache 和 Page Cache的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,Central Cache中的哈希桶,是按跟Thread Cache一样大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
4.申请内存流程
Linux平台:
使用brk或mmap向系统直接申请堆内存
申请小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)。
申请大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),
Windows平台下使用VirtualAlloc向系统申请和释放堆内存
//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage * (1 << 13), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//kpage是字节大小
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
//sbrk unmmap等
#endif // _WIN32
}
4.1向系统申请内存
我们的高并发内存池项目对外仅提供两个接口,对于申请内存的接口是ConcurrentAlloc()
①如果申请的内存超过256 K(32页),则直接会越过Thread Cache去调用Page Cache的NewSpan()函数,NewSpan的功能是获取一个K页的span对象,这里有两种情况,第一种情况是申请的页数介于32页和128页之间,若PageCache里面有,则向PageCache索要,若索要的页数所在的spanList链表为空则向后寻找更大的页,若一直没有相匹配的页,则最终会向堆申请128页,然后进行切分,返回你申请的那一部分内存。第二种情况是申请的页数大于128页,这种情况会直接调用VirtualAlloc()向系统申请内存。
②如果申请的内存小于256 K,则会先在线程独享的ThreadCache里所提供的Allocate()接口,计算出要找哪一个索引下标index的freelist,如果有则直接返回。没有就会调FetchFromCentralCache接口,通过你要的内存size计算出需要返回的批量个数(慢启动方式),并且调用CentralCache的FetchRangeObj()计算出Central Cache中心缓存实际真正能给你返回的数量,这时就得调用Central Cache 中的GetOneSpan()接口在获取一个非空的SpanCentral Cache ,如果Central Cache中对应的SpanList[i]中没有空闲的Span或者内存被用完了,这时就找Page Cache要,通过调用Page Cache中的New Span接口来获取一个K页的span对象,计算索引看Page Cache中是否有合适的页,如果没有则需要往后找更大的页,如果一直没找到,就只能向系统申请一个128页的内存了,然后进行切分,由于Central Cache中的span都是已经切好的,因此在返回这K页的内存时,需要提前按照之前计算字节向上对齐的大小切好,然后再返回给Central Cache,然后计算出Central Cache中心缓存实际真正能给你返回的数量。然后返回的内存块挂接到Thread Cache的freeList[i]上,取出一个内存块返回给用户。
5.释放内存流程
还回来的内存挂接在Thread Cache中对应的Free List中,当其中一个Free List中的Size(当前的内存块数)大于Max Size时,从该Free List取出Max Size个内存归还到Central Cache,但是每一块小内存都可能来自于不同的Span,这里就要调用MapObjectToSpan()接口根据每块小内存的起始地址计算出它所对应的页号,通过一个基数树映射根据页号找到所对应的Span,那么就可以确保每一小块内存都能归还到之前所切出来的对应的Span中,当Central Cache中的每一大块Span里面有一个usecount,他记录的是分配出去的内存块数,当他为0时,说明该span对象之前借出去的小内存都换回来了,这时就可以将该span归还给Page Cache,通过PageId(页号)和n(页数),进行在Page Cache中向前和向后合并,形成更大的页,当来缓解内存碎片问题。
6.性能测试
测试平台Visual Studio 2019
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&, k]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
//v.push_back(malloc(16));
v.push_back(malloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
free(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks*rounds*ntimes, malloc_costtime.load() + free_costtime.load());
}
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
//v.push_back(ConcurrentAlloc(16));
v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
ConcurrentFree(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks*rounds*ntimes, malloc_costtime.load() + free_costtime.load());
}
int main()
{
size_t n = 10000;
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n, 4, 10);
cout << endl << endl;
BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << endl;
return 0;
}
可看出消耗cpu占用率,malloc,free这两库函数占比率最大,是系统开销大的主要原因。
7.项目不足及扩展学习
项目的独立性不足:
1.不足:
①当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以没有完全脱离malloc。
②按理来说连thread、mutex等库里面的也不可以使用,因为你不确定是否他们的库里实现使用了new或者malloc申请内存
③如果此时threadCache销毁了,如果他还有大量内存未释放这样会造成严重的内存泄漏,这时该怎么办?
2.解决方案:
①项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk(Linux平台)、VirarulAlloc(Windows平台)等向系统申请,使用定长内存池,在new span时向内存池中获取内存,定长内存池的内存是通过SystemAlloc()来获取内存的会脱离malloc的调用
定长内存池代码实现如下
#pragma once
#include"Common.h"
template<class T>
class ObjectPool
{
public:
T* New()
{
T* obj = nullptr;
//优先在自由链表中申请内存
if (_freeList)
{
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else
{
//剩余内存不够一个对象大小时,需要重新开辟内存空间
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
/*_memory = (char*)malloc(_remainBytes);*/
_memory = (char*)SystemAlloc(_remainBytes >> 13);//参数是以页为单位
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= objSize;
}
//定位new,显式调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//显示调用析构函数清理对象
obj->~T();
*(void**)obj = _freeList;
_freeList = obj;
}
private:
char* _memory = nullptr;//指向大块内存的指针 (char* 方便于从内存中取内存,+-操作方便)
size_t _remainBytes = 0;//大块内存在被切分过程中剩余的字节数
void* _freeList = nullptr;//自由链表:回收不再使用的内存
};
②不同平台替换方式不同。 基于lunix的系统上的glibc,使用了weak alias的方式替换。具体来说是因为这些入口函数都被定义成了weak symbols。
再加上gcc支持 alias attribute,所以替换就变成了这种通用形式:
void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc))),因此所有malloc的调用都跳转到了tc_malloc的实现
GCC attribute 之weak,alias属性(实际上就是当我们调用malloc的时候,他会自动跳转到我们自己实现的tc_malloc)
https://blog.csdn.net/BingoAmI/article/details/78683906
有些平台不支持这样的东西,需要使用hook的钩子技术来做。
https://www.cnblogs.com/feng9exe/p/6015910.html
③针对上面的第三点会造成耽误内存还回CentralCache,导致小页无法合大,注册一个线程结束后回调清理ThreadCache的内存函数。但是实际上线程局部存储机制中在对象内存在线程开始后分配,线程结束时回收,且每个线程有该对象自己的实例。