TCMalloc 内存分配 API 完整参考:C/C++ malloc 与 operator new 全家族解析

原创2026-10-12 22:01:29614 阅读
文章标签:内存管理标准库

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++ 标准要求,使用 aligned operator new 分配的内存,必须使用 aligned operator 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)。

六、如何验证与进一步阅读

结语

TCMalloc 的 API 表面看似只是标准库例程的"再实现",但细节中充满了对性能与标准语义的精妙权衡:默认 16 字节对齐与小于 16 字节分配的放宽、sized delete 免去指针反查、nothrow 与"直接崩溃"两条失败路径的选择、hot_cold_t 冷热提示参与 size class 映射、__size_returning_new 消除"分配后还要自己记录大小"的负担。理解这些 API 的语义边界(零大小、对齐约束、溢出检查、失败行为)与它们在源码中的统一策略化实现,是正确使用和深入排查 TCMalloc 行为的前提。

登录后查看全文
tcmalloc