嵌入式 tcmalloc
地址适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(构建libtpu_lts_20260413_b_RC00,build-id md589edbbe81c5b328a958fe628a9f2207d)。该镜像未剥离;反修饰后的 C++ 符号名按原样引用。其他版本会不同。
摘要
libtpu.so 中主机侧分配器的情况有一个与本页标题相矛盾的核心结论:libtpu 并未嵌入一个可工作的 tcmalloc。 它链接了一个被截去核心的 tcmalloc,即 MallocExtension / MallocHook / 实验选择 API 表面,以及按 CPU 的 rseq 原语;这些内容被打包进 18,162 字节的 google_malloc ELF 节([19] @ 0x0e6373c0)和 261 字节的 __lcxx_override 节([26] @ 0x213f0720),但没有任何分配器核心。没有 PageHeap,没有 CentralFreeList,没有 PerCPUCache,没有 TransferCache,没有 SizeMap,没有 ThreadCache,没有 HugePageAwareAllocator,也没有 PageAllocator。实际的进程 malloc 来自 glibc:__lcxx_override 中的每个 operator new 都调用 PLT malloc,它通过 R_X86_64_JUMP_SLOT 解析到 malloc@GLIBC_2.2.5;每个 operator delete 都调用 free@GLIBC_2.2.5。jemalloc 完全不存在(0 个符号,0 个字符串)。
这对重新实现者很重要,原因很明确。熟悉 tcmalloc 的读者会期待本页回答这些问题:大小类是什么、每线程缓存有多大、中央空闲链表的几何结构是什么、哪些 TCMALLOC_* 开关会被遵守。这些问题没有数值答案,因为保存这些数值的数据结构完全位于缺失的核心中。对“大小类结构”的诚实回答是此二进制中没有;对“中央/线程缓存”的回答是不存在;对“TPU 专用调优”的回答是一次无操作的软限制调用,以及一张对分配没有影响的实验名查询表。本来会把 tcmalloc 接入进来的机制,是经典的 Abseil 弱-强符号插入(tcmalloc/malloc_extension.cc 将 _Internal provider 声明为 ABSL_ATTRIBUTE_WEAK);在这个 wheel 中没有链接强定义,因此每个弱引用都保持 UND→NULL,每个 if (&MallocExtension_Internal_X) guard 都求值为 false。
本页按顺序记录三件事:(1) 链接关系,即二进制里有什么、没有什么,四个 operator new 函数体,以及让它们绑定到 glibc 的弱符号插入;(2) 已存在的垫片表面,包括 MallocExtension wrapper、MallocHook、实验格架,以及唯一的软限制开关,它们全都惰性无效;(3) 想要真实每线程缓存和大小类的工程师实际应该使用什么(glibc tunables),以及设备侧分配器在哪里(hbm-allocator.md、overview.md),因为管理 TPU 上每个重要字节的是这些分配器,而不是 tcmalloc。
注意 — 页面标题是一个为延续页面族命名而保留的误称。tcmalloc 以 API/hook/experiment 垫片的形式存在(约 55 KiB,约 110 个符号),但并未作为分配器被嵌入,它的核心没有被链接。
google_malloc节不包含malloc,只包含支撑代码;所有主机分配都经由__lcxx_override→ PLT → glibc。overview.md 的 host-heap 行也用一眼可见的方式给出同样结论;本页是其细节。
对重新实现而言,定向约定如下:
- 链接决策 — tcmalloc 的分配器核心 TU 未链接;
_Internalprovider 是弱UND;glibc 提供强malloc/free/aligned_alloc/posix_memalign。没有IFUNC,没有IRELATIVE,也没有--whole-archive强制构造器。 operator new/delete函数体 —__lcxx_override中四个会抛异常的可替换 operator、它们的malloc/aligned_alloc重试循环、new_handler过程,以及被丢弃的__hot_cold_t提示。- 惰性的垫片表面 — 8 个
MallocExtensionwrapper(受弱空值 guard 保护的 thunk)、MallocHook(可注册但永不触发)、18 项实验表,以及一次运行时无操作的软限制调用。 - 替换条件 — 因为选择取决于“最终链接时是否存在强符号”,所以一个静态链接真实 tcmalloc 的 google3 内部二进制会点亮整个垫片;pip-wheel 安装不会。
| 工作的进程分配器 | glibc malloc/free(malloc@GLIBC_2.2.5、free@GLIBC_2.2.5)— 不是 tcmalloc |
| tcmalloc 足迹 | 约 110 个符号 / 约 55,313 B;仅支撑代码(无分配器核心) |
google_malloc 节 | [19] @ 0x0e6373c0,18,162 B — rseq/experiments/MallocHook/crash-printer |
google_malloc_bss | [48] @ 0x2285a180,20,736 B NOBITS — 仅垫片状态(无 PageMap/CFL) |
__lcxx_override 节 | [26] @ 0x213f0720,261 B,AX align 32 — 4 个会抛异常的 operator new 函数体 |
malloc_hook 节 | [25] @ 0x213efe80,2,206 B(0x89e)— mmap/munmap/sbrk/LowLevelAlloc thunk |
__rseq_cs 节 | [39] @ 0x224bf980,8,800 B — 247 个 RseqFunction_* + 28 个 CountingMutex(共享,并非 tcmalloc 专属) |
operator new(size_t) | 0x213f0720(69 B)→ PLT malloc 0x213f10a0 → malloc@GLIBC_2.2.5 |
| jemalloc | 不存在(0 个 je_*,0 个 mallctl,0 个 MALLOC_CONF) |
| Size classes / ThreadCache / CentralFreeList | 0 个符号 — 本构建中不存在 |
| 唯一的调优尝试 | BarnaCoreManagerBase::Init 调用 MallocExtension::SetMemoryLimit — no-op(弱 UND) |
| 置信度 | HIGH(架构性发现已在符号、重定位、节大小和反编译层面逐字节确认) |
1. 链接决策
目的
tcmalloc 是否是进程分配器是在最终链接时决定的,而不是在运行时决定的,并且这个二进制中的决定并不支持 tcmalloc。本节建立支配其他所有章节的否定结果:分配器核心缺失,glibc 才是 malloc,tcmalloc 唯一贡献的是惰性无效的 API/hook/experiment 垫片。若重新实现者把页面族名称按字面理解,就会错误建模整个主机堆。
存在与缺失
nm/符号表分区是决定性的。完整恢复的 google_malloc 节清单([19],18,162 B)只包含支撑代码:按 CPU 的 rseq trampoline、实验选择、MallocHook 生命周期、信号安全 I/O、crash/OOM printer,以及 PbtxtRegion 统计序列化器。分配器核心类的符号数量为零。
| 类别 | 是否存在? |
|---|---|
分配器核心(PageHeap、CentralFreeList、PerCPUCache、TransferCache、SizeMap、ThreadCache、HugePageAwareAllocator、PageAllocator) | 否 — 0 个符号 |
强 malloc/free/tc_malloc/tc_new/tc_free | 否 |
大小类表 / kPageSize / kHugePageSize / kMaxSize / kNumClasses | 否 |
MallocExtension / TCMalloc_Internal API | 是,但为弱 UND |
MallocHook(Add/Remove{New,Delete,…}Hook、Invoke…HookSlow) | 是,可注册 |
实验选择(SelectExperiments,18 项表) | 是 |
rseq 按 CPU 原语 | 是,共享 |
| jemalloc(任何内容) | 否 — 0 个符号,0 个字符串 |
陷阱 —
rseq原语(RseqFunction_PerCpuCmpxchg64、PerCpuTryLock、PerCpuReadCycleCounter)并不能证明这里存在 tcmalloc 的按 CPU 缓存。__rseq_cs中 247 条RseqFunction_*记录的消费者是 abseil synchronization 和 RCU,也就是共享的 google3 按 CPU 库。若重新实现者推断“存在 rseq ⇒ tcmalloc 按 CPU 缓存处于活动状态”,就会建模一个二进制中并不存在的缓存。缓存位于缺失的核心中;rseq trampoline 因其他子系统使用而保留下来。
替换机制 — 弱-强符号插入(此处惰性无效)
tcmalloc 用来让自己成为可选项的接线方式,是标准 Abseil 模式:tcmalloc/malloc_extension.cc 将 _Internal hook 声明为 ABSL_ATTRIBUTE_WEAK,因此二进制可以链接 MallocExtension API,而不强迫 tcmalloc 成为 malloc。每个弱 provider 都带有 addend 为 0 的 GLOB_DAT + JUMP_SLOT reloc:
0x224c3700 JUMP_SLOT MallocExtension_Internal_MarkThreadIdle + 0
0x224c3708 JUMP_SLOT MallocExtension_Internal_MarkThreadBusy + 0
0x224c3710 JUMP_SLOT MallocExtension_Internal_SetMemoryLimit + 0
0x224c3718 JUMP_SLOT MallocExtension_Internal_GetNumericProperty + 0
0x224c3720 JUMP_SLOT MallocExtension_Internal_GetAllocatedSize + 0
0x224c3728 JUMP_SLOT MallocExtension_Internal_GetProperties + 0
0x224c3730 JUMP_SLOT MallocExtension_Internal_ProcessBackgroundActions + 0
0x224c3628 JUMP_SLOT TCMalloc_Internal_PossiblyCold + 0
0x224c3698 JUMP_SLOT TCMalloc_Internal_SetProfileSamplingInterval + 0
0x224c36a0 JUMP_SLOT TCMalloc_Internal_GetStats + 0
(each also has a paired GLOB_DAT in 0x22054ff0..0x22055130)
```text
解析规则是二元的:
- **链接了真实 tcmalloc 时**(例如在 google3 内部):tcmalloc 的 TU 定义*强* `malloc`/`free`/`MallocExtension_Internal_*`/`TCMalloc_Internal_*`。弱引用绑定到它们;guard 为 true;C++ `new`/`delete` 和 `MallocExtension` wrapper 都路由到 tcmalloc。
- **在这个 `libtpu.so` 中**(pip wheel):分配器核心 TU *未*链接。强定义缺失。弱符号保持 `UND`→`NULL`,所以 `malloc`/`free` 绑定到唯一剩下的 provider,即通过普通 PLT 指向 `libc.so` 的 glibc;而 `if (&MallocExtension_Internal_X)` 为 false,因此每个 wrapper 都返回默认值(no-op / `0` / unset)。
分配器函数没有 **`STT_GNU_IFUNC`**,也没有 **`R_X86_64_IRELATIVE`**:不存在运行时分配器选择 resolver。也没有由 `--whole-archive` 强制保留的 tcmalloc 构造器。选择纯粹取决于“链接时是否存在强符号”,而这里不存在。
> **注意 —** 唯一残留的不确定性是*进程作用域*。弱 `UND` 符号是在加载时针对整个进程镜像解析的,而不只针对 `libtpu.so`。如果部署的 JAX/TPU 进程中某个*其他* DSO 静态链接了真实 tcmalloc,并导出了强 `malloc`/`MallocExtension_Internal_*`,libtpu 的弱引用会绑定到*那个*实现,垫片也会被点亮。在标准 pip-wheel 安装中(libtpu 由使用 glibc malloc 的 CPython 加载)不会发生,因此 no-op 分析成立。这就是“取决于最终链接”的限定,也是任何大小类 / 缓存行为回来的唯一路径。(标准安装中存在此类 DSO 的可能性为 LOW。)
---
## 2. `operator new` / `operator delete` 函数体
### 目的
261 字节的 `__lcxx_override` 节是 libtpu *拥有*的整个主机分配热路径。它包含四个会抛异常的可替换全局 `operator new` operator,libc++ 将它们归组到专用节中(`-fexperimental-library` / google3 `__lcxx_override` 放置),使链接器可以把它们作为一个整体保留或替换。在这个二进制中它们被*保留*,没有被替换,因此转发到 glibc。重新实现者可通过复现这四个函数体,并将其路由到最终链接提供的 `malloc` 来复现堆行为。
### 节布局
```text
ELF section [26] __lcxx_override VA 0x213f0720 size 0x105 (261 B) AX align 32
0x213f0720 operator new(unsigned long) 69 B ── canonical libc++ loop
0x213f0780 operator new[](unsigned long) ── tail-call to op new
0x213f07a0 operator new(unsigned long, std::align_val_t) ── aligned
0x213f0820 operator new[](unsigned long, std::align_val_t) ── alignedcold / nothrow / __hot_cold_t 变体位于 __lcxx_override 之外,在普通 .text 中,并转发到热路径版本:operator new(size_t, __hot_cold_t) @ 0x211646c0(提示被丢弃),operator new(size_t, nothrow_t const&) @ 0x211eb3c0(围绕热路径 op 的 try/catch),以及 TPU 内部 placement operator new(size_t, NamedBufferAlloc const&) @ 0x208b1000。每个 operator delete 都是到 free 的 thunk(例如 operator delete(void*) @ 0x211eb440、operator delete(void*, align_val_t) @ 0x211eb540)。
算法
// operator new(unsigned long) sub_213F0720 (__lcxx_override, 69 B)
// the canonical libc++ __libcpp_operator_new loop
function operator_new(size_t n):
size_t s = n + (n == 0); // bump 0 -> 1 so malloc(0) never returns NULL spuriously
void* p;
while ((p = malloc(s)) == NULL): // call _malloc rel32 -> 0x213f10a0 (PLT) -> malloc@GLIBC_2.2.5
new_handler h = std::get_new_handler();
if (h == NULL): std::__throw_bad_alloc();
h(); // run the installed new-handler, then retry
return p;
// operator new(unsigned long, std::align_val_t) sub_213F07A0 (__lcxx_override)
function operator_new_aligned(size_t n, align_val_t a):
size_t s = n + (n == 0);
size_t al = (a < 9) ? 8 : (size_t)a; // minimum alignment 8 (std::align_val_t < 9 floored to 8)
size_t sz = max(s, round_up(s, al));
void* p;
while ((p = aligned_alloc(al, sz)) == NULL): // PLT aligned_alloc 0x213f1300 -> aligned_alloc@GLIBC_2.16
new_handler h = std::get_new_handler();
if (h == NULL): std::__throw_bad_alloc();
h();
return p;
```text
反汇编关键行是 `0x213f0734 call _malloc ; rel32 -> 0x213f10a0`。*真实* tcmalloc 会消费的 `__hot_cold_t` 热/冷页提示(用于把热分配和冷分配分隔到不同 span)在这里被静默丢弃,因为 glibc 没有这个概念;hot/cold operator 会转发到普通 operator,同时丢弃提示。
### PLT thunk → glibc
```text
0x213f10a0 malloc jmp cs:off_224C2DC8 ── R_X86_64_JUMP_SLOT malloc@GLIBC_2.2.5 + 0
0x213f1300 aligned_alloc jmp through GOT slot ── aligned_alloc@GLIBC_2.16
0x213f1e70 posix_memalign jmp through GOT slot ── posix_memalign@GLIBC_2.2.5
operator delete family 0x211eb440..0x211eb580 -> free@GLIBC_2.2.5动态符号表将每个分配原语标记为普通 FUNC GLOBAL UND 导入:free/malloc/calloc/realloc @ GLIBC_2.2.5,aligned_alloc @ GLIBC_2.16,posix_memalign @ GLIBC_2.2.5,以及 memalign/pvalloc/valloc/reallocarray。因此:每个 C++ new ⇒ malloc/aligned_alloc,每个 delete ⇒ free,全都通过 PLT 路由到 glibc。
特性 — 同一个
posix_memalign@GLIBC_2.2.5thunk(0x213f1e70)也被设备侧主机后备路径直接调用,tpu::PremappedMemoryManager和tpu::AllocateAligned调用posix_memalign,不经过operator new。所以 C++ 堆(经由__lcxx_override)和 DMA staging 池(经由PremappedMemoryManager)最终都落到同一个 glibc 分配器,但入口点不同。它们都不共享 tcmalloc;设备侧主机路径见 overview.md。
3. 惰性无效的垫片表面
目的
垫片是真实代码,也有真实调用者,只是在此构建中运行时什么都不做。重新实现者需要知道运行时调用了哪些 API(以便保留同样调用),并且知道每个 API 都是受弱空值 guard 保护、返回默认值的 thunk。本节列出活动但休眠的表面:MallocExtension、MallocHook、实验格架,以及唯一的软限制开关。
MallocExtension wrapper
编译出了八个 MallocExtension 方法(来自 malloc_extension.cc,0x21164xxx),每个都是其 _Internal provider 之上的弱空值 guard thunk。由于 provider 为 NULL,这些方法在此处运行时都是 no-op。
| Method | VA | Provider(弱 UND) | 此处效果 | 调用者 |
|---|---|---|---|---|
MarkThreadIdle() | 0x21164480 | …_MarkThreadIdle | no-op | abseil 每线程 sem wait;RCU domain thread;exit/liveness watchers |
MarkThreadBusy() | 0x211644a0 | …_MarkThreadBusy | no-op | abseil 每线程 sem wait;RCU domain thread |
SetMemoryLimit(n,kind) | 0x211644c0 | …_SetMemoryLimit | no-op | BarnaCoreManagerBase::Init(LimitKind=0/kSoft) |
GetNumericProperty(sv) | 0x211644e0 | …_GetNumericProperty | 返回 false/unset | InstallSignalHandlers;LloDumper::AddHeapSizeRecord;Thread::Start |
GetAllocatedSize(p) | 0x21164540 | …_GetAllocatedSize | 返回 0 | tsl::port::MallocExtension_GetAllocatedSize(TF stats shim) |
GetProperties() | 0x21164560 | …_GetProperties + GetExperiments | 只填充 experiment map | — |
ProcessBackgroundActions() | 0x211645c0 | …_ProcessBackgroundActions | no-op | MemoryReleaser daemon body |
NeedsProcessBackgroundActions() | 0x211645e0 | (同上) | 返回 false | MemoryReleaser daemon launcher |
标准 wrapper 形态(此处为 SetMemoryLimit)是弱空值 guard 加上 n | -(n==0) 的“0 ⇒ unlimited”惯用法:
// MallocExtension::SetMemoryLimit sub_211644C0
function SetMemoryLimit(size_t n, LimitKind k):
if (&MallocExtension_Internal_SetMemoryLimit != NULL): // weak symbol address test
return MallocExtension_Internal_SetMemoryLimit(n | -(n == 0), k); // 0 -> ULLONG_MAX
return /* default-constructed result, the call is dropped */;
```text
> **陷阱 —** `MemoryReleaser` daemon(`google_init_module_malloc_memory_release_thread` @ `0x213efc00`)是一个 init-module,*本来会*生成名为 `"MemoryReleaser"` 的线程(优先级 `0xE`),循环运行 `ProcessBackgroundActions`,但条件是 `if BackgroundThreadsAllowed() && NeedsProcessBackgroundActions()`。这里 `NeedsProcessBackgroundActions()` 为 false(弱 `NULL`),所以线程从未创建。重新实现者若复制 init-module,必须保留该 guard,否则会生成一个在 no-op 上自旋的后台释放线程。
完全没有编译出的方法(甚至没有 wrapper):`ReleaseMemory` / `ReleasePerCpuMemoryToOS`、`GetStats` / `SnapshotCurrent`、`GetMemoryLimit`、`SetMaxPerCpuCacheSize`、`SetMaxTotalThreadCacheBytes`、`GetRegionFactory`、`ActivateGuardedSampling`、`EnableForkSupport`。两个经典的每线程/每 CPU 缓存大小 setter(`SetMaxTotalThreadCacheBytes`、`SetMaxPerCpuCacheSize`)因此*甚至不可达*,不存在为一个不存在的缓存设定大小的 API。
### MallocHook — 已接线但休眠
`MallocHook` 在 `google_malloc` 中完整编译并可注册:`Add/Remove{New,Delete,SampledNew,SampledDelete}Hook`、`Invoke{…}HookSlow`,以及 `HookList<T>::{Add,Remove}`(8 种函数指针签名实例化)。hook 存储位于 `google_malloc_bss`(`new_hooks_` @ `.data 0x224c2940`,以及 `delete_hooks_` / `sampled_new_hooks_` / `sampled_delete_hooks_` / `hooklist_spinlock_`)。
两个真实消费者会注册 hook:
- `HeapLeakChecker::{BeforeConstructorsLocked, TurnItselfOffLocked}` 注册 `NewHook`(`0x210effa0`)/ `DeleteHook`(`0x210e8fe0`)— perftools 堆泄漏检测器。
- `crash_analysis::reporting::remote_coredumper::MemoryAllocPreventer` 将 `FailOnAlloc`(`0xfccc520`)安装为 `NewHook`,从而在从信号处理器写 crash core 时*禁止*分配(FailOnAlloc aborts)。
> **特性 —** hook 会成功注册,但普通分配时永不触发。在每次 `malloc`/`operator new` 上调用 `InvokeNewHookSlow` 的代码位于缺失的分配器核心中;glibc 的 `malloc` 不调用它。所以堆泄漏检测和采样分配 profiling 都是已接线但休眠。唯一例外是 `malloc_hook` 节 [25](2,206 B),它包装 `mmap`/`mmap64`/`munmap`/`mremap`/`sbrk` 和 abseil `LowLevelAlloc`,这些*页级* hook 仍会看到 `mmap`/`munmap`,因为这些调用经过 libtpu 自己的 thunk,而不是 glibc 的 malloc。因此重新实现者可以免费获得页粒度跟踪,但不能获得逐对象分配跟踪。
### 实验格架 — 仅查询
`SelectExperiments`(`0x0e638b40`)通过 `tcmalloc_internal::thread_safe_getenv` 读取四个环境变量*一次*(CallOnce):`TEST_TARGET`、`BORG_EXPERIMENTS`、`BORG_DISABLE_EXPERIMENTS`、`BORG_PHYSICAL_CELL`;解析逗号分隔的 `enable_`/disable 列表,应用 target-name 启发式,并运行以 cell name 为 key、约 1/7 比例的 CRC32-hash rollout sampler。它填充一张按实验划分的 bool 表,供 `IsExperimentActive` / `FindExperimentByName` / `WalkExperiments` 查询。`tcmalloc::experiments` 表有 18 项(stride 24 B),按名称长度匹配后再 `bcmp`。
> **注意 —** 二进制中的每个 `TCMALLOC_*` 字符串都是这张查询表里的*实验名*,不是 `getenv` key。`TCMALLOC_PGHO_EXPERIMENT`、`TCMALLOC_L3_AWARE_VCPU_V2`、`TEST_ONLY_TCMALLOC_{SPAN_LIFETIME_TRACKING,SHARDED_TRANSFER_CACHE,HEAP_PARTITIONING,MADV_COLD_HUGEPAGE,HUGE_CACHE_RELEASE_30S,POW2_SIZECLASS,ALWAYS_DISCARDING}` 等都是*被查找的名称*,从不从环境读取。这里没有读取 `TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES`、`TCMALLOC_PER_CPU_CACHES`、`TCMALLOC_RELEASE_RATE` 或 `MALLOCSTATS`;这些 getenv key 不存在。实验选择对*分配行为没有影响*,因为会消费这些 bool 的核心缺失;它只服务 `GetExperiments` / `IsExperimentActive` 查询。不过实验格架确实可以给垫片*断代*(见 [§4](#4-version--feature-level))。
### 唯一调优开关
运行时*尝试*设置的唯一 tcmalloc 参数是软内存限制:
```text
FLAGS_barna_core_tcmalloc_desired_usage_limit_bytes (.data 0x222c51a8, type int)
-> platforms_deepsea::jellyfish::barna_core::BarnaCoreManagerBase::Init (0xf977320)
MallocExtension::SetMemoryLimit(limit, /*LimitKind=*/0 = kSoft) -- runtime NO-OP该 flag 由 abseil flag 构造器注册(help text 被剥离为 kStrippedFlagHelp);其静态存储为零初始化,而 wrapper 的 n | -(n==0) 惯用法会把 0 转换为 ULLONG_MAX,所以预期默认值是“除非覆盖,否则无限制”。但该调用在运行时是 no-op,因为 MallocExtension_Internal_SetMemoryLimit 为 NULL。因此即使这唯一的开关在这里也不起作用。(该 flag 编译时是否有非零默认值未追踪,_GLOBAL__sub_I initializer 未解码;LOW。)
4. 版本 / 特性级别 {#4-version--feature-level}
目的
没有保留语义化版本字符串,Google 内部 TCMalloc 没有 semver 标签。垫片只能通过其特性集指纹来断代,这有助于重新实现者选择用哪个上游 tcmalloc 修订版作为真实集成基础,也确认该构建来自 2024/2025 内部流(与 LLVM-trunk / clang 9999.0.0 构建身份一致)。
特性指纹
| 特性证据 | 含义 |
|---|---|
RseqFunction_PerCpuCmpxchg64 / PerCpuCmpxchgCheck64 / PerCpuTryLock / PerCpuReadCycleCounter | rseq 加速的按 CPU 缓存 → 2020+ TCMalloc |
__hot_cold_t operator new overloads | TCMalloc 热/冷页分离(约 2022) |
__alloc_token_{0,1,9}_* instrumentation thunks(malloc/calloc/realloc/memalign/posix_memalign/_Znwm…) | -falloc-token typed-alloc profiling,LLVM/TCMalloc 2024+ |
__size_returning_new[_aligned][_hot_cold] thunks | P0901R10 size-returning operator new(C++26)→ trunk libc++ + TCMalloc 2024/2025 |
实验 TCMALLOC_PGHO_EXPERIMENT | Profile-Guided Heap Optimization,2024+ |
实验 TCMALLOC_L3_AWARE_VCPU_V2 + TEST_ONLY_L3_AWARE | L3-cache-aware vCPU caches v2,2024+ |
结论: 该垫片是 2024/2025 Google 内部 TCMalloc 流的 API/experiment 表面,证据包括 __size_returning_new、__alloc_token、PGHO 和 L3-aware-vcpu-v2。具体修订版字符串没有暴露。由于核心缺失,这里的“版本”是 API/experiment 垫片的版本,而不是运行中分配器的版本。(流/时代判断置信度 HIGH;精确修订版是开放缺口。)
5. 重新实现者实际应使用什么
目的
本页的 brief,即“大小类结构、中央/线程缓存、TPU 专用调优”,在这里的 tcmalloc 内没有答案,因为 tcmalloc 没有运行。本节说明等价行为实际来自哪里,避免重新实现者继续寻找一个不存在的缓存。
每线程 / 每进程主机大小由 glibc 决定
这里没有 tcmalloc ThreadCache,也没有 PerCPUCache。MarkThreadBusy/MarkThreadIdle 调用(在真实 tcmalloc 中会在调用线程空闲时把该线程缓存缩回中央空闲链表)被接到 abseil semaphore wait、RCU domain thread,以及 liveness/exit watcher,但它们都是 no-op。因此实际的每线程和每进程主机堆大小由 glibc 决定:
- 每线程(每 arena)堆由 glibc 自己的
MALLOC_ARENA_MAX/M_ARENA_MAX、mmapthreshold 和 trim threshold 控制,libtpu 都没有配置。想限制每线程 arena 膨胀的部署需要在环境中设置MALLOC_ARENA_MAX;libtpu 没有等价开关。 - 每进程足迹只受(no-op 的)BarnaCore 软限制和操作系统约束;tcmalloc 垫片不提供进程内内存限制执行。
重要字节位于设备分配器,而不是主机堆
TPU 程序实际放置的每个字节,即 HBM tensor、VMEM/CMEM/SMEM/SFLAG operand,都由设备侧分配器管理,不是由主机 malloc 管理:
- 设备 HBM/VMEM/SMEM/CMEM/SFLAG 由
tpu::BestFitAllocator服务(best-fit RB-tree + free 时立即合并),每层一个实例,重放编译期 MSA offset。设备侧与大小类等价的东西是 MSA 的编译期放置,而不是运行时 free-list bin 方案。见 hbm-allocator.md。 - 主机 DMA-staging 使用
tpu::PremappedMemoryManager(N 个 2 的幂分区,轮转,每个分区在 mutex 下包装一个BestFitAllocator)构建于posix_memalign之上,以及tpu::internal::HostBufferPool(每大小类回收缓存,flat_hash_map<size_t, SizedBucket>中的SizedBucket)构建于tpu::AllocateAligned→posix_memalign之上。这个回收池是 libtpu 中最接近大小类缓存的东西,并且它位于主机传输 staging 路径,不在 C++ 堆上。 - Host-RAM spill(MSA 选择卸载的 HBM buffer)是唯一真正的
tsl::BFCAllocator(带合并的 bin-bucketed best-fit,21 个大小类 bin,256 GiB 上限,2 MiB region doubling),仅经由HostOffloadingTpuAllocator到达。
因此设备分配的主机元数据,即 BestFitAllocator 对象本身(200 B,operator new(0xC8))、std::set RB-tree node、absl::flat_hash_map ctrl/slot array、ProgramMemoryMetadata proto、MSA AllocationValue vector、HeapSimulator chunk map 和 Eigen scratch,全都由 operator new / std::aligned_alloc 分配,也就是 glibc malloc。设备 HBM 字节由 BestFit 管理。这两个领域从不共享分配器,tcmalloc 也不参与其中任何一个。设备侧细节由下面这些页面负责。
相关组件
| 组件 | 关系 |
|---|---|
| overview.md | 内存层级图;其中 host-heap 行(“no tcmalloc/jemalloc; PremappedMemoryManager / tsl::BFCAllocator over posix_memalign”)正是本页展开的内容 |
| hbm-allocator.md | 通用 tpu::BestFitAllocator 算法,即管理主机堆不管理的字节的设备分配器 |
| module-init-plugin-discovery.md | Module-init 路径;google_init_* cold init-module(包括 MemoryReleaser launcher 和 RemoveInitialHooksAndCallInitializers)在这里运行 |
交叉引用
- overview.md — 六区域分类;本页展开的 host-heap 行,以及设备侧
posix_memalign路径(PremappedMemoryManager、BFCAllocator) - hbm-allocator.md —
tpu::BestFitAllocator(best-fit + eager coalescing);实际管理 TPU 字节的设备分配器 - vmem-allocator.md — VMEM 层,也是一份
BestFitAllocator实例,而不是 tcmalloc 大小类 - module-init-plugin-discovery.md —
MemoryReleaserdaemon launcher 和 tcmalloc hook initializer 在模块初始化中的排序位置 - back to index — Part X — On-Chip Memory & DMA / Memory tiers