内容树遍历游标栈:实现与验证
结果
基线提交:9a3c5f7bdaeaa8eca31b6160f140a8928704322c。分支:perf/descendants-iterator-stack。仅改变 Node::descendants 的遍历状态表示,没有合并调用方的扫描职责或修改树结构。
工作区测试 847 项通过,无忽略项。
51,739 页站点,18 次完整构建,全部 78,004 项输出的长度与 SHA-256 均与冻结基准一致。
7 组交错 A/B 全部略快,配对 wall 变化的中位数为 -0.87%;不代表所有站点或交互操作的收益。
独立父进程分配探针记录 malloc/calloc/realloc 成功调用合计减少 5,716,272 次,约 3.88%。
峰值 RSS 基本不变;减少累计分配调用不等于降低存活内存。
实现
crates/notist-core/src/item.rs:
保留先根、从左到右的深度优先顺序,返回原节点引用,根节点包含在内。
当前层使用借用的孩子切片迭代器,不再一次把所有兄弟压入节点栈。
遇到有孩子的节点,仅当父游标还有兄弟待访问时才保存父游标;单孩子链不需要保存祖先游标。
待恢复游标使用 SmallVec,内联 8 项;超出容量后使用堆,不改成递归,不限制可遍历深度。
构造及 next 路径标记 inline,让跨 crate 的调用方可以内联和消除不必要的状态搬运。
对外方法签名、find、来源、字段、预算和节点身份语义不变。
空间由待恢复的分支深度决定,不随同一层的兄弟数量增长。内联容量没有宣称经过全局调优。smallvec 原已存在于 Cargo.lock;本次增加 notist-core 的直接依赖,没有升级其版本。
回归覆盖
crates/notist-core/src/item.rs 的 traversal_tests:
混合树的先根顺序与引用身份,以及 find 的包含根/找不到行为。
4,096 个兄弟的宽树保持顺序,游标栈不溢出内联存储。
深度 4,096 的分支树触发堆回退,遍历本身无递归;测试清理也采用迭代方式,避免混入节点递归销毁问题。
单节点只访问一次,多次调用耗尽迭代器保持 None。
crates/notist-core/tests/descendants_alloc.rs:
独立测试二进制以线程局部开关记录 System 分配 API,仅测遍历,不包含树构造和销毁。
单节点、4,096 个兄弟的浅树、嵌套链均为零分配;深度 128 的单孩子链也为零分配。
深度 128 的分支树正常触发堆分配并访问所有节点。
全工作区命令:cargo test --workspace --locked --offline -j2 -- --test-threads=2。测试使用 CARGO_INCREMENTAL=0、CARGO_PROFILE_DEV_DEBUG=0、CARGO_PROFILE_TEST_DEBUG=0,以限制磁盘使用。磁盘不足导致的链接失败及冻结副本缺少 target 临时目录导致的失败日志均保留;补建空目录后完整重跑成功,没有跳过失败测试。
整站测量
基线使用既有冻结普通 release 二进制,候选由本次冻结源码、独立 Cargo target 构建。二者均使用 release、debug=1、incremental=0、jobs=2;计时版本无分配探针。独立站点副本,前后各预热一次;随后 7 对测量,每对交替运行顺序。输出核对在计时结束后执行。普通测量已有输出目录,发布主要比较产物,而非每轮首次写出站点;没有清空操作系统缓存。
指标,中位数 | 基线 | 游标实现 |
|---|---|---|
wall | 8.3724 s | 8.3257 s |
user CPU | 7.6654 s | 7.5709 s |
system CPU | 1.0243 s | 1.0218 s |
maxrss | 2,832,800 KiB | 2,833,416 KiB |
配对 wall 变化依次为 -1.022%、-1.091%、-1.215%、-0.541%、-0.869%、-0.795%、-0.061%。“配对比值中位数”和“两组中位数之比”是不同统计量,不混称。样本来自同一台机器的一轮局部实验,没有做独立机器重复。
独立分配诊断各运行一次,不纳入 wall 对比。包装器调用原 glibc allocator,只对指定 Rust 父进程启用,不统计扩展子进程及未拦截的对齐分配入口。
成功 API 调用 | 基线 | 游标实现 |
|---|---|---|
malloc + calloc + realloc | 147,456,455 | 141,740,183 |
其中 realloc | 14,726,628 | 13,541,263 |
这些是本次进程调用总数,不是存活对象数,也不表示每次 realloc 都物理复制;多线程通路的调用总量可能随运行有所波动。
探索记录与证据
最初“每层游标全部压栈”的候选同样减少分配,但整站配对中位数约慢 2.57%,没有以此作为最终实现。独立微探针支持继续减少游标维护工作;其合成树结果不替代整站验收,也不用于承诺各树形的加速。
最终证据目录:/tmp/opencode/notist-descendants-cursors-20261109。包含 run.py、manifest.json、sources/after、bin、logs、samples.json、allocations.json、summary.json 及全部输出清单。run.py 是这次已有冻结实验的续跑脚本,主动拒绝覆盖已记录的二进制或计时数据;不是对同一目录可反复执行的通用基准入口。初始候选及失败日志:/tmp/opencode/notist-descendants-20261109。基线二进制与证据:/tmp/opencode/notist-hotspots-9a3c5f7-20261109。
未测增量编辑、LSP/Worker 或其他站点;没有声称消除了所有树遍历分配,也没有实施重复扫描合并。