Notist
On this page
  1. 文档求值专项拆解:01649b4
  2. 结论
  3. 第一层:求值过程边界
  4. 第二层:递归内部的互斥 CPU 分解
  5. execute 内部还在干什么
  6. 第三层:换一个角度,CPU 在执行哪些操作
  7. 为什么会累计成这么多操作
  8. 绝大多数调用不是用户 Code 函数
  9. 大多数容器只是空或单元素
  10. 已有实现查询并没有反复编译
  11. 对应源码和最值得验证的方向
  12. 1. 立即挂载 native 构造:检查能否少走一层内容计划
  13. 2. 空/单元素表示和写入调用者输出
  14. 3. 数据 native 的参数和结果通路
  15. 暂不优先
  16. 方法、扰动与复现

文档求值专项拆解: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 子目录,未覆盖有效证据。