如何在 Rust 中创建自定义内存分配器

这是一篇关于 Rust 语言中内存分配器的博客文章,主要内容如下:

  • 背景:Rust 是一种流行的低级语言,以其速度和内存安全性而受到称赞,但这两者都高度依赖于所使用的内存分配器。通常 Rust 使用系统分配器,其性能并非最佳,但自 2018 年以来,程序员可以设置自定义全局分配器。
  • 设置自定义分配器:在 Rust 中设置自定义分配器非常简单,只需实现GlobalAlloc trait 并添加#[global_allocator]属性,如use rsbmalloc::RSBMalloc; #[global_allocator] static ALLOCATOR: RSBMalloc = RSBMalloc::new();。可以在crates.io上查看#allocator标签来选择好的全局分配器,如snmalloc-rsjemallocmimallocrsbmalloc等。
  • Rust 中自定义分配器的工作原理:如果现有的分配器不满足需求,可以编写自己的分配器。计算机内存以字节数组形式组织,分配器将字节数组分割成可用于各种变量的部分。在用户空间程序中,程序具有虚拟地址空间,指针需要通过页表映射到实际物理位置,否则会出现段错误。可以使用mmap函数创建页,使用munmap函数释放内存。在 Rust 中,分配器需要实现GlobalAlloc trait,其中包含allocdealloc等函数。
  • 编写页分配器:最简单的分配器分配整个页,首先需要找到页大小,如在 macOS 上页大小是vm_page_size,在 Linux 或 Windows 上需要调用函数并使用lazy_static缓存。然后可以在GlobalAlloc impl 块中编写alloc函数,通过mmap请求内存,并设置相应的标志。dealloc函数用于释放内存,通过munmapVirtualFree函数实现。
  • 编写分箱分配器:为了提高分配速度,可以使用分箱分配器,将整个页分配给特定大小的分配。分箱分配器中的bin是包含多个相同大小slot的内存区域,slot是一个联合类型,可以是包含程序数据的缓冲区,也可以是指向下一个空闲slot的指针或空指针。rsbmalloc使用 15 个大小翻倍的 bin,在alloc函数中根据请求的大小匹配相应的 bin 并进行分配,在dealloc函数中将指针转换为Slot指针并设置为下一个空闲slot。这种分配器的缺点是小于 64KiB 的分配不会释放回操作系统,单线程效率低,可能存在空间浪费等问题。
  • 编写带有线程缓存的多线程分配器:理论上前面编写的分配器是多线程的,但由于只有一个静态的 bin 列表,并且每个分配都需要使用互斥锁,导致性能下降。可以使用线程缓存的方法,为每个线程分配一组 bin,避免锁竞争。rsbmalloc通过预创建线程缓存并使用线程 ID 映射到线程缓存来实现多线程分配器,在allocdeallocrealloc函数中添加获取线程缓存的逻辑,可以显著提高多线程性能。
  • 在 Rust 中调试分配器:调试分配器时,println!()panic!()宏可能会因为调用堆分配函数而失效,需要使用lldbgdb等调试器来查看堆栈跟踪和变量,帮助找到问题。在开发分配器时,会看到很多信号,如SIGABRTSIGSEGV等,需要调试器来分析问题。

总之,自定义内存分配器可以根据特定需求优化性能,也可以使用像snmalloc这样的更快的分配器来提高程序的分配速度,同时不放弃 Rust 的内存安全保证。

阅读 7
0 条评论