文档求值专项拆解:01649b4
结论
这里的“求值约 2.69 秒”是 51,739 页整站的累计阶段时间,平均约每页 52 微秒,不是单篇文档执行了 2.69 秒。该站点最终产生 4,219,447 个节点,执行 6,073,286 次函数调用和 8,440,426 次 present。平均每页约 82 个最终节点、117 次调用、163 次 present;大量很便宜的小操作叠加,形成主要耗时。
专项证据支持:主要成本是通用调用和内容表示的构造/复制/销毁,而不是复杂算术、Code 函数体、模块编译或 JSON 反复解析。
粗粒度边界:约 94.3% 的 run 时间在顶层 present 内部,包括其所有递归与子调用,不是 present 自身占 94%。
按互斥 CPU 职责归因:execute 本地工作约 37.5%,内容挂载/发射本地工作约 24.4%。
换成互斥叶子操作归因:分配器 28.8%、销毁/引用计数释放 13.5%、clone 11.2%、memcpy/memmove 2.3%,合计约 55.7%。
调用种类:native 内容构造 66.9%,数据 API 32.2%,Code 0.85%。
present 的内容片段输入 25.6% 为空、63.4% 为单元素;通用容器路径被大量用于极小结果。
最优先研究“立即挂载的 native 构造是否需要先包装成 Content,再分配临时输出 Vec”,其次是空/单元素内容表示和数据调用实参字典的成本。本轮没有实施优化,没有修改生产代码。
第一层:求值过程边界
在独立冻结副本中,仅对每个文档 run 的几个边界计时;不对百万级递归调用逐次读时钟。下表取计数关闭的三次诊断运行中位数,计时有扰动,不是普通 release 的精确子项。
run 内边界 | 诊断 wall | 约占 run |
|---|---|---|
来源视图装配、合并和来源引用校验 | 0.1192 s | 4.34% |
Evaluator 初始化 | 0.0041 s | 0.15% |
首次 bind_content | 0.0098 s | 0.36% |
顶层 present,含全部递归和调用 | 2.5907 s | 94.25% |
结果装配、收尾及未单列探针边界 | 约 0.0250 s | 0.91% |
run 合计 | 2.7487 s | 100% |
各项中位数不保证严格相加;最后一项是每次 run.total 减其已计时子段的余量再取中位数,不等同于纯析构时间。run 还处于上层 pipeline/宿主求值调用内部,因此也不要求与此前 pipeline.evaluate 的 2.686 秒完全相等。
首次 bind 只处理文档的初始内容计划,后续 bind_content 会在 present 触发 Code、插值、子内容等路径中继续发生。不能由“首次 bind 只有 10 ms”推断全部绑定工作只有 10 ms。
第二层:递归内部的互斥 CPU 分解
六次使用无计数探针的 frame-pointer 可执行文件采样,499 Hz,文档求值 run 内共 7,978 个样本。每条栈只归给最内层的一个 Evaluator 方法;它调用的标准库、分配器和其他辅助函数归入该方法,但更深层的 Evaluator 方法不重复计入。不在任何 Evaluator 方法下的 run 装配/校验/收尾单列。这一口径可以相加,不是原来的 inclusive 函数排行榜。
互斥职责 | 占求值 CPU | 每次构建采样折算 CPU |
|---|---|---|
execute 本地:调用准备、实参字段、native 构造、局部销毁等 | 37.5% | 约 1.000 s |
present_call/present_piece/present 的本地挂载与发射 | 24.4% | 约 0.649 s |
bind_content 本地:绑定内容计划 | 7.8% | 约 0.209 s |
expr 本地:表达式分派、读取和结果处理 | 6.9% | 约 0.184 s |
implementation:已有实现查询、键、缓存等 | 6.7% | 约 0.178 s |
read_data/data_native/retain_data_result | 8.4% | 约 0.224 s |
run 入口、校验、装配、收尾及初始化 | 5.6% | 约 0.150 s |
repeat、call_children、tick、target 等其余方法 | 2.7% | 约 0.071 s |
合计 | 100% | 约 2.665 s |
“采样折算 CPU”是样本数乘采样周期、再除六次运行,不是各子项的独立 wall 计时,更不是承诺可消除的时间。其总量与约 2.69 秒的阶段耗时接近,但不能用它们混合求差或精确推算 I/O。小项的采样方差较大;例如 6.9% 的 expr 是排除更深调用后的本地成本,不能说完整表达式求值只有这么多。
execute 内部还在干什么
对归属 execute 的样本,再按其最内层调用帧的 inline 源码位置粗分;仍排除已归给 expr、implementation、data_native 等方法的工作。
execute 源码区域 | 占全部求值 CPU | 采样折算 CPU/次 |
|---|---|---|
native Construct:构造节点与 Content 计划 | 16.2% | 约 0.433 s |
实参循环、参数匹配、值校验与字段字典装配,不含更深 expr | 12.2% | 约 0.324 s |
target/registry 准备、实例参数复制、初始字段对象等 | 6.1% | 约 0.162 s |
函数退出位置的清理 | 1.3% | 约 0.034 s |
返回值类型校验与错误传播 | 0.7% | 约 0.019 s |
其余区域及未定位 | 约 1.0% | 约 0.027 s |
这些行只细分上表的 execute 37.5%,不能再加到它上面。优化后的机器码、内联和自动析构会影响行号归属,尤其“退出位置”不涵盖全部销毁;这不是逐语句的精确秒表。但它已足以区分:重点不是 Code 调用深度检查或返回类型检查,而是构造路径和通用实参装配。
第三层:换一个角度,CPU 在执行哪些操作
下面与上一节是同一批求值样本的另一种分类。每条栈只按最内层执行位置分一类,因此表内可相加;两张表不能互相相加。
叶子操作 | 求值 CPU | 采样折算 CPU/次 |
|---|---|---|
glibc malloc/free/realloc 等自身 | 28.8% | 约 0.766 s |
drop/drop glue/引用计数释放自身,不含已经归入 allocator 的样本 | 13.5% | 约 0.359 s |
clone 自身,包括共享引用克隆,不含其下更深 allocator/memcpy | 11.2% | 约 0.298 s |
memcpy/memmove 自身 | 2.3% | 约 0.061 s |
其他执行位置 | 44.3% | 约 1.180 s |
约 55.7% 的求值 CPU 可直接落在内存和对象生命周期操作上。clone 不都意味着深复制,很多是 Arc/共享容器引用计数;反过来,其他执行位置也包含字段查找、指针操作、预算和结构检查,并不全是“有效业务计算”。这不是说这 55.7% 都能消除,也不是说需要更换 allocator。优先减少没有必要存在的中间表示,比仅让同样的分配更快更贴近当前证据。
为什么会累计成这么多操作
独立开启计数的两次运行,所有调用计数、函数名分组和容器分布完全一致。该计数覆盖 CLI 中的 evaluator 入口,包括可能的编译期 constant 调用;计时和 CPU 分析则严格限定文档 run。计数不用于给单次调用估算真实延迟。
绝大多数调用不是用户 Code 函数
实际解析后的实现种类 | 调用次数 | 占 execute |
|---|---|---|
Native Construct | 4,064,738 | 66.93% |
Native Data | 1,955,784 | 32.20% |
Code | 51,738 | 0.85% |
Native Str | 1,026 | 0.02% |
按不带模块名的函数名统计,前几项为 text 2,058,753、data_expect 977,892、data_require_index 922,050、paragraph 770,940、strong 512,763。不带模块名的统计仅用于展示这份工作负载,不是新的函数身份规则。
因此,优先优化一般 Code 函数体或算术指令,触及不了主要调用量。数据 API 的 195 万次也不是解析 195 万个 JSON:此前实际解析只有 565 次。大量工作是取值、decode/require、Result 包装/解包,以及它们共同经过的调用、字段和类型检查流程。
大多数容器只是空或单元素
分布 | 空 | 单元素 | 2~4 | 5~16 | 17+ |
|---|---|---|---|---|---|
present 输入 pieces,8,440,426 次 | 25.61% | 63.43% | 10.34% | 0.63% | 0 |
present 输出节点数,同样次数 | 25.61% | 62.82% | 10.94% | 0.63% | 0.01% |
execute 实参数,6,073,286 次 | 29.63% | 52.60% | 17.77% | 0 | 0 |
Content::one 被调用 4,065,251 次,其中 native Construct 占 4,064,738 次,另有 513 次 for 内容包装。present 中实际处理 Call 4,116,476 次、Element 4,219,447 次;后者与最终节点数量一致。这不是“最终树上需要存 844 万个不同节点”,而是同一内容在计划、调用结果、发射阶段经过多层表示和递归。
已有实现查询并没有反复编译
implementation 的快速命中为 5,293,620 次,快速未命中 779,666 次,命中率约 87.16%。未命中后仍可从 CheckedProgram 找到已有实例;没有采到需要 specialize 的计数,诊断中该分支执行次数为 0。因此它的约 6.7% 求值 CPU 是查询、键和引用操作成本,不应被解释为反复重新编译。
对应源码和最值得验证的方向
1. 立即挂载 native 构造:检查能否少走一层内容计划
crates/notist-eval/src/eval.rs 的现有路径:
present_call
→ execute:求实参、组字段、找实现
→ native Construct:构造 Node 元数据
→ Content::one:Vec<Piece> + 共享存储
→ present
→ present_piece(Element):求子内容、构造最终 Node
→ 返回临时 Vec<Node>
→ out.extend
这条路径既有 406 万次频次,又对应最多的 execute 本地 CPU。最有针对性的实验是:只在“当前就要挂载”的 native Construct 通路,复用相同的实参/错误处理,直接进入最终发射,减少临时计划和临时输出。不是让所有 Content 变成已求值树,更不是缓存挂载结果;Code 返回的第一类 Content 仍需保留延迟、共享和重复独立挂载语义。
2. 空/单元素表示和写入调用者输出
Content::one、Content::default、present 的局部 Vec,以及 Shared<Vec> 是相关检查点。89.0% 的 present 输入为空或单元素,为局部 empty/single 表示或自然复用输出工作空间提供了明确的分布依据。仍需检查唯一所有权、COW、预算代理和回滚边界,不能只把 Vec 换成另一个容器就宣称一定加速。尤其不能为省临时 Vec,破坏超预算时只回滚当前输出后缀的行为。
3. 数据 native 的参数和结果通路
execute 约 29.6% 的调用无实参、52.6% 仅一个实参,但仍走通用字段字典和参数匹配流程。data_require_index 与 data_expect 合计约 190 万次;可检查是否存在可共享的 checked 调用布局/借用参数路径,减少临时 Dict 和包装的无用构造。不能直接跳过接受类型校验、数据工作预算、Result 错误传播或来源定位,也不建议再扩大 JSON parse 缓存。
暂不优先
来源视图校验约 4.3% 边界时间、Evaluator 初始化约 0.15%、真实 Code 调用不到 1% 的调用量。它们不是零成本,但不是当前第一刀;尤其不能通过省略来源与预算校验换性能。
方法、扰动与复现
提交保持 01649b417e98f0eccc9cf782151cc2b45127162e。
独立输入与产物副本,同一 51,739 页站点;15 次完整构建,78,004 项产物逐次长度和 SHA-256 全部一致。
一个诊断冻结副本:边界计时始终开启,热路径计数通过环境变量开启。只在外部副本改代码,Cargo target 独立、编译 jobs=2。
三对普通/计数关闭诊断控制,普通 wall 中位数 8.044 秒,诊断 7.824 秒,但普通有一次 9.628 秒的波动,不能声称探针加速或无开销。
user CPU 中位数为 6.865/6.956 秒,诊断高约 1.3%;只说明有扰动,不建立固定开销模型。
计数关闭时 run 中位数 2.749 秒;计数开启的一次为 3.871 秒。计数开启的计时不用于真实热点秒数。
六次 CPU 采样全部使用原来的、未加入本轮诊断修改的 frame-pointer 二进制,避免把计数器/直方图开销当成求值成本。
CPU 分类每条栈互斥计数,操作视图与职责视图分别总计 100%;优化后源码行归因只用于粗分区域。
本轮没有做候选优化 A/B,不承诺上述方向的收益,也没有测单篇编辑、serve、LSP/Worker 或其他站点。
证据根目录:/tmp/opencode/notist-eval-focus-01649b4-20261109。run.py 生成独立诊断副本、构建、控制、计数和 CPU 采样;analyze.py 生成互斥归因。evidence/focus-summary.json、control-samples.json、count-samples.json、cpu-samples.json、原始 .prof、inline 符号缓存、完整日志、manifest.json 与输出清单均保留。首次准备因文本锚点同时匹配测试代码而停止,未执行构建或测量;修正后使用新的 evidence 子目录,未覆盖有效证据。