跟进 main 后的跨页面求值并行复核
结论
跟进 main 后,跨页面批量求值并行仍然可行,且最新提交的两轮测量重复了收益。 4 线程整站配对变化中位数为 -14.89%/-14.64%;8 线程为 -18.35%/-18.16%。两种配置各十组比较全部更快。不是沿用旧提交的百分比。
需要修正之前对两线程的判断:当前提交五组比较全部更快,整站配对中位数为 -5.46%,不再是旧实验中没有整站收益的状态。但这不是对所有站点或单页编辑的承诺。
不建议因“平均每页只有几十微秒”放弃整站批量并行;每阶段仅创建少量线程,开销能够摊薄,checked 输入无需深复制。也不建议将现有原型直接默认启用:共享预算和跨页扩展顺序合同尚未解决,接口仍有临时 Send 约束。
若继续工程化,在当前大型站点上,8 线程适合优先降低延迟,4 线程适合减少相对的 CPU 增量;小批次/单页仍应有串行路径。没有测量 16/32 线程,也没有证明 128 页是最佳批大小。
最终基线上的首轮数据
模式 | 整站 wall 中位数 | 求值阶段中位数 | 峰值 RSS 中位数 | 整站配对变化 |
|---|---|---|---|---|
未修改 main | 7.678 s | 未加计时 | 2699.7 MiB | 基准 |
候选原串行路径 | 7.674 s | 2.355 s | 2700.1 MiB | -0.09% |
适配器单线程,无工作线程 | 7.906 s | 2.628 s | 2710.8 MiB | +2.91% |
2 线程 | 7.259 s | 1.704 s | 2910.7 MiB | -5.46% |
4 线程 | 6.535 s | 1.159 s | 2860.0 MiB | -14.89% |
8 线程 | 6.257 s | 0.947 s | 2740.1 MiB | -18.35% |
候选原串行相对未修改 main 的五组配对变化在 -0.62%~+0.50%,中位数 -0.09%,控制项接近不变。2/4/8 线程各五组全部更快;适配器单线程五组全部更慢。这直接区分了接入路径成本与创建线程成本:不开工作线程也会产生额外成本。
同一二进制的直接配对补测
配对实验 | 串行整站中位数 | 并行整站中位数 | 整站配对变化中位数 | 求值阶段配对变化中位数 |
|---|---|---|---|---|
4 线程 | 7.634 s | 6.519 s | -14.64% | -51.14% |
8 线程 | 7.654 s | 6.264 s | -18.16% | -59.35% |
4 线程组的求值阶段中位数为 2.347 → 1.145 s;8 线程组为 2.357 → 0.956 s。后者按配对比值约为串行的 2.46 倍速度,不是八倍加速;整站约为 1.22 倍速度。
逐次 user + system 后取中位数,4 线程由 7.908 → 9.450 CPU s,增加 19.51%;8 线程由 7.926 → 10.345 CPU s,增加 30.52%。峰值 RSS 对应增加 160.1/40.6 MiB。内存变化不随线程数单调,不能将它解释成固定的每线程开销或推导“线程越少越省内存”。
逐次 wall 减本次求值阶段时间后再取中位数,4 线程组为 5.284 → 5.372 s,8 线程组为 5.289 → 5.311 s。这不是精确的析构或 IPC 计时,也不是可被求值并行消除的部分。当前求值以外仍有约 5.3 s 工作,因此即使继续加核,整站收益也不能随求值阶段线性增长。
创建与数据搬运成本
空任务 scope 创建并 join 2/4/8 个线程的本机中位数为 20.661/33.541/59.572 µs,p95 为 25.540/45.251/88.422 µs。实际创建成本会随环境和任务变化,但这里每阶段只付一次少量线程的成本,不是每页付一次;相比秒级阶段收益不是主要障碍。
没有新增整份 AST/DOM/输出树的序列化或深复制。已有 clone、分配及引用计数仍存在,借用和 Arc 共享也不是零成本;本轮没有独立量化锁等待、跨核引用计数或分配器争用,不能声称已确定并行扩展性的主要瓶颈。
验证
最终候选默认全工作区 99 个测试套件,918 项通过,0 失败、0 忽略。原有三项并行回归继续覆盖冷缓存、数据错误/来源、顺序、依赖、句柄隔离及逐页预算恢复。
最终基线共 56 次成功完整构建,每次 78,004 项产物、451,812,631 bytes,其中 51,739 个 HTML 页面,长度与 SHA-256 全部一致;输出清单也与 3d17013 的清单完全一致。没有将中间基线的慢样本删去后混入当前统计。
比较范围
最终基线固定为 f6cca8d2de7cdf417cc823ef09e9f4542a6bb0f9。实验分支 perf/eval-batch-parallel 已合入该提交;没有向 main 合并实验代码。本轮重新构建 main 基线和候选,不能把之前 49a643a 上的百分比当作当前结果。
main 中与本轮有关的改动包括 checked 调用参数绑定、统一内容发射、Empty/One/Many 内容表示、comptime where 与泛型 Path 链接,以及 build-scoped SSG 扩展事件。随后加入的 LSP 分析快照及导航重构也包含在最终基线内。
并行方案保持不变:页内串行、每批 128 页、最多 8 个 scoped 工作线程;共享资源与数据缓存通过 Mutex 适配器访问,不复制整个 Vault,不通过进程 IPC 传递 AST 或结果树。最终归并顺序与页面顺序一致。此次只解决 main 的阶段顺序冲突并格式化接入代码,没有增加新的求值优化。
站点与协议适配
同一份冻结 icexiv-db 文档、JSON、模板和原页面生成函数,51,739 页。旧站点生成脚本使用单请求扩展协议;独立夹具增加 eval_parallel_events.py,将 before_compile 交给原 extend 函数处理,继续执行原导出/catalog 校验,返回相同的生成文档。catalog 从原 data/catalog.json 读取。
适配器按当前主机合同接收 before_template 的完整请求后返回无修改响应,不跳过该事件、不省略页面/HTML 的序列化或 Python 解析。原生成器没有模板阶段修改,因此此处不添加新的模板行为。主仓库与实际 consumer 未修改;两侧使用相同适配器。
需要区分两种数据传递:本实验工作线程共享借用的 checked 输入,并转移输出容器所有权;SSG 命令扩展则确实会经 JSON Lines/子进程传递数据。后者包含当前 Page.html,是公共串行阶段,不能算作工作线程复制开销,也不能用本次求值并行消除。
方法
冻结源码后测试与构建,baseline/candidate 各有独立 Cargo target。release、debug=1、incremental=0、locked、offline、编译 jobs=2。
全工作区默认 feature 测试,test-threads=2;没有声称全 feature 或其他平台回归。
正式测量不与本会话编译/测试重叠。未修改系统设置,未控制其他程序、OS cache 或 CPU 频率。
首轮包含未修改 main、候选原串行路径、适配器单线程、2/4/8 线程;各预热,再进行五轮轮换/反转顺序。
补测使用同一候选可执行文件,原串行路径与 4/8 线程分别五组直接配对,交替先后顺序。
每次完整构建在计时外校验产物路径、长度、SHA-256。保留所有慢样本;整站变化采用逐对 wall 比值的中位数,不冒充两组中位数相除。
求值阶段计时包括文档装配及收尾;wall 包括完整构建、扩展和子进程生命周期。user/system CPU 和 maxrss 是进程资源指标,不是求值独占成本。
空任务线程测量独立执行,各配置先预热 20 次,再保留 500 次 scope 创建/join,不与整站计时混跑。
原型仍有的限制
ResourceSession/DataCache 的共享预算和缓存未复制为线程私有副本,但并行请求交错会改变共享预算耗尽时的页面归属。输出扩展的跨页可观察行为也需要单独定义;Send + Sync 只保证线程安全,不保证业务顺序不变。
当前普通 SSG 泛型 API 临时增加 Send 约束,并行阶段还会延长全部 Analysis 与输出树的重叠持有期。适配器单线程控制项用于观察这类接入开销,不应将所有额外时间都归咎于线程创建。
因此实验仍是显式启用:未设置 NOTIST_EVAL_WORKERS 走原串行路径;设为 1 使用适配器但不创建工作线程;2/4/8 使用对应工作线程数。保留独立分支,不默认启用、不直接作为生产合入方案。
中间基线与证据
先对启动时 main 3d17013 完成一次独立实验。911 项默认工作区测试通过,56 次成功整站构建逐次产物一致,但首轮 wall 出现 41.537~141.158 s 的异常慢值;候选原串行控制相对未修改基线的配对中位数甚至为 -13.10%。该轮不适合给出稳定的整站加速幅度。
3d17013 的八线程补测虽为 -17.77%,仍不替代后续 f6cca8d 的独立测量。原始样本全部保留于 /tmp/opencode/notist-eval-parallel-3d17013,不删除或合并进最终提交的数据集。首次测试因为独立源码副本缺少 target 目录导致两项 CLI 测试创建临时目录失败;补齐目录后完整重跑,没有修改测试或跳过用例。
最终证据目录:/tmp/opencode/notist-eval-parallel-f6cca8d。baseline-source、candidate-source、candidate-source-hashes.json 和 candidate.patch 保留源码;bin、binaries.json 保留被测二进制及摘要;inputs.json、output-inventory.json 保留完整输入/输出清单;samples.json、summary.json、confirmation-samples.json、confirmation-summary.json 与 logs 保存全部样本和日志。tools/profile_eval_parallel.py 的 event-protocol 准备选项和 tools/thread_scope_cost.rs 提供复现入口。