mold 项目中的 oneTBB 可扩展内存分配器(tbbmalloc):scalable_allocator、cache_aligned_allocator 与 malloc 自动替换实践
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本篇技术指南以 mold 仓库内 vendored 的 oneTBB 官方文档 Scalable_Memory_Allocator.rst 为核心骨架,系统讲解 oneTBB 可扩展内存分配器(tbbmalloc)的库结构、分配器模板、C 接口、运行期配置与 malloc 自动替换方案。读完本文,你将掌握 tbbmalloc 在 mold 项目中的实际定位、scalable_allocator<T>与cache_aligned_allocator<T>的选型依据、scalable_allocation_mode/scalable_allocation_command的完整参数语义,以及如何在 Linux/Windows 上通过 proxy 库无缝接管 C/C++ 动态内存分配。
背景:mold 与 oneTBB 可扩展内存分配器的关系
mold 是一个高性能现代链接器,其并行实现大量依赖 oneTBB(oneAPI Threading Building Blocks)。从源码可见,mold 在 arch-arm32.cc、arch-x86-64.cc、gc-sections.cc 等核心文件中直接使用tbb::parallel_for、tbb::parallel_for_each、tbb::concurrent_unordered_map、tbb::concurrent_vector,并在 cmdline.cc 中使用tbb::global_control控制线程数。oneTBB 被整体 vendored 在仓库的 third-party/tbb 目录下,其官方文档、头文件、源码与测试一并保留,其中就包括可扩展内存分配器(scalable memory allocator)的完整文档体系。
oneTBB 的 release 与 debug 版本均由两个动态共享库构成:一个是提供通用并行支持(并行算法、容器、任务调度)的库,另一个就是可扩展内存分配器库,后者以库名中的malloc字样区分。以 Windows 为例,release 版对应为tbb<version>.dll与tbbmalloc.dll。应用程序可以选择只链接通用库、只链接可扩展内存分配器,或者两者都使用(详见 Scalable_Memory_Allocator.rst)。
需要说明的是:mold 本体在并行化中主要使用 oneTBB 的通用库组件(并行算法与并发容器),而本文重点阐述的 tbbmalloc 是同一发行版中可独立选用的内存分配组件——mold 将其随 oneTBB 一并 vendored,作为完整的第三方依赖树的一部分。
两个分配器模板:解决并行编程的两大核心问题
oneTBB 提供多个类似 STLstd::allocator的分配器模板,其中scalable_allocator<T>与cache_aligned_allocator<T>分别针对并行编程中两个截然不同的问题(详见 Memory_Allocation.rst)。
可扩展性:scalable_allocator
问题:为串行程序设计的分配器在并发场景下会出现扩展性问题——多线程竞争同一个共享内存池,同一时刻只允许一个线程执行分配,形成串行化瓶颈。
方案:使用scalable_allocator<T>模板规避扩展性瓶颈。该模板能显著提升频繁分配/释放内存的程序的性能,其内部为每个线程维护独立的缓存与缓冲,减少全局锁竞争。
伪共享:cache_aligned_allocator
问题:当两个线程访问共享同一条缓存行(cache line)中的不同字时,会发生"伪共享"(false sharing)。缓存行是处理器缓存间信息交换的单位:若一个处理器修改某条缓存行而另一个处理器读取同一条缓存行,则整条缓存行必须从一个处理器搬运到另一个处理器——即使两个处理器操作的是行内不同的字。由于缓存行搬运可能耗费数百个时钟周期,伪共享会显著损害性能。
方案:使用cache_aligned_allocator<T>模板,保证每次分配都对齐到独立的缓存行。由cache_aligned_allocator分配的两个对象之间保证不会发生伪共享;但如果一个对象由cache_aligned_allocator分配、另一个对象以其他方式分配,则无此保证。
这两个分配器模板可直接作为 STL 容器的allocator模板参数使用。例如声明一个使用cache_aligned_allocator分配的std::vector:
std::vector<int, cache_aligned_allocator<int>> v;使用提示:cache_aligned_allocator<T>的功能有空间代价——即使是很小的对象,它也至少要分配一条缓存行的内存。因此只有当伪共享确实可能成为问题时才应使用它(该提示同样记录在 Memory_Allocation.rst 中)。
库的选型:Which Dynamic Libraries to Use
不同分配器模板对链接库的要求并不相同。scalable_allocator<T>必须依赖可扩展内存分配器库(且无需依赖 oneTBB 通用库,可独立使用);而tbb_allocator<T>与cache_aligned_allocator<T>在分配器库存在时使用它,否则自动回退到malloc/free——因此即使应用选择不链接分配器库,这两个模板依然可用(详见 Which_Dynamic_Libraries_to_Use.rst)。
| 模板 | 库要求 | 说明 |
|---|---|---|
scalable_allocator<T> | 必须链接 oneTBB 可扩展内存分配器库 | 不要求通用库,可独立于 oneTBB 其余部分使用 |
tbb_allocator<T>、cache_aligned_allocator<T> | 无强制要求 | 分配器库存在时使用之,否则回退到malloc/free |
oneTBB 其余组件(并行算法、任务调度、容器等)无论是否链接分配器库均可正常使用。
C 级接口:scalable_malloc 函数家族
除 C++ 模板分配器外,可扩展内存分配器还提供一组与 C 标准库内存管理例程等价的函数,区别仅在于函数名带有scalable_前缀(定义于头文件<oneapi/tbb/scalable_allocator.h>,完整声明见 c_interface_to_scalable_allocator.rst):
extern "C" { // C 内存分配器的可扩展等价物 void* scalable_malloc( size_t size ); void scalable_free( void* ptr ); void* scalable_calloc( size_t nobj, size_t size ); void* scalable_realloc( void* ptr, size_t size ); // _msize/malloc_size/malloc_usable_size 的等价物 size_t scalable_msize( void* ptr ); // posix_memalign 的可扩展等价物 int scalable_posix_memalign( void** memptr, size_t alignment, size_t size ); // 对齐分配 void* scalable_aligned_malloc( size_t size, size_t alignment); void scalable_aligned_free( void* ptr ); void* scalable_aligned_realloc( void* ptr, size_t size, size_t alignment ); }除scalable_allocation_mode与scalable_allocation_command外,每个scalable_x例程的行为都与库函数x类似。这些例程构成两个家族,同一家族的分配/释放/调整大小函数必须配对使用:由scalable_x函数家族分配的存储,必须由同一家族的函数释放或调整大小,不得交给 C 标准库函数处理;反之,由 C 标准库函数分配的存储也不应由scalable_x函数释放或调整大小。
| 分配例程 | 释放例程 | 对应的库函数家族 |
|---|---|---|
scalable_malloc/scalable_calloc/scalable_realloc | scalable_free | C 标准库 |
scalable_posix_memalign | scalable_free | POSIX |
scalable_aligned_malloc/scalable_aligned_realloc | scalable_aligned_free | Microsoft C 运行时库 |
另外两个不执行分配/释放、但可查询信息或影响分配器行为的函数:
scalable_msize(ptr):若ptr指向由可扩展分配器分配的内存块,返回其可用大小;若ptr不是这样的块,返回 0。scalable_allocation_mode(mode, value)/scalable_allocation_command(cmd, reserved):见下节。
状态码方面,分配器相关函数以枚举ScalableAllocationResult返回结果:TBBMALLOC_OK(成功)、TBBMALLOC_INVALID_PARAM(参数无效)、TBBMALLOC_UNSUPPORTED(不支持)、TBBMALLOC_NO_MEMORY(内存不足)、TBBMALLOC_NO_EFFECT(操作未产生效果)。
运行期配置:分配模式、命令与环境变量
scalable_allocation_mode:分配模式
int scalable_allocation_mode(int mode, intptr_t value)用于调整分配器行为,其设置会一直生效,直到再次调用改变它。返回TBBMALLOC_OK表示成功;若mode不是下述合法值,或value对给定模式无效,返回TBBMALLOC_INVALID_PARAM。合法模式参数如下:
| 参数宏 | 语义 |
|---|---|
TBBMALLOC_USE_HUGE_PAGES | scalable_allocation_mode(TBBMALLOC_USE_HUGE_PAGES, 1)指示分配器在操作系统允许时使用大页(huge pages);传 0 关闭。若平台不支持大页,可能返回TBBMALLOC_NO_EFFECT。当前仅 Linux 支持,兼容显式配置与透明大页两种模式 |
TBBMALLOC_SET_SOFT_HEAP_LIMIT | scalable_allocation_mode(TBBMALLOC_SET_SOFT_HEAP_LIMIT, size)设置分配器从操作系统获取内存总量的软阈值(字节)。超过阈值会促使分配器释放内部缓冲中的内存,但不会阻止其在需要时继续申请更多内存 |
TBBMALLOC_SET_HUGE_SIZE_THRESHOLD | scalable_allocation_mode(TBBMALLOC_SET_HUGE_SIZE_THRESHOLD, size)设置"巨大对象"的下界阈值(字节,无上限)。任何大于该阈值的对象都被视为巨大对象,不参与内部定期清理逻辑;但不影响TBBMALLOC_SET_SOFT_HEAP_LIMIT模式与TBBMALLOC_CLEAN_ALL_BUFFERS操作的逻辑 |
scalable_allocation_command:分配器命令
int scalable_allocation_command(int cmd, void* reserved)用于命令分配器执行指定动作,第二个参数保留、必须传 0(否则返回TBBMALLOC_INVALID_PARAM)。合法命令如下:
| 命令宏 | 语义 |
|---|---|
TBBMALLOC_CLEAN_ALL_BUFFERS | 清理分配器所有线程的内部内存缓冲,可能降低内存占用;但可能导致后续分配请求耗时增加。该命令不适合频繁调用,建议仔细评估性能影响。注意:不保证调用后会释放全部未用内存;若无缓冲被释放,可能返回TBBMALLOC_NO_EFFECT |
TBBMALLOC_CLEAN_THREAD_BUFFERS | 仅清理调用线程的内部内存缓冲;若无缓冲被释放,可能返回TBBMALLOC_NO_EFFECT |
环境变量:无需改代码即可调优
部分分配器参数也可以通过系统环境变量设置,便于在不修改应用源码的情况下调整行为、确保设置尽早生效,或避免对 oneTBB 分配器二进制文件的显式依赖(详见 Allocator_Configuration.rst):
TBB_MALLOC_USE_HUGE_PAGES:控制内存映射是否使用大页。设为 1 的效果等同于scalable_allocation_mode(TBBMALLOC_USE_HUGE_PAGES, 1)。TBB_MALLOC_SET_HUGE_OBJECT_THRESHOLD:定义被视为"巨大"、不参与常规清理操作的对象大小的下界(字节)。注意:文档在环境变量一节写作TBB_MALLOC_SET_HUGE_OBJECT_THRESHOLD,而规范文档 c_interface_to_scalable_allocator.rst 中对应的环境变量名为TBB_MALLOC_SET_HUGE_SIZE_THRESHOLD,实际以你使用的 oneTBB 版本头文件与发行说明为准;该环境变量的取值上限受LONG_MAX限制。
优先级规则:这些环境变量只在内存管理器初始化时生效,之后的修改会被忽略;而scalable_allocation_mode的调用覆盖对应环境变量的效果(即函数调用优先于环境变量)。
自动替换 malloc:proxy 库方案
在 Windows 与 Linux 上,可以自动将所有标准动态内存分配函数(如malloc)的调用替换为 oneTBB 的可扩展等价实现,这有时能改善应用性能。替换由proxy 库提供(库名见各平台小节),且proxy 库与可扩展内存分配器库必须取自同一 oneTBB 发行版,否则两者可能互不兼容(详见 automatically-replacing-malloc.rst)。
Linux 下的替换
release 版 proxy 库为libtbbmalloc_proxy.so,debug 版为libtbbmalloc_proxy_debug.so。被替换的动态内存函数包括:
- 标准 C 库函数:
malloc、calloc、realloc、free,以及 C11 新增的aligned_alloc; - 标准 POSIX 函数:
posix_memalign; - 已废弃函数:
valloc、memalign、pvalloc、mallopt; - 可替换的全局 C++ 运算符
new与delete; - glibc 特有函数:
malloc_usable_size、__libc_malloc、__libc_calloc、__libc_memalign、__libc_free、__libc_realloc、__libc_pvalloc、__libc_valloc。
两种替换方式(详见 Linux_C_Dynamic_Memory_Interface_Replacement.rst):
# 方式一:程序加载时通过 LD_PRELOAD 装载 release 版 proxy 库(无需改动可执行文件) LD_PRELOAD=libtbbmalloc_proxy.so # 方式二:链接主可执行文件时直接链接 proxy 库 g++ foo.o bar.o -ltbbmalloc_proxy -o a.out使用 debug 版时,将上述命令中的tbbmalloc_proxy替换为tbbmalloc_proxy_debug即可。程序加载器必须能在加载时找到 proxy 库与可扩展内存分配器库——可将库所在目录加入LD_LIBRARY_PATH环境变量,或加入/etc/ld.so.conf。
Linux 替换限制:不支持 glibc 内存分配钩子(如__malloc_hook);不支持 Mono 运行时。
Windows 下的替换
release 版 proxy 库为tbbmalloc_proxy.dll,debug 版为tbbmalloc_proxy_debug.dll。被替换的函数包括:标准 C 库的malloc、calloc、realloc、free;可替换的全局new/delete;Microsoft C 运行时库的_msize、_aligned_malloc、_aligned_realloc、_aligned_free、_aligned_msize。注意:不支持对 Universal Windows Platform(UWP)应用的替换。
两种替换方式(详见 Windows_C_Dynamic_Memory_Interface_Replacement.rst):
// 方式一:在应用启动时加载的任一二进制源码中加入头文件 #include "oneapi/tbb/tbbmalloc_proxy.h"; 方式二:在启动时加载的 .exe/.dll 的链接器选项中添加参数 ; 32 位代码(注意是三重下划线) tbbmalloc_proxy.lib /INCLUDE:"___TBB_malloc_proxy" ; 64 位代码(注意是双重下划线) tbbmalloc_proxy.lib /INCLUDE:"__TBB_malloc_proxy"程序加载器须能在加载时找到 proxy 库与分配器库,可将库所在目录加入PATH环境变量。Windows 的替换基于对 Visual C++ 运行时库的内存内二进制插桩(in-memory binary instrumentation):为保证正确性,必须首先识别出这些库中的一部分动态内存函数;若出现问题,替换会被跳过,程序继续使用标准内存分配函数。可用TBB_malloc_replacement_log函数检查替换是否成功并获取附加信息。若要将某次程序调用的替换禁用,可将TBB_MALLOC_DISABLE_REPLACEMENT环境变量设为 1;但即使禁用,程序启动时仍然需要 oneTBB 内存分配库存在。
与 mold 项目的对应关系:从文档到真实并行代码
上述文档体系并非孤立的第三方资料,而是 mold 实际依赖的并行基础设施。mold 在链接流程的多个热路径上使用 oneTBB 的并行组件,例如:
- gc-sections.cc 使用
tbb::concurrent_unordered_map与tbb::concurrent_vector组织段集合(sections)与根集(rootset),并用tbb::parallel_for_each并行遍历输入对象文件; - arch-arm32.cc 与 arch-ppc64v1.cc 等架构后端用
tbb::parallel_for/tbb::parallel_for_each并行处理重定位等条目; - cmdline.cc 通过
tbb::global_control设置并行工作线程数量。
这正是"多线程需要可扩展内存分配、避免分配器串行化瓶颈"这一文档核心诉求的真实场景。对于需要自己管理内存分配的模块(例如大量小对象并发分配),即可按本文介绍的分配器模板与 C 接口进行替换;如需全局接管,则可参考 proxy 库方案。相关文档与规范的完整体系可在仓库内继续研读:Memory_Allocation.rst(分配器模板与伪共享详解)、Which_Dynamic_Libraries_to_Use.rst(库选型表)、Allocator_Configuration.rst(运行期配置)、c_interface_to_scalable_allocator.rst(C 接口规范),以及 specification/source/memory_allocation.rst(内存分配规范总览,涵盖std::pmr::memory_resource实现如cache_aligned_resource、scalable_memory_resource)。
实践要点小结
- 按需选库:仅用
scalable_allocator<T>可只链接分配器库;tbb_allocator<T>/cache_aligned_allocator<T>在无分配器库时自动回退到malloc/free。 - 配对使用:
scalable_x家族与 C 标准库家族的分配/释放函数不可混用;scalable_allocation_command的reserved参数必须为 0。 - 区分两类问题:追求多线程分配吞吐用
scalable_allocator<T>;对抗缓存行伪共享用cache_aligned_allocator<T>(注意其最小按缓存行分配的空间代价)。 - 配置优先级:
scalable_allocation_mode优先于同名环境变量;环境变量仅在内存管理器初始化时生效。 - proxy 替换:Linux 用
LD_PRELOAD或链接-ltbbmalloc_proxy;Windows 用头文件或/INCLUDE链接选项;务必保证 proxy 库与分配器库来自同一 oneTBB 发行版。 - 平台限制:大页模式目前仅 Linux 支持;glibc 的
__malloc_hook与 Mono 不受支持;Windows 的 UWP 应用不支持替换。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考