三组优化合并后的整站热点评估:01649b4
结论
冻结提交 01649b4,对照 9a3c5f7。求值分配、路径复用、遍历游标三组优化合并后:
五组交错对照全部更快,配对 wall 变化中位数为 -8.57%;两组 wall 中位数为 8.254/7.555 秒。
malloc 减少 29,764,482 次,即 22.42%;realloc 减少 4,644,120 次,即 31.54%。
三种成功分配 API 调用合计减少 34,408,602 次,即 23.33%。
峰值 RSS 基本不变,仍约 2.70 GiB。减少累计分配不等于减少峰值存活内存。
当前第一过程热点仍是求值,第二是正文处理;对象构造/释放仍是主要操作成本,但已不是原先那些明显冗余构造。
下一步应优先研究“内容计划到最终节点之间的中间容器与重复物化”,其次是宿主有序路径索引的剩余重复工作。不应继续把已经修复的空 native 作用域、临时节点壳、字段键 String 中转、浅树遍历堆栈和路径逐分量扩容列为待修问题。本轮仅测量、分析和新增报告,没有修改生产代码或实施新的优化。
方法与证据
当前提交:01649b4;基线:9a3c5f7bdaeaa8eca31b6160f140a8928704322c。
同一冻结站点:51,739 页,78,004 项产物,共 451,812,631 字节。
git archive 冻结源码;普通 release、frame-pointer、coarse、detail 各用独立 Cargo target,编译并行度 2。
复制独立站点输入和输出目录,不修改 consumer 或旧实验目录。
当前版本 16 次完整构建:首次发布、五次普通计时、两次 frame-pointer 控制、三次 coarse、两次 detail、两次 CPU、一次分配。
另做新旧各一次对照预热、五组交错普通计时,以及一次基线分配诊断;共 29 次完整构建。
每次计时结束后核对所有产物长度及 SHA-256,29 次全部与冻结基准一致;最终再验证源码、二进制、输入和输出。
普通 A/B 均无探针,复用已验证的基线二进制;每对交替运行顺序,同一独立站点路径和已有产物条件。
CPU 为 499 Hz 的父进程用户空间调用栈采样,两次共 7,530 个样本,全部含目标二进制帧。不是包含扩展子进程的 wall 归因,也不是精确 off-CPU 分解。
分配包装器调用原 glibc 实现,仅统计指定父进程成功 malloc/calloc/realloc,不统计扩展子进程及未拦截入口。当前概率分配栈 89,606 条,记录丢失 0;1 条未找到应用帧,落在标准库线程栈 guard 初始化。
Python profiling 工具回归 71 项通过。没有在本轮重新运行全部 Rust 功能回归;生产变更的各组回归记录见相应日期日志。
证据目录:/tmp/opencode/notist-hotspots-01649b4-20261109。主要文件:manifest.json、paired-samples.json、paired-summary.json、samples.json、phase-summary.json、operation-summary.json、refresh-operations.json、allocation-focused.json、allocation-stacks.attribution.json,以及原始 CPU profile、分配二进制、符号缓存和 logs。工具副本在 reproducer;其中 analyze_refresh.py 补充新旧遍历表示的匹配和分配器调用上下文统计,不覆盖原始分析结果。
新旧交错对照:总体收益
指标,中位数 | 基线 9a3c5f7 | 当前 01649b4 |
|---|---|---|
wall | 8.254 s | 7.555 s |
user CPU | 7.545 s | 6.859 s |
system CPU | 0.961 s | 0.956 s |
maxrss | 2,832,736 KiB | 2,831,784 KiB |
五对 wall 变化:-8.50%、-8.62%、-8.08%、-8.86%、-8.57%。报告配对比值中位数 -8.57%,不将其与两组中位数之比混称;这只是本机该完整站点的一轮对照,不是跨机器置信保证。各优化的单项实验百分比不能相加,本轮只能归因于三组优化的合并效果。
当前版本单独五次普通计时为 12.244、11.337、7.579、8.140、7.858 秒,中位数 8.140 秒,全部保留。它们在编译后早期有明显波动,部分慢运行出现 major fault,但没有足够证据把全部差异归给缺页或 I/O。因此不直接拿上轮 8.407 秒与这组 8.140 秒作收益结论,而使用随后更稳定的新旧交错对照。
首轮向空产物目录发布为 10.384 秒,不纳入上述统计,也不是严格清空系统缓存后的冷启动实验。其余普通构建有完整产物,仍重新执行整站分析和求值,发布主要核对产物,不是编辑后的增量构建。
过程热点:现在在哪个阶段耗时
coarse 三次中位数;比例分母为同组 cli.total 的 7.517 秒,不与普通对照分母混算。
过程 | wall | 占阶段总时间 |
|---|---|---|
文档求值 | 2.686 s | 35.7% |
正文链接/嵌入、HTML 及阶段回收 | 1.638 s | 21.8% |
文档分析及宿主环境装配 | 0.917 s | 12.2% |
资源准备、复制及模板加载 | 0.746 s | 9.9% |
发布 | 0.440 s | 5.9% |
扩展与路由 | 0.296 s | 3.9% |
公共模板上下文 | 0.139 s | 1.8% |
最终页面模板阶段 | 0.069 s | 0.9% |
求值与正文仍占约 57.5%,没有出现新的单一主导阶段。上轮记录中的求值/正文约为 3.071/1.886 秒,本次为 2.686/1.638 秒,方向与总体改善一致;这些阶段不是新旧交错探针实验,不能将差值当作精确的逐项优化归因。发布中的 unchanged 比较约 0.386 秒;扩展进程执行及等待约 0.201 秒。表未列全装配、目录和返回边界;各项中位数也不保证严格相加。
coarse 外部 wall 中位数为 7.587 秒,detail 为 9.819 秒;detail 明显扰动求值,不用它给真实构建分配精确秒数。frame-pointer 两次为 8.245/7.612 秒,也有波动,因此不声称已精确测出探针的固定开销。
操作热点:剩余 CPU 成本
self 仅计最内层执行位置;inclusive 含被调用工作。各类别相互包含,不能相加,也不能直接换算为可消除的 wall 秒数。
操作/调用路径 | 两次 CPU 样本比例 |
|---|---|
glibc 分配/释放/扩容自身 | 26.2~26.9% self |
文档求值 run | 35.6~36.1% inclusive |
present 内容物化 | 33.6~33.8% inclusive |
execute | 23.8~24.2% inclusive |
expr | 12.9~13.8% inclusive |
std::path::compare_components | 9.10~9.17% inclusive |
HTML render_with_diagnostics | 7.6~7.7% inclusive |
Dict::insert | 2.8~3.0% inclusive |
新遍历器及旧入口合并识别 | 2.5~3.0% inclusive |
implementation 查找 | 2.2~2.6% inclusive |
memcpy/memmove 自身 | 2.4~2.6% self |
HTML escape::write | 约 1.9% inclusive |
来源引用校验 | 约 1.7% inclusive |
实例/编译期值 key | 1.2~1.3% inclusive |
分配器占比仍高,不表示分配优化无效
总体执行时间和分配次数都下降后,分配器在更小总量中的比例仍可保持较高。历史采样的分配器 self 为 27.0~28.2%,当前为 26.2~26.9%;不能只看百分比得出“没优化成功”。当前分配器 self 的调用上下文按互斥规则划分:
求值 run 栈内:占全部 CPU 样本约 10.0~10.2%。
HTML renderer 栈内:约 2.6~3.0%。
其余上下文:约 13.2~14.1%,包括宿主构建/资源/发布及不在前两者栈内的对象销毁。
这三项仅划分分配器叶子样本,不能把其余全部称为最终析构,也不能把 26% 都归给 evaluator。
路径查找改善,但宿主比较仍有成本
当前约 9.1% 的路径比较主要仍来自 Vault::ensure_environment、Assets::insert、Files::inspect、ResourceSession::register_source、资源准备和 publication::unchanged。DocumentIndex::find 不再出现在两次路径比较的主要调用者列表,符合精确索引改为 HashMap 的改动。这不表示文档索引零成本:哈希与相等判断仍执行,只是不再走原来的有序路径比较热点。剩余 ordered map 有输出顺序和冲突检测职责,不能将所有容器机械改为 HashMap。
遍历并没有消失,也没有因为换名而漏统计
原工具只识别 Node::descendants,当前主要迭代工作进入 Descendants::next。补充分析同时匹配新 next 实现和旧入口/闭包,只识别真实函数帧,不把泛型参数提及该类型算进去;当前仍约 2.5~3.0% CPU。历史采样为约 3.6~4.4%,是不同时间的诊断对照,不能承诺精确的绝对耗时下降比例。已解决浅树堆栈分配,不等于消除了多次扫描本身。
原热点是否被消除:分配证据
当前和重新运行的基线均使用 frame-pointer 二进制、相同包装器及已有完整产物条件。分配探针的耗时不进入普通 A/B。
成功 API 调用,父进程精确计数 | 基线 | 当前 | 减少 |
|---|---|---|---|
malloc | 132,729,790 | 102,965,308 | 22.42% |
calloc | 37 | 37 | 0 |
realloc | 14,726,628 | 10,082,508 | 31.54% |
合计 | 147,456,455 | 113,047,853 | 23.33% |
概率调用栈中的对应证据:
共享 Scope 的 malloc 栈样本:上轮 2,960 条,本轮 37 条。native 空作用域已经不再是百万级分配来源。
遍历路径的 malloc 栈样本:上轮 2,268 条,本轮 42 条;realloc 样本由 4,652 条变为本次未采到。
normalize 的 realloc 样本:上轮 10,944 条,本轮未采到;malloc 样本仍有 398 条,规范化并非变成零分配。
以上调用栈比较使用上轮保留的基线概率样本,本轮另跑的基线 API 总数与上轮完全一致。“未采到”不是一般性证明所有输入都不会分配;调用栈事件按概率采集,不能当作精确逐点计数。
还在大量创建什么
malloc 采样率 1/2048、realloc 为 1/256。下表按逆概率估计最近应用分配点,主要说明规模,不表示各组对象全部无用:
当前最近应用 malloc 点 | 次数估计 |
|---|---|
Shared<Vec>::new | 1,022 万 |
Shared<Vec>::new | 857 万 |
Dict::default | 677 万 |
present | 639 万 |
Text::from(&str) | 578 万 |
Dict::insert<&String> | 433 万 |
Content::one | 408 万 |
Shared::new | 396 万 |
Lexer::lex_code_string | 361 万 |
Content::one、present 和共享 Piece 存储仍为百万级规模,意味着原优化移除了额外包装,但内容计划、挂载结果与最终树的多层存储仍有成本。字段键借用优化消除了 String 中转,不意味着最终 Text、字段 entries 和 COW 的成本也消失。仅凭 Shared<Vec>::new 的分配栈不能断言容器为空;需要追加长度/所有权分布才能决定是否采用更紧凑表示。词法字符串等也有很多小分配,但数量多不自动意味着应抢在求值通路之前重构 parser。
调用次数未变:execute/implementation 各 6,073,286 次,present 8,440,426 次,bind_content 2,213,972 次,data.native 1,955,784 次。实际 data.parse 565 次、parse_hit 51,173 次、模块编译 1 次、bind_cached 3 次。这些优化主要减少每次操作的成本,没有减少逻辑执行次数;现有数据与模块缓存仍然有效。
下一轮优先级
1. 研究内容物化的中间容器,而不是继续修已消除的冗余
crates/notist-eval/src/eval.rs:
Content::one 仍创建单元素 Vec,再进入共享存储。
native Construct 仍先形成可复用内容计划,随后 present_call 调用 present 获得临时 Vec,最后 extend 到调用者输出。
Element 路径虽已不再先造空壳,但仍有计划、子结果与最终共享 children 的表示转换。
最值得先补的是这些容器的空/单元素/多元素分布、唯一所有权比例和调用链分配归因。然后尝试局部减少确定不需要的临时结果,或让最终发射自然写入调用者工作空间,而不是一开始就引入全局池/缓存或全面改容器。这一方向有阶段、CPU、分配三方面证据,但 Content 具有可复用计划语义,不能缓存挂载结果或合并重复执行。必须保留实参及错误顺序、预算/回滚、来源链、词法捕获和重复挂载的独立执行。
2. 针对剩余宿主路径查找,缩小重复工作范围
src/vault.rs 的环境定位、src/publication/mod.rs 的有序输出冲突检查、资源快照与文件检查仍反复比较路径。下一步先测同一规范路径的重复查询次数与调用方已有句柄,寻找能自然复用已有结果的局部边界。DocumentIndex 已经处理,不必继续围绕它重复优化;不建议直接扩大成全局路径 ID、授权缓存或跳过 unchanged 核对。
3. 正文阶段的遍历与 HTML 缓冲区,可做独立小实验
crates/notist-ssg/src/build.rs 仍分别扫描链接和嵌入;遍历存储优化没有合并扫描职责。crates/notist-html/src/escape.rs 的 write 逐字符 push,采样仍可见它下方的 allocator 工作;可以检查安全地批量追加未转义片段及输出容量策略。两者都必须保持顺序、Unicode、HTML 转义及链接/嵌入检查,先做局部 A/B,不把正文阶段全部剩余时间都称为扫描或转义。
暂不优先
模块/data.parse 再加缓存:未命中工作量仍很低。
Symbol/FieldId/特化键全面重构:实例键约 1.2~1.3% CPU,不是最大头。
来源校验省略:约 1.7% CPU,不值得以语义和安全换收益。
最终模板并行优化:当前阶段约 0.9%,收益上限有限。
更换 allocator 或并行化求值:不是没有潜力,但本轮证据更支持先降低表示转换和小对象 churn。
内存目标:若关注 2.70 GiB 峰值,应单独测整批分析结果、文档树、资源快照和产物字节的存活边界;不能靠分配计数推断泄漏或节省量。
限制与复现
仅覆盖这一站点的 CLI 完整构建,没有覆盖 serve 编辑、LSP/Worker、不同树形/站点规模或严格冷缓存。未取得精确 off-CPU、磁盘系统调用延迟或硬件缓存 miss 分解;statx 的 CPU 样本不是磁盘等待时间。本轮 CPU 基线沿用上轮原始采样,普通 release 总体收益则是本轮新旧交错测量,二者证据强度不同。
使用 tools/profile_hotspots.py 的 prepare、build、measure、cpu、allocations、verify,传新的 --out;prepare 后将 site 符号链接替换为不含 dist 的独立输入副本。分析使用 tools/analyze_hotspots.py,以及证据目录 reproducer/analyze_refresh.py --out 当前目录 --baseline 上轮目录。普通交错对照通过 tools/profile_source_cost.py 的 run,在两侧预热后五对交替运行 baseline/normal;重新执行基线分配时使用已验证的基线 frame-pointer 二进制和相同包装器。所有输入、源码和二进制摘要见 manifest.json,不覆盖已有日志和测量文件。