TCMalloc 内存分配 API 完整参考:C/C++ malloc 与 operator new 全家族解析
TCMalloc 内存分配 API 完整参考:C/C++ malloc 与 operator new 全家族解析
TCMalloc(Thread-Caching Malloc)为 C 标准库的 <stdlib.h> 动态内存管理例程(malloc()、calloc()、realloc()、free()、aligned_alloc() 等)以及 C++ 标准库 <new> 头文件中的 ::operator new / ::operator delete 全家族提供了高性能实现。本文以官方 reference.md 为骨架,结合当前仓库的 tcmalloc.cc、libc_override.h、malloc_extension.h 等源码实现,系统讲解每一个 API 的签名、行为语义、边界条件(零大小、对齐、失败路径)以及 TCMalloc 特有的扩展接口(hot_cold_t 访问提示、__size_returning_new()、nallocx()/sdallocx()),帮助你理解 TCMalloc 对外暴露的完整 API 面,并能在自己的 C/C++ 工程中正确接入与调用。
一、适用前提与标准要求
在使用 TCMalloc 提供的这些 API 之前,需要先确认代码符合以下标准要求:
- C 标准库代码:要求符合 C11 或更高版本 的 C 标准库(
<stdlib.h>中的动态内存管理 API)。 - C++ 标准库代码:要求符合 C++11、C++14 或 C++17 的标准库。C++14 引入 sized
::operator delete,C++17 引入 overaligned::operator new/::operator delete,TCMalloc 对这几个版本的新增变体均有完整实现。
需要特别说明的是:虽然本参考文档中的 C API 部分以 C 语言语法描述(例如使用 size_t),但整个 TCMalloc API 设计上都可以直接在 C++ 代码中调用,且大多数实际使用场景正是 C++。使用等价的 C++ 别名类型(例如 std::size_t)的调用同样被天然支持。
在仓库中,这些 API 的实现与导出集中在 tcmalloc/tcmalloc.cc(内部实现函数)与 tcmalloc/libc_override.h(对外符号别名声明)两个文件中;接入工程的完整步骤可以参考 docs/quickstart.md(Bazel 方式,在 cc_binary 中通过 malloc = "@com_google_tcmalloc//tcmalloc" 指定自定义分配器)。
二、C++ API:::operator new 与 ::operator delete 家族
TCMalloc 实现了 C++11 / C++14 / C++17 标准中暴露在 <new> 头文件里的全部 operator new 与 operator delete 变体,包括:
- 基础形态的
::operator new()、::operator delete()及对应的数组变体([]); - C++14 引入的 sized
::operator delete()(带std::size_t大小参数); - C++17 引入的 overaligned
::operator new()/::operator delete()(带std::align_val_t对齐参数)。按照 C++ 标准要求,使用 alignedoperator new分配的内存,必须使用 alignedoperator delete释放。
2.1 ::operator new / ::operator new[] 全签名
TCMalloc 支持以下重载形态:
void* operator new(std::size_t count);
void* operator new(std::size_t count, const std::nothrow_t& tag) noexcept;
void* operator new(std::size_t count, std::align_val_t al); // C++17
void* operator new(std::size_t count,
std::align_val_t al, const std::nothrow_t&) noexcept; // C++17
void* operator new[](std::size_t count);
void* operator new[](std::size_t count, const std::nothrow_t& tag) noexcept;
void* operator new[](std::size_t count, std::align_val_t al); // C++17
void* operator new[](std::size_t count,
std::align_val_t al, const std::nothrow_t&) noexcept; // C++17
语义要点:
operator new/operator new[]分配count字节内存。它们可以(在极少情况下)被直接调用,但更常见的调用方式是作为 C++ 的 new-expression(即new T(...)/new T[n])的一部分由编译器隐式触发。std::align_val_t变体返回的内存会按请求的al对齐值进行对齐。
默认对齐行为(关键细节):
- 当未定义
__STDCPP_DEFAULT_NEW_ALIGNMENT__(或它的值大于 8 字节)时,对于不带std::align_val_t参数的::operator new,TCMalloc 使用标准的 16 字节对齐; - 但是,对于小于 16 字节的分配,TCMalloc 可能返回对齐要求更低的地址——因为小于 16 字节的空间里不可能容纳任何要求更大对齐的对象,这样做不会违反标准语义;
- 当编译时指定
__STDCPP_DEFAULT_NEW_ALIGNMENT__ <= 8时,TCMalloc 对使用::operator new分配的原始存储采用一组按 8 字节对齐的尺寸。 - 注意:在很多平台上,
__STDCPP_DEFAULT_NEW_ALIGNMENT__的值可以通过编译器的-fnew-alignment=...标志配置。
失败路径语义(与 C++ 标准的差异,务必注意):
- 如果分配失败,TCMalloc 会直接终止程序。这一点与 C++ 标准不同——标准要求在分配失败时抛出
std::bad_alloc异常,或反复调用std::get_new_handler()尝试再次分配;而 TCMalloc 直接崩溃,不再尝试。 - 这一行为可以被用作一类性能优化:对尚未标记
noexcept的移动构造函数而言,其内部的内存分配失败可以直接终止程序,而不必回退到拷贝构造等复杂路径。在 Abseil 代码中,这类直接分配失败行为由 Abseil 的构建期配置宏ABSL_ALLOCATOR_NOTHROW开启。 - 如果使用带
std::nothrow_t的变体,失败时::operator new会返回nullptr而不是终止程序。
hot/cold 访问提示扩展(TCMalloc 特有):
tcmalloc::hot_cold_t 变体接受一个 hot_cold 访问频率提示。该提示是一个 8 位无符号整数(uint8_t):
0表示该分配会被很少访问(cold);255表示该分配会被非常频繁地访问(hot)。
TCMalloc 可以使用这些提示进行更好的数据放置与局部性优化(例如将冷热数据分别映射到不同的 size class 区域)。在源码层面,__hot_cold_t 定义于 tcmalloc/malloc_extension.h,并通过 using hot_cold_t = __hot_cold_t; 在 tcmalloc 命名空间暴露别名;同时定义了 kDefaultMinHotAccessHint = 1 作为默认的最小热访问提示值。
从实现上看,operator new(size_t, __hot_cold_t) 等变体最终会进入 fast_alloc(size, CppPolicy().AccessAs(hot_cold))(见 tcmalloc/tcmalloc.cc),访问提示通过 tcmalloc_policy.h 中的 AllocationAccessAsPolicy 模板传入分配策略,并最终影响 size class 的选取(在 tcmalloc/tcmalloc.cc 中,冷 size class 的分配会映射为 hot_cold_t{0},热 size class 映射为 hot_cold_t{255})。仓库中的 tcmalloc/new_extension_test.cc 对带 hot_cold_t{0}、hot_cold_t{128}、hot_cold_t{255} 的 operator new/operator new[] 进行了失败路径(超大分配直接终止)的测试验证。
2.2 ::operator delete / ::operator delete[] 全签名
void operator delete(void* ptr) noexcept;
void operator delete(void* ptr, std::size_t sz) noexcept;
void operator delete(void* ptr, std::align_val_t al) noexcept;
void operator delete(void* ptr, std::size_t sz,
std::align_val_t all) noexcept;
void operator delete[](void* ptr) noexcept;
void operator delete[](void* ptr, std::size_t sz) noexcept; // C++14
void operator delete[](void* ptr, std::align_val_t al) noexcept; // C++17
void operator delete[](void* ptr, std::size_t sz,
std::align_val_t al) noexcept; // C++17
语义要点:
::operator delete/::operator delete[]用于释放之前由对应形态的::operator new/::operator new[]分配的内存;最常见的调用方式是作为 delete-expression 的一部分。- sized delete 是一项关键的性能优化:它在释放时直接携带分配大小,免去了"从指针反查大小"的高成本查找过程,因此 TCMalloc 强烈建议编译器在可能时使用 sized delete 形态。
在实现层面,sized 释放路径会走 TCMallocInternalDeleteSized → do_free_with_size(p, size, CppPolicy()),而 aligned 释放路径 TCMallocInternalDeleteAligned 还会额外用 CorrectAlignment(p, alignment) 校验传入对齐与实际指针是否一致(见 tcmalloc/tcmalloc.cc)。
2.3 扩展:__size_returning_new()(P0901 原型)
TCMalloc 还暴露了一个 P0901("size feedback in operator new",2019 年的 C++ 标准提案)的原型实现,声明于 tcmalloc/malloc_extension.h:
__size_returning_new(size_t size):同时返回内存指针和实际分配字节数。返回值类型为全局命名空间的__sized_ptr_t结构体({ void* p; size_t n; },定义于 tcmalloc/malloc_extension.h),其中n是实际容量(可能大于请求的size,因为会向上取整到 size class)。- 该内存可以直接用
::operator delete释放;若使用 sized delete 释放,传入的 size 参数必须"不小于原始请求大小、且不大于返回的容量"。 - 在 C++ 侧还提供了更易用的封装:
tcmalloc_size_returning_operator_new()、tcmalloc_size_returning_operator_new_hot_cold()、以及带std::align_val_t和nothrow的多种组合(其中 nothrow 变体失败时返回{nullptr, 0})。 - 仓库为
__size_returning_new/__size_returning_new_aligned等符号提供了强实现(TCMALLOC_ALIAS(TCMallocInternalSizeReturningNew),见 tcmalloc/tcmalloc.cc),它们最终调用fast_alloc(size, CppPolicy().SizeReturning())。
三、C API:<stdlib.h> 动态内存管理全解析
C 标准库在 <stdlib.h> 头文件中规定了动态内存管理的 API,TCMalloc 的实现要求使用 C11 或更高版本。TCMalloc 提供以下 C API 函数的实现:
| 函数 | 说明 |
|---|---|
malloc() |
分配 size 字节 |
calloc() |
为对象数组分配内存并清零 |
realloc() |
扩展或收缩已有内存区域 |
free() |
释放内存 |
aligned_alloc() |
按指定对齐分配内存 |
此外还提供 POSIX 标准库函数(glibc 内可用):
posix_memalign()
以及通常由 libc 实现提供的过时函数(仅为兼容性提供,不提供文档):
cfree()memalign()valloc()pvalloc()
3.1 对齐语义:遵循 C90 DR075 与 DR445
对于 malloc、calloc 和 realloc,TCMalloc 遵守 C90 DR075 与 DR445 的规定:
即使 size 小到无法容纳任何需要给定对齐的对象,对齐要求仍然适用。
换言之,malloc(1) 返回的指针必须按 alignof(std::max_align_t) 对齐。这保证了任何类型(含 SIMD 向量类型等)的指针都可以安全地写入这块内存。基于 N2293 提案的进展,TCMalloc 未来可能会放宽这一对齐要求。
在策略层,这一语义体现在 tcmalloc/tcmalloc_policy.h:MallocAlignPolicy::align() 返回 alignof(std::max_align_t),malloc 路径(MallocPolicy())正是基于这一对齐策略;而 C++ 的 ::operator new 默认路径(CppPolicy())则使用默认 size table 的对齐(DefaultAlignPolicy,其 constexpr 值为 1,表示使用 TCMalloc 大小表自身的默认对齐)。
3.2 malloc()
void* malloc(size_t size);
- 分配
size字节内存,返回指向该内存起始处的void *指针。 malloc(0)返回一个非 NULL 的零大小指针(对该地址进行内存访问属于未定义行为)。- 如果
malloc()因任何原因失败,返回NULL。
对应实现为 TCMallocInternalMalloc:return fast_alloc(size, MallocPolicy())(tcmalloc/tcmalloc.cc)。
3.3 calloc()
void* calloc(size_t num, size_t size);
- 为
num个对象(每个对象size字节)的数组分配内存,将分配存储中的所有字节零初始化;成功时返回指向分配内存块首字节的指针。 calloc(num, 0)或calloc(0, size)返回一个非 NULL 的零大小指针(访问该地址为未定义行为)。- 如果失败,返回
NULL。
实现要点:TCMallocInternalCalloc 在分配前会通过 MultiplyOverflow(n, elem_size, &size) 做乘法溢出检查,溢出时直接进入 OOM 处理(tcmalloc/tcmalloc.cc);分配成功后用 memset(result, 0, size) 清零。
3.4 realloc()
void* realloc(void *ptr, size_t new_size);
- 对已有内存区域重新分配:根据传入的
new_size(字节)扩展或收缩该内存,返回指向该内存起始处的void*指针(指针可能不变,也可能改变);不会对新区域的内存执行任何初始化。 realloc(OBJ*, 0)返回一个NULL指针(并且会释放原内存)。- 如果
realloc()失败,同样返回NULL。
实现要点:TCMallocInternalRealloc 对 ptr == nullptr 的情况直接等价于 malloc(size);对 size == 0 的情况执行 do_free 并返回 nullptr;否则进入 do_realloc(tcmalloc/tcmalloc.cc)。do_realloc 内部会通过 GetSizeAndSampled 获取旧块大小;对被采样的分配(sampled allocation),即使严格意义上不必要也会重新分配并复制数据,这一点对超大分配可能造成额外开销。
3.5 aligned_alloc()
void* aligned_alloc(size_t alignment, size_t size);
- 以
alignment对齐分配size字节内存,返回指向内存起始处的void *指针;不执行任何初始化。 - 约束条件:
size必须是alignment的整数倍,且alignment必须是 2 的幂。若任一条件不满足,aligned_alloc()失败并返回NULL指针。 aligned_alloc且size=0时返回非 NULL 的零大小指针(访问该地址为未定义行为)。
实现要点:TCMallocInternalAlignedAlloc 会先校验 alignment 是否为 2 的幂(absl::has_single_bit(align)),不满足时按 glibc/FreeBSD/NetBSD 手册约定设置 errno = EINVAL 并返回 nullptr(tcmalloc/tcmalloc.cc)。
3.6 posix_memalign()
int posix_memalign(void **memptr, size_t alignment, size_t size);
- 与
aligned_alloc()类似,分配size字节、按alignment对齐的内存,并将其起始地址写入**memptr指向的位置;不执行任何初始化。该指针可以转换为所需类型的数据指针后解引用。 - 成功时返回
0;否则返回错误值。 - 与
aligned_alloc()的区别在于约束:alignment必须是sizeof(void *)的 2 的幂倍数。约束不满足时posix_memalign()失败。 posix_memalign且size=0时返回非 NULL 的零大小指针(访问该地址为未定义行为)。
实现要点:TCMallocInternalPosixMemalign 校验 (align % sizeof(void*)) != 0 || !absl::has_single_bit(align),不满足返回 EINVAL;分配失败(fast_alloc 返回空)时返回 ENOMEM;成功时写入 *result_ptr 并返回 0(tcmalloc/tcmalloc.cc)。
3.7 free()
void free(void* ptr);
- 释放之前由
malloc()、calloc()、aligned_alloc()、posix_memalign()或realloc()分配的内存。 - 若传入空指针,
free()不做任何事情。
对应实现为 TCMallocInternalFree:do_free(ptr, MallocPolicy())(tcmalloc/tcmalloc.cc)。
3.8 过时函数(仅兼容性提供)
以下函数通常由 libc 实现提供,TCMalloc 为兼容性实现了它们,但不提供文档:
cfree():等同于free()(实现为TCMALLOC_ALIAS(TCMallocInternalFree),见 tcmalloc/tcmalloc.cc)。memalign(size_t align, size_t s):TCMallocInternalMemalign走MallocPolicy().AlignAs(align)。valloc(size_t size):分配页对齐的对象(MallocPolicy().AlignAs(GetPageSize()))。pvalloc(size_t size):把 size 向上取整到页大小的整数倍后再页对齐分配;pvalloc(0)按历史约定分配一页(见 tcmalloc/tcmalloc.cc)。
3.9 扩展:nallocx() 与 sdallocx()
TCMalloc 还提供两个源自 jemalloc 的扩展函数,声明位于 tcmalloc/malloc_extension.h:
nallocx(size_t size, int flags):不分配任何内存,只返回"与malloc(size)等价的分配"实际会得到的字节数,该结果受flags指定的对齐约束影响。它通过 size class 映射计算实际容量(小对象返回class_to_size(size_class),大对象向上取整到页边界,见 tcmalloc/tcmalloc.cc 的nallocx_slow与nallocx)。flags的低 6 位编码对齐指数:alignment = 1 << (flags & 0x3f)。注意:文档与头文件均建议优先使用tcmalloc_size_returning_operator_new而不是nallocx,前者更高效且能为 TCMalloc 提供更好的遥测数据。sdallocx(void* ptr, size_t size, int flags):释放由malloc或memalign分配的内存,携带原始分配大小size,以提升释放性能。默认弱实现只是调用free(),而 TCMalloc 的强实现TCMallocInternalSdallocx会利用 size 走do_free_with_size;当flags != 0时按同样方式从低 6 位解码对齐并执行对齐释放(tcmalloc/tcmalloc.cc)。
四、源码层面的导出机制:符号别名与 glibc 挂钩
理解 TCMalloc 为什么能"接管"整个进程的 malloc/operator new,需要看 tcmalloc/libc_override.h 中的导出机制:
TCMALLOC_ALIAS(tc_fn):利用编译器的__attribute__((alias(...), visibility("default")))将公开符号直接别名到内部实现。例如malloc→TCMallocInternalMalloc、::operator new(size_t)→TCMallocInternalNew、::operator delete(void*, size_t)→TCMallocInternalDeleteSized(tcmalloc/libc_override.h)。- glibc 内部符号挂钩:在 glibc 环境下,还会把
__libc_malloc、__libc_free、__libc_realloc、__libc_calloc、__posix_memalign等内部符号同样别名到 TCMalloc 实现,并设置__malloc_hook/__free_hook等钩子(在静态链接的怪异链接器错误场景下仍能生效),确保共享库在 DEEPBIND 等特殊场景下也走 TCMalloc 路径(tcmalloc/libc_override.h)。 - 通过这种"别名 + 强符号"机制,TCMalloc 还额外导出了一批 glibc 兼容函数,例如
malloc_stats()、malloc_trim()、mallopt()、malloc_usable_size()、reallocarray()、free_sized()/free_aligned_sized()(C23 引入的 sized free)等(tcmalloc/libc_override.h)。
五、统一的分配入口:fast_alloc 与策略模板
所有 API 最终都汇聚到同一个快速分配入口 fast_alloc(size, Policy),其中 Policy 是一个由 tcmalloc/tcmalloc_policy.h 中定义的编译期策略模板组合而成的类型。这个设计让你可以直观理解每个 API 行为差异的来源:
MallocPolicy():默认MallocAlignPolicy(alignof(std::max_align_t)对齐)+ OOM 时返回空指针的语义,服务于malloc一族;CppPolicy():默认DefaultAlignPolicy(16 字节默认对齐)+CppOOMPolicy(OOM 时直接CrashWithOOM终止程序,不抛异常),服务于::operator new一族;- 通过
.Nothrow()、.AlignAs(align)、.AccessAs(hot_cold)、.SizeReturning()等链式方法,可以组合出 nothrow、对齐、冷热提示、返回容量等所有变体(tcmalloc/tcmalloc_policy.h)。
例如 TCMallocInternalNewNothrow 对应 fast_alloc(size, CppPolicy().Nothrow()),TCMallocInternalNewAlignedHotCold 对应 fast_alloc(size, CppPolicy().AlignAs(alignment).AccessAs(hot_cold))(tcmalloc/tcmalloc.cc)。
六、如何验证与进一步阅读
- 想快速上手在 Bazel 工程中接入 TCMalloc,参考 docs/quickstart.md(
bazel test //tcmalloc/...可运行全部测试;cc_binary中指定malloc = "@com_google_tcmalloc//tcmalloc"即可让目标二进制使用 TCMalloc)。 - 想深入理解 TCMalloc 的分配架构(Thread Cache、Central Free List、Page Heap、Huge Page 等),阅读 docs/overview.md。
- 想确认每个 API 的实际行为,仓库中有大量对应测试:例如 tcmalloc/testing/tcmalloc_test.cc、tcmalloc/testing/realloc_test.cc、tcmalloc/testing/memalign_test.cc、tcmalloc/new_extension_test.cc(验证 hot_cold 变体与 size-returning 扩展)、以及 tcmalloc/malloc_extension_test.cc。
- 想要查阅扩展 API 的完整声明(
nallocx、sdallocx、__size_returning_new、hot_cold_t、tcmalloc::sized_ptr_t等),直接阅读 tcmalloc/malloc_extension.h。
结语
TCMalloc 的 API 表面看似只是标准库例程的"再实现",但细节中充满了对性能与标准语义的精妙权衡:默认 16 字节对齐与小于 16 字节分配的放宽、sized delete 免去指针反查、nothrow 与"直接崩溃"两条失败路径的选择、hot_cold_t 冷热提示参与 size class 映射、__size_returning_new 消除"分配后还要自己记录大小"的负担。理解这些 API 的语义边界(零大小、对齐约束、溢出检查、失败行为)与它们在源码中的统一策略化实现,是正确使用和深入排查 TCMalloc 行为的前提。