边界字段分配与调用恢复所有权(2026-11-04)
接续 签名 COW 与实例复用。本次只处理调用失败恢复的备份,不更改公开 Item/Origin/FunctionId 类型,不改外层 present 的总节点预算恢复,不引入整树 COW、结果缓存或跨轮复用。
1. 先测字段,而非猜测哪个 clone 最贵
在独立源码副本中,分别标记 bind、present 恢复、调用恢复、capture 的 item_shell,并将七个字段的 clone 放进固定容量记录器。观察器将分配转发给相同的 System allocator,不分配记账容器、不对每次操作计时;普通 A/B 二进制不链接该观察器。
完整 IceXIV 的当前基线:
item_shell 字段 | 分配事件 | 请求字节,十进制 MB |
|---|---|---|
ctor | 413,904 | 14.079 |
instance | 34,800,031 | 288.093 |
target | 8,697,058 | 71.992 |
origin | 41,382,429 | 2,320.072 |
evaluation | 0 | 0 |
fields / attrs | 0 | 0 |
合计 85,293,422 次、2,694.236MB,其中来源占请求字节的 86.11%。fields/attrs 的共享 Dict 已使它们的 clone 不产生新分配。这不等于其它字典构造、插入或修改也没有成本,也不是整进程分配总量。
按复制用途:
场景 | shell 次数 | 分配事件 | 请求 MB |
|---|---|---|---|
bind | 4,272,724 | 13,073,258 | 349.465 |
外层 present 恢复 | 8,389,713 | 51,156,773 | 1,593.834 |
调用失败恢复 | 4,116,476 | 21,046,462 | 750.484 |
capture_item | 5,130 | 16,929 | 0.453 |
这里的事件和请求字节是复制期间的累计流量,不是 live heap、实际复制字节数或 RSS。来源字符串长度取决于本次冻结输入的路径,不把此前另一证据目录的请求字节硬当作本次基线。调用恢复一项中的来源克隆为 674.972MB、实例克隆为 71.992MB、ctor 为 3.520MB。
2. 选择最小的所有权改进
原来的 present_call 已经拥有 Item,却在交给 execute 前另做完整 item_shell,以便 execute 失败时恢复:
拥有原边界 → 复制恢复备份 → 把原边界交给 execute
├─ 成功:丢弃备份
└─ 失败:丢弃原边界,使用备份
现在由调用方保留 Option<Item>,execute 借用这个槽位。参数、实现、返回类型等可能产生 Result 错误的操作全部完成后,才 take 边界并组装成功结果:
拥有原边界 → 暂借 execute
├─ 成功:take 原边界,移入结果
└─ 失败:原边界仍由调用方拥有,直接恢复
因此不需要为每次成功调用提前复制身份和来源,也不需要多包一层 Arc/COW。与上一批相同,优先移走不必要的复制,而非让所有复制都变成引用计数。
这不是对求值器状态做事务回滚:已经发生的逻辑读取、步骤、累计额度与诊断仍按原顺序处理,不退款、不吞掉硬预算。原边界 children 在执行前已移出,失败时本就应清空;公开恢复树仍保留原 ctor、instance、字段、注解、来源、span、level,target 与诊断继续由原 error/error_at 逻辑产生。
所有生产改动及新增回归只在 crates/notist-pipeline/src/code/eval.rs。回归覆盖两个 frontend,检查调用前拒绝、参数求值失败、内容返回类型违约、数据返回契约违约四条路径;特意附加字段、注解、来源扩展和 children,验证失败保留正确边界而非部分执行产物。现有循环、资源读取、深度和内容额度测试继续参与验收。
3. 对照与证据边界
证据目录:/tmp/opencode/notist-boundaries-20261104/。before 是上一批已验收 after 的逐文件相同副本;after 仅覆盖 eval.rs。baseline 的 workspace 默认 733/全特性 742 测试日志从该相同快照沿用,明确不声称又执行了一轮;普通二进制仍在本批新目录重建。
tools/boundary_probe.rs 与 tools/profile_boundaries.py 保留字段/场景观察器及冻结工具;普通 A/B、workspace 验证、四组完整语义 JSON、19 次完整构建输出哈希和独立 RSS 采样沿用 tools/profile_runtime_cow.py 的协议。所有脚本在证据目录保存准确副本,源码、输入和二进制另有指纹。
普通主组先完成初次发布,再每版一次 warmup,随后六轮交替顺序的整进程计时;输出核对在进程结束后执行。字段观察器的 wall 不用于计算加速,RSS 组也不混入主组。不会把这轮加速与上一批加速直接相加。
4. 验收结果
精确归因复核
after 字段观察器中,调用恢复场景的 shell、分配及请求字节全部为零;其它三个场景的每个字段记录均与 before 完全一致。总 shell 从 16,784,043 → 12,667,567,shell 内分配事件从 85,293,422 → 64,246,960,请求字节从 2,694,235,862 → 1,943,751,887。
因此可以确认消除的是预期的那一类备份,而非少处理了 bind、挂载或来源。上述仍仅为 shell 范围的分配归因,不宣称是整个 Rust 进程的分配总量。两版字段探针分别用时 19.239s、21.260s,显然受观察器和环境影响,不用于性能结论。
普通二进制:六轮配对
中位数 | before | after |
|---|---|---|
wall | 14.234s | 13.698s |
user | 12.731s | 12.207s |
system | 1.711s | 1.705s |
两个 wall 中位数少 0.536s。逐轮配对的耗时降幅为 3.31 / 3.65 / 4.16 / 2.68 / 4.02 / 4.15%,中位数 3.84%,六轮均更快;六样本配对中位数的描述性 bootstrap 95% 区间为 3.00–4.16%。
CPU 频率和系统任务没有完全隔离;保留全部样本,正式主组记录 1 次 major fault。after 的 warmup 曾为 16.330s,按预先规定的 warmup 分组保留、不计入主统计,不事后从正式组删除慢样本。
正确性与内存
after workspace 默认 734 项、全特性 743 项通过,74 个 test target,零警告;核心/管线无默认特性 130 项通过。
字段观察器 Rust 单测 1 项、冻结/插桩工具 Python 单测 2 项通过;原通用验收工具单测不改。
21 次全量构建的全部 78,004 个输出文件 SHA-256 一致:普通验收 19 次、字段探针 2 次。四组完整 analysis/evaluation JSON 前后相同;消费者自己的详情/目录/本地 HTML 链接检查通过。
两次独立 Rust PID RSS 峰值:before 6.4891 / 6.4889 GiB,after 6.4912 / 6.4912 GiB,基本不变。消除短命备份没有改变最终树规模或全站保留策略,不宣称降低峰值内存。
源文件、输入、普通及探针二进制、脚本最终指纹复核。仅清理本批外部 Cargo target 的可重建产物;源码快照、二进制、完整输入/输出、测试和观察日志保留。所有生产修改仍未提交。
后续
最大的剩余复制仍是外层 present 的预算恢复。它必须保存改写之前的字段、target、来源与错误顺序,不能简单拿执行后的节点充当原边界。本次不连带修改它;随后再单变量评估廉价 checkpoint,或根据剩余字段成本评估共享来源/身份。全站树生命周期仍由另一专项处理。