Notist
On this page
  1. BindingId 完整候选的 RSS 增长归因
  2. 状态
  3. 先区分几个不同问题
  4. 探针方法与可解释边界
  5. 保留布局的 pages 探针
  6. 保留原始请求量的 tags 探针
  7. trim 诊断
  8. 请求堆增长出现在哪里
  9. 存活归属:不是签名或数字 ID 占掉几 MiB
  10. 布局扩张会传播到宿主容器
  11. 分配器如何把请求量放大为 RSS
  12. 单变量验证与下一步
  13. 结论与建议顺序

BindingId 完整候选的 RSS 增长归因

状态

分阶段分配器、存活归属、单变量副本和结构数量普查均已完成。本次只分析,不修改已验收的生产实现;所有变体都是外部目录中的诊断副本,不自动提交。

基准是完整验收的两份冻结源码与各自二进制,目录/tmp/opencode/notist-binding-identity-complete-20261108-v2。新证据位于/tmp/opencode/notist-binding-rss-20261108-v2,每个探针/变体隔离 target,保留源码清单、二进制哈希、stderr、完整结果和汇总,不覆盖上一轮证据。

先区分几个不同问题

  • 正常构建原八组样本的峰值 RSS 中位数之差是 +8.62 MiB;配对差额中位数为 +8.87 MiB,每对均增加,范围 +7.08~+10.27 MiB。不能只解释为一次采样噪声。

  • 精确 Rust 请求堆峰值增加 4,288,910 字节(4.09 MiB),不等于 RSS 的 +8.62 MiB。

  • 累计普通分配次数减少约 212 万,并不意味着同一时刻的存活对象、分配器取整后的空间或实际驻留页会减少。

  • CLI 局部析构后的请求堆差额仅 16,369 字节。峰值增量和退出前 allocator 保留空闲页,都不能直接称为新的长期内存泄漏。

探针方法与可解释边界

保留布局的 pages 探针

System allocator 仍按原始 Layout 调用 alloc / alloc_zeroed / realloc / dealloc。额外统计 Rust 活跃请求字节与 malloc_usable_size;阶段边界读取 mallinfo2 以及/proc/self/status 的 RSS、匿名页、文件页和 HWM。before / after 交错执行三对。

这套探针不加分配头,不改变业务对象布局;但新增指令、统计与二进制页仍有扰动,只用来解释阶段与分配器构成,不拿它替换正常构建的 clean RSS 数据。

保留原始请求量的 tags 探针

每个 Rust 分配额外存一个归属头,记录创建它的源码身份、空绑定索引、绑定声明、签名、作用域池、运行时 Scope 等逻辑区域。dealloc 在别的区域发生时仍归属原创建者,realloc也保留原拥有者。按原始 Layout 统计存活请求字节与分配数,不把探针头计入业务请求量。

它改变了实际分配布局,不能用于解释其自身 RSS。它给的是存活请求归属,不是原程序的页映射或完整调用栈。对齐、清零、扩缩容、内容保留和跨区域释放由独立 Rust 测试检查;两种模式均通过。

trim 诊断

另各跑一次 pages 变体,在每个阶段采样后调用 malloc_trim(0),观察哪些驻留空闲页可归还系统。它改变后续页面复用,不是正常构建样本,不把该结果当作优化收益或建议直接在生产阶段强制 trim。

请求堆增长出现在哪里

原独立账本的阶段差额:

阶段

after − before 请求堆

分析/求值结束

+4,288,558 B

正文渲染前

+4,288,558 B

正文渲染/释放文档后

+867,238 B

返回完整输出前

+867,238 B

CLI 局部析构后

+16,369 B

新增量在输出正文与模板前已存在,并非输出字节增加。文档释放后大部分消失,剩余部分随批次宿主环境等继续保留,最后随 CLI 局部对象释放。

存活归属:不是签名或数字 ID 占掉几 MiB

tags 探针在分析/求值结束时的额外存活请求:

归属

额外存活字节

MiB

每个来源的绑定索引容器

2,483,520

2.368

来源身份本身扩张

413,920

0.395

未按上述区域单独标记的宿主/容器等

1,350,557

1.288

绑定声明与地址索引条目

10,642

0.010

已解析签名

9,571

0.009

成功作用域弱规范化池

809

0.001

检查特化的其余差额

11,499

0.011

Registry 安装的其余差额

7,696

0.007

空 Bindings 快照

240

<0.001

该次诊断的总差额与独立账本只相差几十字节,属于探针自身临时输出差异;不声称与正常构建逐字节相同。上述逻辑区域不等同于严格的语义类型,“其余差额”的数量拆分见下节。

最大项来源明确:当时有 51,740 个存活的来源身份,每个都新分配一个Arc<Mutex>,其请求布局为 48 字节,故 51,740 × 48 = 2,483,520。Identity 新增 Arc 字段又使每份请求增加 8 字节,故 51,740 × 8 = 413,920。两者合计 2.763 MiB,占约 4.09 MiB 增量的 67.6%。这是急切分配空容器及其拥有关系的固定成本,不是局部变量数目变多,更不是给每个 ID 分配了一个对象。

另一个最初怀疑项被否定:空 Bindings 默认快照虽然在此前累计分配 155,219 次,合计约 5.92 MiB 请求,但该阶段只剩 6 份、240 字节;多数是短命对象,不能拿累计量当作本轮峰值的主要原因。运行时 Scope 的归属也在该阶段归零。

布局扩张会传播到宿主容器

用两份已构建探针的 rlib 单独编译布局探针,实测:

类型

before

after

Sources

32 B

40 B

CodeProgram

72 B

80 B

Context

256 B

264 B

Environment

288 B

296 B

ModuleScope

56 B

64 B

Analysis

360 B

376 B

CheckedProgram

168 B

176 B

Evaluation

184 B

192 B

InstanceRecord

288 B

312 B

与 SSG Document 同字段的结构

424 B

432 B

Source 保持 8 B,TypedExpr 保持 136 B,Invocation 保持 144 B。这些都是本机布局,不是稳定 ABI;实际 Document 的数量与布局由完整构建中的 census 进一步确认。

Sources 多出的绑定拥有快照字段会传播到按值持有它的外层结构。若大 Vec 元素长 8 B,付费的是 capacity × 8,而非仅 len × 8;宿主 Arc 或 BTree 节点也有自己的取整。完整构建的普查给出:51,739 份文档、51,739 份 Environment、3 份缓存依赖图;DocumentVec 的容量为 65,536,实际 Document 为 424 → 432 B,与独立形状探针一致。

每份文档环境装配都会经 Environment::for_document → Context::with_scope 新建文档根 ModuleScope。新增的 pooled 标志虽只有 bool,但让整个 ModuleScope 从 56 B 变成64 B,且未入池的文档根作用域也付费。因此“池里只多 809 B”不能覆盖这部分旁路成本。这里的根作用域数量按普查的环境数量和该一对一装配路径核算,不伪称独立逐对象枚举。

由数量、实测布局和已记录的分配归属可核对如下账目:

来源

核算

额外请求

急切来源绑定索引容器

51,740 × 48 B

2,483,520 B

Identity 新增指针

51,740 × 8 B

413,920 B

Environment 内 Context 扩张

51,739 × 8 B

413,912 B

文档根 ModuleScope 的 pooled 布局开销

51,739 × 8 B

413,912 B

Document Vec 元素扩张

capacity 65,536 × 8 B

524,288 B

上述合计

4,249,552 B

相对阶段实测 4,288,558 B 的净余量

39,006 B

这些项解释了阶段请求增量的约 99.1%。余量是签名、声明索引、池元数据、检查记录等小项以及旧字符串/容器减少的净效应,不再有一个未解释的 MiB 级“签名内存”。实例记录虽然每份也大 24 B,但本工作负载没有足够多的存活实例使它成为主要项。

分配器如何把请求量放大为 RSS

三对 pages 探针在正文渲染前的中位数差额:

统计口径

增量

Rust 活跃请求

+4.090 MiB

Rust malloc_usable_size 总量

+6.353 MiB

glibc arena 内在用块

+6.241 MiB

glibc mmap 区域

+0.500 MiB

glibc arena 内空闲块

+0.001 MiB

匿名 RSS

+6.641 MiB

文件 RSS

+0.527 MiB

总 RSS

+7.168 MiB

请求到 usable 的差额额外扩大约 2.26 MiB,说明块内取整/未收缩余量很重要。这个数包含尺寸类和 realloc 后可能保留的可用空间,探针没有将其逐分配点拆开,不能全部精确归于某一种小块尺寸类。glibc 在用块加mmap 的差额约 6.74 MiB,其中还含 chunk 管理等;不能把它们与 usable 重复相加。此阶段空闲块差额几乎为零,所以“最初全是碎片”也不成立。

正文渲染后,Rust 活跃请求只多 0.827 MiB,但 glibc 空闲块多约 5.01 MiB;RSS 差额仍约 6.65 MiB。释放业务对象不等于这些页立即从 RSS 消失,allocator 保留页会延续高水位。这与前面在用块的尺寸类放大是两个阶段的问题。

CLI 局部析构后,本组探针的 RSS 中位数差额甚至约 49.6 MiB,而 Rust 请求差额仅约16 KiB,glibc arena 空闲量差额约 59.7 MiB。它反映该退出前时刻的 allocator 布局/归还差异,不是“峰值多了 49.6 MiB”,也不是多保留了相同大小的绑定对象;正常 CLI随即退出,由系统释放。不能由此推断长期服务的泄漏或稳定驻留量。

trim 实验显示两版在正文渲染后都有约 1 GiB 量级驻留空闲页可以回收。它解释本站整体高 RSS 的重要组成,但不能把这一大块全算作 BindingId 的退化。trim 后 after 反而可能低于 before,说明页布局与可回收性会改变;不能据此宣称新版本业务对象更少。

正常构建的 +8.62 MiB 与阶段探针的 +7.17 MiB 不来自同一组可执行文件/采样点,不能硬拼成精确的逐字节等式。可靠结论是:确有约 4.09 MiB 业务请求增量,小块取整和 chunk 管理将其放大,后续空闲页驻留使 RSS 不随对象释放同步回落;其余还有文件页、采样与地址布局变化。没有用 RSS 减请求量就全称为“碎片”。

单变量验证与下一步

两个顺序相反的诊断组,各跑原 after-pages、共享空 Bindings、按需创建来源绑定索引、两者组合;以下是相对同批原候选的中位数差额,不是新的 clean 性能验收:

单变量/组合

请求堆峰值变化

返回输出前 RSS 变化

共享不可变空 Bindings,写入仍 COW

−80 B

+0.70 MiB

来源索引改为 OnceLock 按需创建

−2,069,552 B(−1.974 MiB)

−2.61 MiB

两者组合

−2,069,632 B

−2.61 MiB

第二个实验具有精确的对象成本解释:未触及的索引不再分配 48 B,但 OnceLock<Arc<…>>比原 Arc 字段大 8 B;净效果为 (51,740 − 1) × 48 − 51,740 × 8 = 2,069,552 B,与观测一致。这也说明本批存活来源中实际上只有 1 个需要创建声明索引。它不是把拥有者丢掉或改成永久全局名字池,也没有缩减输出或提前销毁业务对象。

共享空快照只减少 80 B 峰值,RSS 反而略高。它可能减少短命分配,但本轮没有据此量化 CPU 收益,更不能把它当作 RSS 主修复。两样本的 RSS 数字只提供方向证据,不宣称 0.70 或 2.61 MiB 是跨机器稳定常数。

结论与建议顺序

  1. 优先考虑来源绑定索引的按需创建:因果证据最直接,避免绝大多数空容器;但本实验仅诊断副本,不代表已经修改或验收生产方案。应保留并发创建、弱键回收、函数拥有者和跨快照相等语义,再重新完整测试与 clean A/B。

  2. 其次审视 Sources 增加的字段如何在大量文档/Environment 中传播,以及 pooled 标志给所有未入池 ModuleScope 增加的 padding。避免为省这 8 B 反过来让每个普通作用域析构都争抢全局池锁,也不撤掉并发共享与回收保证。

  3. 共享空 Bindings 可作为单独的短命分配实验,不用它代替峰值修复。不为几个 KiB 的ResolvedSignature 放弃内部权威表示,也不回退到字符串查找。

  4. allocator trim 只解释页驻留,不作为这轮内核修复,更不将输出驻留/SSG 生命周期专项混入。正常构建多 8.62 MiB,不等于绑定对象多 8.62 MiB。

本轮 6 次普通阶段探针、2 次 trim、2 次归属探针、8 次消融、2 次数量普查,共 20 次完整站点构建,逐次核对 78,004 项/451,812,631 字节输出,全部一致。正常版本的既有八组样本与精确账本也仍保留。没有修改生产源码、consumer、锁文件或默认预算,没有重启 CPU 采样服务器,没有把诊断耗时当作新的性能结论。

复现工具为 tools/profile_binding_rss.py、tools/binding_rss_probe.rs 与tools/binding_rss_layout_probe.rs;探针和插桩辅助的新增 2 项测试及工具总套件均通过。证据中的 summary.json、results.json、ablation-results.json 和 logs/ 保留原始数据。