这篇文章中我们会介绍一下mimalloc的实现,其中可能涉及上一篇文章提到的内容,如果不了解的可以先看下这篇mimalloc剖析。首先我们需要了解的是其整体结构,mimalloc的结构如下图所示

 

mimalloc整体结构
在mimalloc中,每个线程都有一个Thread Local的堆,每个线程在进行内存的分配时均从该线程对应的堆上进行分配。在一个堆中会有一个或多个segment,一个segment会对应一个或多个页,而内存的分配就是在这些页上进行。mimalloc将页分为三类:
  • small类型的segment的大小为4M,其负责分配大小小于MI_SMALL_SIZE_MAX的内存块,该segment中一个页的大小均为64KB,因此在一个segment中会包含多个页,每个页中会有多个块
  • large类型的segment的大小为4M,其负责分配大小处于MI_SMALL_SIZE_MAX与MI_LARGE_SIZE_MAX之间的内存块,该segment中仅会有一个页,该页占据该segment的剩余所有空间,该页中会有多个块
  • huge类型的segment,该类segment的负责分配大小大于MI_LARGE_SIZE_MAX的内存块,该类segment的大小取决于需要分配的内存的大小,该segment中也仅包含一个页,该页中仅会有一个块
根据heap的定义我们可以看到其有pages_free_direct数组、pages数组、Thread Delayed Free List以及一些元信息。其中pages_free_direct数组中每个元素对应一个内存块大小的类别,其内容为一个指针,指向一个负责分配对应大小内存块的页,mimalloc在分配比较小的内存时可以通过该数组直接找到对应的页,然后试图从该页上分配内存,从而提升效率。pages数组中每个元素为一个队列,该队列中所有的页大小均相同,这些页可能来自不同的segment,其中数组的最后一个元素(即pages[MI_BIN_FULL])就是前文提到的Full List,倒数第二个元素(即pages[MIN_BIN_HUGE])包含了所有的huge类型的页。thread_delayed_free就是前文提到的Thread Delayed Free List,用来让线程的拥有者能够将页面从Full List中移除。
 
struct mi_heap_s {
mi_tld_t* tld;
mi_page_t* pages_free_direct[MI_SMALL_WSIZE_MAX + ];
mi_page_queue_t pages[MI_BIN_FULL + ];
volatile mi_block_t* thread_delayed_free;
uintptr_t thread_id;
uintptr_t cookie;
uintptr_t random;
size_t page_count;
bool no_reclaim;
};
在heap的定义中我们需要特别注意的一个成员是tld(即Thread Local Data)。其成员包括指向对应堆的heap_backing,以及用于segment分配的segment tld以及os tld。
struct mi_tld_s {
unsigned long long heartbeat;
mi_heap_t* heap_backing;
mi_segments_tld_t segments;
mi_os_tld_t os;
mi_stats_t stats;
}; typedef struct mi_segments_tld_s {
// 该队列中所有的segment均有空闲页,由于large与huge类型的segment仅有一个页,因此该队列中所有segment均为small类型
mi_segment_queue_t small_free;
size_t current_size;
size_t peak_size;
size_t cache_count;
size_t cache_size;
// segment的缓存
mi_segment_queue_t cache;
mi_stats_t* stats;
} mi_segments_tld_t; typedef struct mi_os_tld_s {
uintptr_t mmap_next_probable;
void* mmap_previous;
uint8_t* pool;
size_t pool_available;
mi_stats_t* stats;
} mi_os_tld_t;

mi_malloc

首先要说明一下,所有贴出的源代码都可能会有一定程度的删减,例如一些平台相关的代码,一些用于信息统计的代码都可能被删去。接下来我们跟着mi_malloc来看一下内存分配的流程,其流程仅有两部,获取该线程拥有的堆,然后从这个堆上分配一块内存。
extern inline void* mi_malloc(size_t size) mi_attr_noexcept {
return mi_heap_malloc(mi_get_default_heap(), size);
}

获取线程拥有的堆

首先介绍一下mimalloc有哪些堆,mimalloc会为每个线程保留一个Thread Local的堆,每个线程均使用该堆进行内存分配,除此之外还有一个全局变量_mi_heap_main,该堆会被主线程视为Thread Local的堆,由于某些OS会用malloc来进行Thread Local的内存分配,因此_mi_heap_main在mimalloc尚未初始化时也会被视作默认的堆来进行内存分配。
我们先来看一下mi_get_default_heap,该函数会直接返回一个Thread Local的_mi_heap_default,但是该Thread Local默认是被初始化为_mi_heap_empty,之后在调用mi_heap_malloc时如果发现该Thread Local并未初始化则会将其初始化为一个新的堆。
static inline mi_heap_t* mi_get_default_heap(void) {
#ifdef MI_TLS_RECURSE_GUARD
if (!_mi_process_is_initialized) return &_mi_heap_main;
#endif
return _mi_heap_default;
}

从堆上分配内存

由于mimalloc的堆维护了pages_free_direct数组,可以直接通过该数组来找到所有针对对应大小的small类型的页,因此我们可以看到当需要分配的内存块大小小于等于MI_SMALL_SIZE_MAX会调用mi_heap_malloc_small从堆上进行内存的分配,否则调用_mi_malloc_generic从堆上分配内存。当然由于pages_free_direct中指向的页可能Free List已经为空了,那么其最终还是会调用_mi_malloc_generic来进行新的内存的分配。
extern inline void* mi_heap_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept {
void* p;
if (mi_likely(size <= MI_SMALL_SIZE_MAX)) {
p = mi_heap_malloc_small(heap, size);
}
else {
p = _mi_malloc_generic(heap, size);
}
return p;
}
先贴一张从堆上分配内存的总体流程图,接下来我们仔细介绍一下这两个函数具体的调用。
mi_heap_malloc流程图

分配Small类型的内存块

我们先来看一下mi_heap_malloc_small,其首先从堆的pages_free_direct数组中找到负责分配对应大小内存块的页,之后调用_mi_page_malloc从该页的Free List中分配一块内存,如果该页的Free List为空则调用_mi_malloc_generic来进行内存的分配。
extern inline void* mi_heap_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept {
mi_page_t* page = _mi_heap_get_free_small_page(heap,size);
return _mi_page_malloc(heap, page, size);
} extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size) mi_attr_noexcept {
mi_block_t* block = page->free;
if (mi_unlikely(block == NULL)) {
return _mi_malloc_generic(heap, size);
}
page->free = mi_block_next(page,block);
page->used++; ... return block;
}

分配Large或者Huge类型的内存块

接下来我们看一下_mi_malloc_generic,该函数调用的原因可能有如下两种:
  • 需要分配small类型的内存块,但是由pages_free_direct获得的页的Free List已经为空
  • 需要分配large或者huge类型的内存块
我们可以看到_mi_malloc_generic的流程可以归纳为:
  • 如果需要的话进行全局数据/线程相关的数据/堆的初始化
  • 调用回调函数(即实现前文所说的deferred free)
  • 找到或分配新的页
  • 从页中分配内存
void* _mi_malloc_generic(mi_heap_t* heap, size_t size) mi_attr_noexcept
{
if (mi_unlikely(!mi_heap_is_initialized(heap))) {
mi_thread_init();
heap = mi_get_default_heap();
} _mi_deferred_free(heap, false); mi_page_t* page;
if (mi_unlikely(size > MI_LARGE_SIZE_MAX)) {
if (mi_unlikely(size >= (SIZE_MAX - MI_MAX_ALIGN_SIZE))) {
page = NULL;
}
else {
page = mi_huge_page_alloc(heap,size);
}
}
else {
page = mi_find_free_page(heap,size);
}
if (page == NULL) return NULL; return _mi_page_malloc(heap, page, size);
}

初始化

前面我们提到过每个线程都有一个Thread Local的堆,该堆默认被设为_mi_heap_empty。如果调用_mi_malloc_generic时发现该线程的堆为_mi_heap_empty则进行初始化。mi_thread_init会首先调用mi_process_init来进行进程相关数据的初始化,之后初始化Thread Local的堆。
void mi_thread_init(void) mi_attr_noexcept
{
// ensure our process has started already
mi_process_init(); // initialize the thread local default heap
if (_mi_heap_init()) return; // returns true if already initialized ... #endif
}
我们可以看到mi_process_init仅会被调用一次,其初始化了_mi_heap_main,其会被设为主线程的Thread Local的堆。其注册了mi_process_done为线程结束的回调函数,并调用mi_process_setup_auto_thread_done来设置mi_thread_done为线程结束时的回调函数,而_mi_os_init则是用来设置一些与OS有关的常量,例如页面大小等。
void mi_process_init(void) mi_attr_noexcept {
// ensure we are called once
if (_mi_process_is_initialized) return;
// access _mi_heap_default before setting _mi_process_is_initialized to ensure
// that the TLS slot is allocated without getting into recursion on macOS
// when using dynamic linking with interpose.
mi_heap_t* h = _mi_heap_default;
_mi_process_is_initialized = true; _mi_heap_main.thread_id = _mi_thread_id();
uintptr_t random = _mi_random_init(_mi_heap_main.thread_id) ^ (uintptr_t)h;
#ifndef __APPLE__
_mi_heap_main.cookie = (uintptr_t)&_mi_heap_main ^ random;
#endif
_mi_heap_main.random = _mi_random_shuffle(random); atexit(&mi_process_done);
mi_process_setup_auto_thread_done();
mi_stats_reset();
_mi_os_init();
}
我们来看一下mi_process_done与mi_thread_done分别做了什么。
我们可以看到mi_process_done主要是调用了mi_collect来回收已经分配的内存,该函数调用的也是mi_heap_collect_ex,不过由于其调用的参数不同,行为会稍有不同,在此处的调用会收集abandon segment,然后释放这些segment。
static void mi_process_done(void) {
// only shutdown if we were initialized
if (!_mi_process_is_initialized) return;
// ensure we are called once
static bool process_done = false;
if (process_done) return;
process_done = true; #ifndef NDEBUG
mi_collect(true);
#endif
}
mi_thread_done则主要是调用_mi_heap_done来回收部分资源。该函数会先把_mi_heap_default重新设为默认值,如果是主线程就设为_mi_heap_main,否则设为_mi_heap_empty。如果该线程不是主线程的话则调用_mi_heap_collect_abandon来回收内存并释放动态分配的heap,如果是主线程的话会调用_mi_heap_destroy_pages来回收页。
static bool _mi_heap_done(void) {
mi_heap_t* heap = _mi_heap_default;
if (!mi_heap_is_initialized(heap)) return true; // reset default heap
_mi_heap_default = (_mi_is_main_thread() ? &_mi_heap_main : (mi_heap_t*)&_mi_heap_empty); // todo: delete all non-backing heaps? // switch to backing heap and free it
heap = heap->tld->heap_backing;
if (!mi_heap_is_initialized(heap)) return false; // collect if not the main thread
if (heap != &_mi_heap_main) {
_mi_heap_collect_abandon(heap);
} // merge stats
_mi_stats_done(&heap->tld->stats); // free if not the main thread
if (heap != &_mi_heap_main) {
_mi_os_free(heap, sizeof(mi_thread_data_t), &_mi_stats_main);
}
#if (MI_DEBUG > 0)
else {
_mi_heap_destroy_pages(heap);
}
#endif
return false;
}
在mimalloc中,如果一个线程结束了,那么其对应的Thread Local的堆就可以释放了,但是在该堆中还可能存在有一些内存块正在被使用,且此时会将对应的segment设置为ABANDON,之后由其他线程来获取该segment,之后利用该segment进行对应的内存分配与释放(mimalloc也有一个no_reclaim的选项,设置了该选项的堆不会主动获取其他线程ABANDON的segment)。
接下来我们来看一下_mi_heap_collect_abandon,其实际调用了mi_heap_collect_ex,下面的代码中略去了部分不会被_mi_heap_done使用到的分支。该函数的流程如下:
  • 调用deferred free回调函数
  • 标记当前堆的Full List中的所有页面为Normal,从而让其在释放时加入Thread Free List,因为该segment之后可能会被其他线程接收
  • 释放该堆的Thread Delayed Free List中的内存块(不是每页一个的Thread Free List)
  • 遍历该堆所拥有的所有页,对每个页调用一次mi_heap_page_collect
  • 调用_mi_page_free_collect将页中的Local Free List以及Thread Free List追加到Free List之后
  • 如果该页没有正在使用的块则调用_mi_page_free将该页释放回对应的segment中,如果segment中所有的空闲页均被释放则可能直接释放对应的segment回OS或加入堆的缓存中
  • 如果该页尚有正在使用的块则将该页标记为abandon,当某个segment中所有的页均被标记为abandon后会将对应的segment加入全局的abandon segment list中(堆中并未保留有哪些segment的信息,因此需要遍历所有页来完成这一操作)
  • 释放堆中所有缓存的segment
static void mi_heap_collect_ex(mi_heap_t* heap, mi_collect_t collect)
{
_mi_deferred_free(heap,collect > NORMAL);
if (!mi_heap_is_initialized(heap)) return; // 一些接收abandon list中的segment的代码
... // if abandoning, mark all full pages to no longer add to delayed_free
if (collect == ABANDON) {
for (mi_page_t* page = heap->pages[MI_BIN_FULL].first; page != NULL; page = page->next) {
_mi_page_use_delayed_free(page, false); // set thread_free.delayed to MI_NO_DELAYED_FREE
}
} // free thread delayed blocks.
// (if abandoning, after this there are no more local references into the pages.)
_mi_heap_delayed_free(heap); // collect all pages owned by this thread
mi_heap_visit_pages(heap, &mi_heap_page_collect, &collect, NULL);
mi_assert_internal( collect != ABANDON || heap->thread_delayed_free == NULL ); // collect segment caches
if (collect >= FORCE) {
_mi_segment_thread_collect(&heap->tld->segments);
}
}

Huge类型页面的分配

由于huge类型的页面对应的segment中仅有一个页,且该页仅能分配一个块,因此其会重新分配一个segment,从中建立新的页面。mi_huge_page_alloc会调用mi_page_fresh_alloc分配一个页面,然后将其插入堆对应的BIN中(即heap->pages[MI_BIN_HUGE])。由下图可以看到Small与Large类型页面分配时所调用的mi_find_free_page也会调用该函数来进行页面的分配,接下来我们就介绍一下mi_page_fresh_alloc。
_mi_malloc_generic函数调用关系
我们可以看到mi_page_fresh_alloc主要做了三件事,先从堆中分配一个新的页,并对该页进行初始化,最后将该页加入对应的BIN中。其中mi_segment_page_alloc就是从堆中找到一个足够容纳新页的segment并分配一个新的页,其会根据需要分配的内存块的大小调用mi_segment_small_page_alloc/mi_segment_large_page_alloc/mi_segment_huge_page_alloc。
static mi_page_t* mi_page_fresh_alloc(mi_heap_t* heap, mi_page_queue_t* pq, size_t block_size) {
mi_page_t* page = _mi_segment_page_alloc(block_size, &heap->tld->segments, &heap->tld->os);
if (page == NULL) return NULL;
mi_page_init(heap, page, block_size, &heap->tld->stats);
mi_page_queue_push(heap, pq, page);
return page;
}
mi_huge_page_alloc/mi_large_page_alloc
mi_huge_page_alloc与mi_large_page_alloc非常类似,因为这两种类型内存块对应的segment都仅有一个页,稍有区别的是large类型的segment的大小为4M,而huge类型的segment大小取决于需要的内存块的大小。因为这两种类型的块的分配必须获取新的segment,因此其均调用mi_segment_alloc获取一个新的segment,然后在新获取的segment中建立一个新的页并标记该页为正在使用。
接下来介绍一下其中用于分配新segment的函数mi_segment_alloc的流程:
  • 计算segment的大小,页的大小
  • 从cache中试图找到一个足够大的segment,如果segment中有较多未使用的空间则会将部分空间释放回OS
  • 设置segment的元信息
mi_segment_small_page_alloc
Small类型的页的分配稍微有些不同,因为large与huge类型的内存块其对应的segment中均只有一个页,而small类型的segment中每个页均有多个页,因此mimalloc在堆中保存了一个segment small free list,该队列中所有的segment均为small类型且均有空闲的页。mi_segment_small_page_alloc会首先从该列表中试图找到一个有空闲页的segment,然后从该segment中分配一页,如果分配完成后该segment中已经没有空闲页了则将其移出该列表,如果没有找到则会调用mi_segment_alloc新分配一个segment并将其加入该列表中。

Small/Large类型页面的分配

由于Small与Large类型的页面中均可以包含多个块,因此分配这两种类型的内存块时需要查找已有的页面,查看其中是否有页中有尚未分配的内存块。因此其会首先找到对应的BIN,遍历其中的所有页面,试图扩展Free List(包括利用尚未使用的空间、合并Local Free List与Thread Free List)从而找到一个有足够空间的页。由于在该过程中会进行Free List的合并,因此其还会释放一些完全空闲的页,进而可能导致segment的释放。如果在遍历完BIN后仍旧没有找到空闲页则会mi_page_fresh来分配一个新的页,在该过程中会调用_mi_segment_try_reclaim_abandoned来试图获取一个abandon的segment,但是要注意的是重新获取一个segment并不一定会带来新的页,因为可能接收的segment为large或huge类型或者其已经没有空闲页了,在这种情况下会去调用mi_page_fresh_alloc去获取新的segment和页或者从已有的segment中分配新的页。

从页中分配内存块

此时我们终于获得了一个空闲页,我们可以从该页中分配一个内存块了,其代码如下。我们可以看到其首先检查了一下当前页的Free List是否为空,如果为空则调用_mi_malloc_generic,这是因为该函数的调用入口有两种,第一种是分配small类型的内存块时调用的mi_heap_malloc_small,第二种才是_mi_malloc_generic。
这里需要介绍一下mimalloc更新pages_free_direct的机制,mimalloc通过在将一个页向BIN中添加或者移除页时更新对应的pages free direct数组,由于对齐的问题,因此一个页面的分配可能需要改变多个pages_free_direct的指向。
extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size) mi_attr_noexcept {
mi_block_t* block = page->free;
if (mi_unlikely(block == NULL)) {
return _mi_malloc_generic(heap, size); // slow path
}
mi_assert_internal(block != NULL && _mi_ptr_page(block) == page);
// pop from the free list
page->free = mi_block_next(page,block);
page->used++; ... return block;
}

总结

以上就是mimalloc中用于内存分配部分的代码的解析了,其中还有很多没有讲到的地方,例如其向OS请求内存部分的代码等等。文章如果有哪里有问题,欢迎提出,对该项目感兴趣的可以去看一下其仓库1,或者参考这篇文章2

引用

mimalloc内存分配代码分析的更多相关文章

  1. map的内存分配机制分析

    该程序演示了map在形成的时候对内存的操作和分配. 因为自己对平衡二叉树的创建细节理解不够,还不太明白程序所显示的日志.等我明白了,再来修改这个文档. /* 功能说明: map的内存分配机制分析. 代 ...

  2. list的内存分配机制分析

    该程序演示了list在内存分配时候的问题.里面的备注信息是我的想法. /* 功能说明: list的内存分配机制分析. 代码说明: list所管理的内存地址可以是不连续的.程序在不断的push_back ...

  3. vector的内存分配机制分析

    该程序初步演示了我对vector在分配内存的时候的理解.可能有误差,随着理解的改变,改代码可以被修改. /* 功能说明: vector的内存分配机制分析. 代码说明: vector所管理的内存地址是连 ...

  4. S5PV210的内存分配研究分析

    S5PV210内存一般会使用SDRAM和DDR2 (DDR SDRAM),SDRAM的uboot启动网络已经有很多资料的,对于DDR2还有有很多疑惑,如果有错误的地方,请大家一定指出,醍醐灌顶,不胜感 ...

  5. java 字符串内存分配的分析与总结

    经常在网上各大版块都能看到对于java字符串运行时内存分配的探讨,形如:String a = "123",String b = new String("123" ...

  6. java内存分配和String类型的深度解析

    [尊重原创文章出自:http://my.oschina.net/xiaohui249/blog/170013] 摘要 从整体上介绍java内存的概念.构成以及分配机制,在此基础上深度解析java中的S ...

  7. 【转】java内存分配和String类型的深度解析

    一.引题 在java语言的所有数据类型中,String类型是比较特殊的一种类型,同时也是面试的时候经常被问到的一个知识点,本文结合java内存分配深度分析关于String的许多令人迷惑的问题.下面是本 ...

  8. 详解Go中内存分配

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 本文使用的go的源码15.7 介绍 Go 语言的内存分配器就借鉴了 TCMalloc 的 ...

  9. Netty源码分析第5章(ByteBuf)---->第7节: page级别的内存分配

    Netty源码分析第五章: ByteBuf 第六节: page级别的内存分配 前面小节我们剖析过命中缓存的内存分配逻辑, 前提是如果缓存中有数据, 那么缓存中没有数据, netty是如何开辟一块内存进 ...

随机推荐

  1. AngularJS的简单使用(入门级)

    AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀的前端JS框架,已经被用于Google的多款产品当中. AngularJS有着诸多特性,最为 ...

  2. “流”的5个例子(TStream是抽象类,写到文件里和内存里,都是一样的)

    http://www.cnblogs.com/keyvip/category/270215.html

  3. 分布式文件系统之MooseFS----介绍

    前一段公司因为nfs存储的问题,做了一段时间的调研.最终虽然有nfs高可用方案(nfs+drbd+heartbeat),但是在nfs故障切换的时候会出现 2 分钟左右的延时.这样子...就开始了对分布 ...

  4. How Qt Signals and Slots Work(感觉是通过Meta根据名字来调用)

    Qt is well known for its signals and slots mechanism. But how does it work? In this blog post, we wi ...

  5. SQLite实现内存键值存储

    SQLite数据文件往Linux内存文件系统/dev/shm/data.sqlite3一放,就是内存级读写性能的SQL系统.用SQLite实现内存键值存储:CREATE TABLE IF NOT EX ...

  6. js中prototype与__proto__区别

    proto(隐式原型)与prototype(显式原型) 显式原型 explicit prototype property:每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数 ...

  7. PHP中关于时间、时区、本地时间、UTC时间、GMT时间、时间戳等知识的梳理

    在PHP开发中,我们经常会在时间问题上被搞糊涂,比如我们希望显示一个北京时间,但是当我们使用date函数进行输出时,却发现少了8个小时.几乎所有的php猿类都必须对php中几个重要的时间转换等方法进行 ...

  8. postman --- 如何在用户登陆和CSRF验证的场景下使用

    一.前提 安装postman和Postman Interceptor postman应用放到桌面: 二.用户登陆 这种场景很简单,只要开启Interceptor,然后先请求登陆地址,再继续请求其他地址 ...

  9. 15 BOM的介绍

    avaScript基础分为三个部分: ECMAScript:JavaScript的语法标准.包括变量.表达式.运算符.函数.if语句.for语句等. DOM:文档对象模型,操作网页上的元素的API.比 ...

  10. 【面试】MySQL 中NULL和空值的区别?

    做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 01 小木的故事 作为后台开发,在日常工作中如果要接触Mysql数据库,那么不可避免会遇到Mysql中的NULL和空值.那 ...