最近实现一个多线程内存池,所以看了很多开源的代码,其中微软的mimalloc无论从性能和代码量上有表现得很好,其实验数据表明相比于jemalloc、tcmalloc等实现大约快了10%,所以这里对其使用做个简单的介绍,具体原理可以研读论文和参阅其他专家的博客。
mimalloc的开源库地址:https://github.com/microsoft/mimalloc
虚拟内存是和进程相关的,进程中的所有线程共享同一个虚拟内存空间,因此一个线程想要请求一块内存,必须考虑数据竞争的情况,即防止同时有另一个线程也在请求这一块内存。一个保守的做法是在请求内存时加锁,这也是很多旧的内存管理器的做法,但加锁太慢了,无法发挥多核的优势。
现代的内存分配器都尽量不用加锁的方式,取而代之的是将内存和线程关联起来,每个线程管理着自己的内存池,分配时只从线程自己的内存池分配,这样就避免了锁的使用;释放时分两种情况,一种是本线程的释放,这和分配一样不需要加锁;另一种是内存块被转移到另一个线程去,并由那个线程执行释放,这种情况就需要一些线程同步的处理,等一会我们会介绍 mimalloc 怎么做。
mimalloc 和其他分配器类似,每个线程都有一块线程本地数据(tld),专门用来管理线程相关的内存。tld主要管理着两个东西:
segment:segment 有点类似于其他分配器的 slab 或 arena 的概念,它是从OS申请出来的比较大的内存块,所有程序分配的内存块都自出segment;tld就维护着segment的列表。
heap:是对内存堆的抽象,也可认为它是和线程相关的内存池,它管理着当前线程所有可分配的内存页(page);
page是比segment更细粒度的单位,实际上page就是从segment切分而来的,一个page是一个固定尺寸的内存块(block)集合。
相关原理可以查阅论文:https://www.microsoft.com/en-us/research/publication/mimalloc-free-list-sharding-in-action/
这里来简单介绍下使用方式:
一.作为库使用
mimalloc使用cmake作为构建工具, 执行以下命令即可编译.
成功编译后在构建目录下会生成所需(libmimalloc)的动态库/静态库以及测试工具.
mkdir -p [dir to build]
cd [dir to build]
cmake [source path]
make
如果是使用编译生成的库,可以在源文件中包含mimalloc.h并增加-lmimalloc选项来使用。
可以通过在源文件中包含mimalloc.h并增加-lmimalloc选项来使用mimalloc.
mimalloc可以与其它allocator共存, 对于cmake构建可以使用以下命令设置:
target_link_libraries([program] PUBLIC mimalloc)
如果不希望重新编译也可以通过设置环境变量来替换:
env LD_PRELOAD=[path/to/libmimalloc.so] [program]
如果需要打印libmalloc的调试信息, 可以设置环境变量(注意后者需要debug版本):
env MIMALLOC_VERBOSE=1 [program]
env MIMALLOC_SHOW_STATS=1 [program]
其它选项:
打印错误/告警信息: MIMALLOC_SHOW_ERRORS=1
使用huge page特性: MIMALLOC_LARGE_OS_PAGES=1
如下是一个示例:
**1.先来看下源码**
[23:18:20] hansy@hansy:~$ cat 1.c
#include <sys/time.h>
int main() {
struct timeval last;
struct timeval next;
gettimeofday(&last, 0);
for (int i = 0; i < 10000000; ++i) {
int *p = malloc(i * sizeof(int));
free(p);
}
gettimeofday(&next, 0);
printf("%llu.%06llu\n",
(next.tv_usec > last.tv_usec ? next.tv_sec - last.tv_sec : next.tv_sec - 1 - last.tv_sec),
(next.tv_usec > last.tv_usec ? next.tv_usec - last.tv_usec : 1000000 + next.tv_usec - last.tv_usec));
return 0;
}
**2.运行源码**
[23:18:25] hansy@hansy:~$ gcc 1.c -w && ./a.out
8.606776
**3.设置环境变量进行调试**
[23:18:42] hansy@hansy:~$ env LD_PRELOAD=./mimalloc/build_release/libmimalloc.so ./a.out
1.314598
**4.打印调试信息**
[23:18:57] hansy@hansy:~$ env MIMALLOC_VERBOSE=1 LD_PRELOAD=./mimalloc/build_release/libmimalloc.so ./a.out
mimalloc: process init: 0x7f4b0d558740
mimalloc: option 'large_os_pages': 0
mimalloc: option 'secure': 0
mimalloc: option 'page_reset': 0
mimalloc: option 'cache_reset': 0
1.323777
mimalloc: option 'show_stats': 0
heap stats: peak total freed unit count
elapsed: 1.324 s
process: user: 1.299 s, system: 0.012 s, faults: 0, reclaims: 479, rss: 2.6 mb
mimalloc: process done: 0x7f4b0d558740
二.作为源代码直接编译使用
如果仅仅使用源码可以编译static.c后进行链接,在源码中包含mimalloc.h
如下是一个简单Makefile文件
CC=gcc
CFLAGS=-I ../include
DEPS = mimalloc.h
SRCS = main.c
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
all: static.o
$(CC) $(SRCS) $(CFLAGS) -o main -lpthread
.PHONY: clean
clean:
rm -f ./*.o
rm -f main
关于API的使用这里做个简单的介绍,mimalloc原生API
mi_malloc()
mi_free()
mi_calloc()
mi_realloc()
......
其他API可以参与mimalloc-doc.h文件
这些API是可以替代malloc使用的,已经实现了线程安全。
三.这里对mi_stats_print的打印信息做一个简单的介绍
heap stats: peak total freed current unit count
//下面是分配的次数和每次分配的大小,最后显示ok则表示已经进行释放
normal 25: 1.2 KiB 1.2 KiB 1.2 KiB 0 1.2 KiB 1 ok
normal 28: 2.0 KiB 2.0 KiB 2.0 KiB 0 2.0 KiB 1 ok
normal 33: 5.0 KiB 5.0 KiB 5.0 KiB 0 5.0 KiB 1 ok
//下面是对上述分配自元进行汇总
heap stats: peak total freed current unit count
normal: 8.2 Ki 8.2 Ki 8.2 Ki 0 2.7 KiB 3 ok
large: 0 0 0 0 ok
huge: 0 0 0 0 ok
total: 8.2 KiB 8.2 KiB 8.2 KiB 0 ok
malloc req: 6.9 KiB 6.9 KiB 6.9 KiB 0 ok
reserved: 128.0 MiB 128.0 MiB 0 128.0 MiB not all freed!
committed: 66.0 MiB 66.1 MiB 128.5 KiB 66.0 MiB not all freed!
reset: 0 0 0 0 ok
touched: 269.8 KiB 269.8 KiB 132.5 KiB 137.2 KiB not all freed!
segments: 3 3 2 1 not all freed!
-abandoned: 1 1 1 0 ok
-cached: 0 0 0 0 ok
pages: 3 3 1 2 not all freed!
-abandoned: 1 1 1 0 ok
-extended: 3
-noretire: 1
mmaps: 3
commits: 2
threads: 1 2 4 -2 ok
searches: 0.0 avg
numa nodes: 1
elapsed: 2.001 s
process: user: 0.001 s, system: 0.000 s, faults: 0, rss: 2.5 MiB, commit: 66.0 MiB
四.接下来对mimalloc-override.h文件做个介绍
改文件用于重写原生API以适应不同平台
下面是一个重写重写和重载的案例
//这里实现对MemPoolMalloc进行重载,判断其传递的参数个数
#define _VA_ARG_NUM(A0,A1,N,...) N
#define VA_ARG_NUM(...) _VA_ARG_NUM(-1,##__VA_ARGS__,1,0)
#define VA_ARG1and2(A0,A1,...) A1
#define MemPoolMalloc(...) ((VA_ARG_NUM(__VA_ARGS__)==0)?mi_malloc(DefaultSize):\
(VA_ARG_NUM(__VA_ARGS__)==1)?mi_malloc((VA_ARG1and2(0,##__VA_ARGS__,0))):(9))
#define MemPoolCalloc(n,c) mi_calloc(n,c)
#define MemPoolRealloc(p,n) mi_realloc(p,n)
#define MemPoolFree(p) mi_free(p)
#define MemPoolInfo(p) mi_stats_print(p)
#define MemHeapNew() mi_heap_new()
#define MemHeapMalloc(p,c) mi_heap_malloc(p,c)
#define MemHeapDestory(p) mi_heap_destroy(p)