参考【项目设计】高并发内存池什么是内存分配器内存分配器本质上是操作系统和应用程序之间的一个‘中间件’。没有内存分配器时程序通过brk或mmap函数申请内存brk或mmap函数申请的是一整页的。brkint只需要4B也会申请8KB巨大的内部碎片。释放后还给os。有了内存分配器mallocmalloc4B程序通过brk或mmap函数申请8KB它会在上面切一小块。如果不用了会放在freelist上。有了定长内存池。memoryPoolMalloc4B程序通过brk或mmap函数申请8KB它会在上面切一小块。如果不用了会放在freelist上。malloc和定长内存池他们俩都是内存分配器思想也非常像。malloc是一个各种类型通用的分配器定长内存池则是只分配某一钟类型。malloc是一个通用的内存池而定长内存池是专门针对申请定长对象而设计的因此在这种特殊场景下定长内存池的效率更高正所谓“尺有所短寸有所长”。普通new和内存池的区别new先调用malloc开辟一块空间。再用定位new在这片空间上构造对象内存池。使用brk或mmap函数向堆里申请一大块内存。在大内存块上移动char型指针。再用定位new定长内存池的实现直接使用brk或mmap函数向堆里申请一大块内存。申请完释放放到freeLIst中阶梯式内存对齐优化tc支持的最大容量是256KB如果完全不对齐每种字节大小建一个桶要建 262,144 个哈希桶如果全部按 8 字节对齐262,144 / 8 32,768 个桶阶梯式对齐策略图tcccpc哪些地方需要把new换成定长内存池在pc中向os申请一个span要把new换成从spanpool中拿在cc还给pc span时若有连续span应合并成一个。freepccc中的spanlist的头结点创建时需要从new换成定长内存池threadCache需要从定长内存池创建pTLSThreadCache new ThreadCache;pccc因为时单例模式在静态区不用换。访问定长内存池时需要加锁吗spanpool池不用加锁因为这个span的申请是在pc的大锁中只准一个线程访问。spanlist的头结点span也不用要么在cc的桶锁中要么在pc的大锁里threadcachePool需要加锁。不然线程竞态线程A申请时char* memory会乱。自旋锁自定义退避策略项目中有用过那些锁吗tc使用了TLS避免了线程竞争。cc使用的是spanlist锁桶锁pc使用的是将整个pc都锁起来的锁。spanlists大锁用基数树代替unorderedmap做页面与span的映射。定长内存池中用锁锁threadCachePool。基数树的结构基数树和页表一模一样。数据结构就是一个数组【0】就代表0号页放的是指向0号页的span。每次我们向os拿出内存页时就会在基数树里记录。页表就是每次拿内存页就把虚拟页号物理页号记录下来基数树在哪里起作用在我们free(ptr)时得把释放的内存还给tc吧那就得知道放到哪一个freelist上8字节16字节还是256KB。所以我们得ptr》页号》查基数树得到span》知道是哪一个freelist。同样也是在释放操作时tc见自己的freelist数据太多要退回一点给cc。这样导致cc中的span可能要合并。假设我现在有一个包含 3 个页的 Span页号是 100、101、102被还回来了。我怎么知道页号 99 和页号 103 的状态它们是空闲的吗就是查基数树表。申请内存的工作流是由tcccpc一个定长内存池组成的三层架构的多线程高并发内存池。申请内存工作流。ConcurrentAlloc(15)字节对齐到16字节。直接去 1 号_freeLists[1]里拿。如果桶里有直接结束。如果没有去cc进货。根据“慢开始”算法加spanlist锁找非空span找到结束。找不到去pc进货。pc诺没有使用mmap/VirtualAlloc找os进货。把进的page放入pc填写基数树。**进货要写借条。**写好基数树后面释放时才好用因为span都是在这一步从os调入堆区的释放内存的工作流Free(ptr)通过 ptr 算出页号通过基数树查页号对应的 Span。直接把内存槽挂回 ThreadCache 的 1 号桶。如果太满了这串多余的对象还给 CentralCache。快回收第一次是有一个就回收第二次是有两个如果这个span切出去的所有slot都回来了cc则拿回到pc中。查基数树看有没有能合并的span。pc中span合并。慢开始反馈借内存算法/快回收我写的不是内存池吗为什么threadcache要叫线程缓存缓存Cache的核心语义是为了弥补速度差异。我先从os堆中去一部分内存出来这个内存用作缓存给各个线程。有用过哪些智能指针并没有使用任何智能指针。因为智能指针的构造和析构都默认使用了new/delete。其次我们“徒手实现”智能指针的思想。Span结构体里面那个极其关键的变量_useCount它的作用和shared_ptr的引用计数一模一样当 CentralCache 把一个小块内存切出去给 ThreadCache 时Span-_useCount。当 ThreadCache 把小块内存还回来时Span-_useCount–。当 _useCount 0 时说明这个大块内存派发出去的所有碎片都回家了。此时触发回收机制把这个完整的 Span 向上交还给 PageCache。哪里使用了lambda函数性能测试的时候创建匿名线程。进行了单元测试测试案例一定长内存池断言查看。核心目的是验证我们的 ObjectPool 能不能正常分配和释放且从Freelist拿出来的内存地址正常不#includeiostream#includecassert// 引入你的定长内存池头文件structTreeNode{int_val;TreeNode*_left;TreeNode*_right;};voidTestObjectPool(){ObjectPoolTreeNodepool;// 1. 测试基础分配TreeNode*t1pool.New();TreeNode*t2pool.New();assert(t1!nullptr);assert(t2!nullptr);assert(t1!t2);// 地址绝对不能一样// 2. 测试回收机制pool.Delete(t1);// 3. 测试复用机制关键断言// t1 刚被 Delete挂入了空闲链表。此时再次 New// 按照机制应该直接把刚才的 t1 拿出来复用TreeNode*t3pool.New();assert(t3t1);// 如果不等于说明复用逻辑写错了std::coutObjectPool 单元测试通过std::endl;}测试案例二Span 合并测试性能测试使用Lambda表达式创建匿名线程用于测试。测试的原理是主线程创建很多子线程子线程对vector进行pushbackmalloc15然后再free。再将数据汇总到主线程。为了使这个项目跨平台做出的工作第一面对不同的OS我通过条件宏#ifdef _WIN32将 Windows 的 VirtualAlloc 和 Linux/macOS 的 mmap封装成了统一的 SystemAlloc 接口第二在硬件架构上。在freelist的指针上我抛弃了固定字节的强转采用(void*) 的二级指针解引用技巧使得代码能够自动兼容 32 位和 64 位系统架构避免了指针截断问题。模拟面试什么是内存池就是解释一下池化技术是什么没有内存池前程序**”零售“的找os申请内存。用malloc有了后”批发“**一次性申请一大堆放在内存池中当需要资源时直接从“池”中获取。内存池最底层用的什么函数还是malloc吗不是用的是virtualAlloc。直接申请一整页空间。内存池主要解决什么问题效率问题。以前是”零售“现在是批发不用一直切换内核态用户态。内部碎片。tc会用一个合适的槽来装数据。外部碎片。pagecache会合页。