mimalloc剖析
mimalloc是微软最近开源的一个malloc实现,其实验数据表明相比于jemalloc、tcmalloc等实现大约快了10%。其通过将空闲块列表(Free List)进行分片(Sharding)来保证分配的内存有更好的空间的局部性,从而提升性能。在mimalloc中一共进行了4次Free List的Sharding。接下来我们会分别介绍这4个Free List的Sharding的位置以及其为什么要进行Free List的Sharding。
在Mimalloc页中进行的Free List的Sharding
在其他的内存分配器的实现中,一般会为每一类大小的对象保留一个Free List,随着内存的不断分配与释放,这个列表中的对象可能散布在整个地址空间中,因此内存分配的局部性较差。而在mimalloc中,其通过将内存分页,每个页负责一个特定大小的内存的分配,每个页有一个Free List,因此内存分配的空间局部性较好。
![](https://img2018.cnblogs.com/blog/1429336/201907/1429336-20190716111958825-1519488194.png)
其他内存分配器的Free List
![](https://img2018.cnblogs.com/blog/1429336/201907/1429336-20190716112021660-1016541611.png)
mimalloc的Free List
Local Free List
mimalloc希望能够限制内存分配与内存释放在最差情况下的时间消耗,如果上层应用需要释放一个非常大的结构体,其可能需要递归的释放一些子结构体,因而可能带来非常大的时间消耗。因而在koka与lean语言中,其运行时系统使用引用计数来追踪各种数据,然后保留一个延迟释放的队列(deferred decrement list),当每次释放的内存块超过一定数量后就将剩余需要释放的内存块加入该队列,在之后需要释放的时候再进行释放。那么下一个需要确定的问题是什么时候再去从该队列中释放内存。从延迟释放队列中继续进行释放的时机最好应当是内存分配器需要更多空间的时候,因此这需要内存分配器与上层应用的协作。
在mimalloc中,其提供一个回调函数,当进行了一定次数内存的分配与释放后会主动调用该回调函数来通知上层应用。mimalloc在实现时检测当前进行内存分配的页的Free List是否为空,如果为空则调用该回调,但是为了避免用于一直不断的分配与释放内存,导致Free List一直不为空,而导致回调函数一直得不到回调。因此mimalloc将Free List第二次进行Sharding,将其分为Free List与Local Free List。
当内存在进行分配时会从对应页的Free List中取得内存块,而释放时会将内存块加入Local Free List中,因而在进行一定次数的内存分配后,Free List必定为空,此时可以进行deferred free的回调函数的调用。
Thread Free List
在mimalloc中每个堆都是一个Thread Local的变量,而每次进行内存分配时,其均会从这个Thread Local的堆中进行内存的分配,而释放时即可能从该线程中释放也可能从其他线程中进行释放。如果进行内存释放的线程是该堆的拥有者,则其释放的内存会加入到对应页的Local Free List中,而由于还可能有其他的线程来释放这些内存,因此mimalloc第三次进行Free List的Sharding,将Local Free List分为Local Free List与Thread Free List。在进行内存的释放时,如果释放的线程为内存块对应堆的拥有着则将其加入Local Free List,否则利用CAS操作将其加入Thread Free List中。mimalloc通过这次分割来保证堆的所有者线程在自己的堆上进行内存的释放是无锁的,从而提升一些性能上的表现。
Full List
第四次的Free List的Sharding其实来自于mimalloc自身的实现,其内存分配的伪代码如下。由于在mimalloc中每个堆中都有一个数组pages,该数组中每个元素都是一个由相应大小的页组成的队列;同时还有一个pages_direct的数组,该数组中每个元素对应一个内存块的大小类型,每个元素均为指向负责对应大小内存块分配的页的指针。因此mimalloc在进行内存分配时会首先从该数组指向的页中尝试进行分配,如果分配失败则调用malloc_generic,在该函数中会遍历pages数组中对应大小的队列,此时如果对应的队列中有很多页均是满的,且队列很长那么每次分配的时候都会进行队列的遍历,导致性能的损失。
void* malloc_small( size_t n ) { heap_t* heap = tlb; page_t* page = heap->pages_direct[(n+7)>>3]; block_t* block = page->free; if (block==NULL) return malloc_generic(heap,n); page->free = block->next; page->used++; return block; }
因此mimalloc构建了一个Full List,将所有已经没有空闲空间的页放入该队列中,仅当该页中有一些空闲空间被释放后才会将其放回pages对应的队列中。而在由于内存的释放可能由对应堆的拥有者线程进行也可能由其他线程进行,因此需要一定的方式提醒对应的堆该页已经有空闲块了,同时为了避免使用锁导致的开销,mimalloc通过加入一个Thread Delayed Free List,如果一个页处于Full List中,那么在释放时会将内存块加入Thread Delayed Free List中,该队列会在调用malloc_generic时进行检测与清除(由于时Thread Local的堆,因此仅可能是拥有者来进行),因此此时仅需通过原子操作即可完成。那么还有一个问题是当释放内存的时候,其他线程如何知道是将内存块加入Thread Free List中还是Thread Delayed Free List中。mimalloc通过设置NORMAL、DELAYED、DELAYING三种状态来完成该操作。
总结
mimalloc通过将Free List进行分割,保证分配的内存具有较好的局部性并避免了锁的使用,从而获得了更好的性能。文章如果有哪里有问题,欢迎提出,对该项目感兴趣的可以去看一下其仓库1,或者参考这篇文章2。