一、简介
mimalloc 是微软研究院在 2019 年发表并开源的一个新的内存分配库:
代码量少,核心代码行数 <3500 行
tcmalloc ~20k LOC
jemalloc ~25k LOC
性能大大优于市面上其他 memory allocator
比 tcmalloc 快 7%
比 jemalloc 快 14%
三个局部存储的分片的空闲列表
增加数据访问局部性 (locality)
减少线程访问竞争
支持性能优化到极致的分配和释放的 fast path
引入一个时间节奏(temporal cadence),使分配器适时离开 fast path 来处理一些维护性的任务
二、内存分配和释放机制
mimalloc 的内存分配释放机制要点:
极致的空闲列表分片
分配和释放的主路径经过深度优化,其他的情况都被延迟到 generic 方法中进行。
没有使用锁,所有的多线程数据竞争都使用原子操作来解决。最坏情况下有上界,元数据约占 0.2%,实际分配空间浪费不超过 16.7%。
1. 空闲列表分片机制
空闲列表
为每个 mimalloc 页(一般是64KiB)维护一个空闲列表,而不是像传统机制那样为整个 size class(如 2^n)维护一个空闲列表。
只要当前页的空闲列表不为空,malloc 就可以一直分配本页中的内存,而不用管堆中是不是有其他新 free 的区域
这样可以提高新分配内存的空间局部性,使大部分新分配的地址都处在同一页中,提高 L2 Cache 的命中率
本地空闲列表
释放大对象时,可能会引起连锁反应递归地释放一大堆对象,造成最坏情况下的 free 时长不可控。因此需要限制 free 的个数,将后续要 free 的对象挂在一个列表上,等后续内存紧张时再释放(malloc_generic 中)。但需要一个机制保证 malloc_generic 每隔一段时间就能被调用到。
为每个 mimalloc 页除了维护一个空闲列表外,再增加一个本地空闲列表(local-free list),当空闲列表被分配完后,将调用 malloc_generic,malloc_generic 会将本地空闲列表转成新的空闲列表。
page->free = page->local_free; // move the listpage->local_free = NULL; // and the local list is empty again
因为空闲列表不会增长,经过一段时间总会被分配完,此机制相当于引入一个时间节奏,使 malloc_generic 的分配代码总是能够定期地被执行,此机制可以用来平摊一些耗时操作的开销:
延后执行的引用计数减导致的 free
维持一个稳定的心跳机制
回收线程释放列表中的内存
线程释放列表
在 mimalloc 中,mimalloc 页属于一个“线程局部”的堆,所有本线程的内存分配都从这个线程局部堆上分配,不需要锁。
但任何线程都能 free 本线程分配的内存。为保证本线程 free 本线程局部堆上的数据不需要锁,将其他线程释放的内存放到一个独立的”线程释放列表“中,也能减少 malloc 中 fast path 上的原子操作。
其他线程释放本线程分配的空间时,会调用 atomic_push 方法,将对应的地址原子地放到本线程的释放列表中:
void atomic_push( block_t** list, block_t* block ) { do { block->next = *list; } while (!atomic_compare_and_swap(list, block, block->next));}
各个线程释放内存时操作的对象被分片到各个 mimalloc 页上,减少了线程间的竞争
每隔一段时间线程释放列表会被原子地移动到空闲列表中,达到批量处理远程 free 的效果
tfree = atomic_swap( &page->thread_free, NULL );append( page->free, tfree );
2. 具体实现
malloc 实现
void* malloc_small( size_t n ) { // 0 < n <= 1024 heap_t* heap = tlb; //线程局部存储指针指向的线程局部堆 page_t* page = heap->pages_direct[(n+7)>>3]; // divide up by 8 block_t* block = page->free; //空闲列表 if (block==NULL) return malloc_generic(heap,n); // slow path page->free = block->next; //移动空闲列表指针 page->used++; return block;}
//slow path,mimalloc机制保证此方法会隔一段时间被调到void* malloc_generic( heap_t* heap, size_t size ) { deferred_free(); // 调用用户自定义的方法 // 遍历现有page foreach( page in heap->pages[size_class(size)] ) { page_collect(page); //回收页内空间 if (page->used - page->thread_freed == 0) { page_free(page); } else if (page->free != NULL) { return malloc(size); } } .... // 现有页中无空闲空间,重新分配一页,并从新页中分配空间}void page_collect(page) { page->free = page->local_free; // 将空闲列表设置为本地空闲列表 page->local_free = NULL; // 本地空闲列表置空 ... // 原子地处理 线程释放 列表}
free 的实现
void free( void* p ) { //找到p所在的segment segment_t* segment = (segment_t*)((uintptr_t)p & ~(4*MB)); if (segment==NULL) return; //找到p所在的page page_t* page = &segment->pages[(p - segment) >> segment->page_shift]; block_t* block = (block_t*)p; if (thread_id() == segment->thread_id) { // free的是本线程分配的内存(local free) block->next = page->local_free; page->local_free = block; page->used--; if (page->used - page->thread_freed == 0) page_free(page); } else { // free的是其他线程分配的内存(non-local free) atomic_push( &page->thread_free, block); atomic_incr( &page->thread_freed ); }}
参考资料
Mimalloc 论文:https://www.microsoft.com/en-us/research/publication/mimalloc-free-list-sharding-in-action/
Mimalloc 源码:https://github.com/microsoft/mimalloc