Notist
On this page
  1. IR、求值管线与 book/site 工具链评估
  2. 结论
  3. 1. 证据与测量边界
  4. 2. 当前架构哪些是好的
  5. 2.1 已经建立的边界值得保留
  6. 2.2 当前并非“只有一个 Item IR”
  7. 3. 语言运行时:主要结构性问题
  8. 3.1 捕获仍然通过递归代换实现
  9. 3.2 静态已知的调用仍走动态协议
  10. 3.3 内容值与计算描述应当分清
  11. 3.4 效果模型决定优化是否合法
  12. 4. Item:保留语义树,不必保留同一种物理表示
  13. 4.1 统一的语义不意味着统一的 struct
  14. 4.2 重新审视“单一权威树”
  15. 5. book/site:这是另一个独立的大瓶颈
  16. 5.1 SSG 全站阶段屏障造成峰值
  17. 5.2 451.8 MB 输出决定必须改变发布产物契约
  18. 5.3 site 模板接口有真实的全局依赖
  19. 5.4 book 还有额外的规模问题
  20. 6. serve:watch 正确不等于增量构建
  21. 7. LLVM 类比:应学的是优化合法性的基础
  22. 8. 性能上限与目标可行性
  23. 8.1 没有一个脱离契约的“最快秒数”
  24. 8.2 3s 与 300MB 是两种不同判断
  25. 9. 建议的架构实验次序
  26. 10. 最终判断

IR、求值管线与 book/site 工具链评估

结论

尚未接近这类系统的性能上限;已经接近的是,在保持当前对象模型与全站批处理方式不变的前提下,靠小分配优化获得大幅收益的边界。

建议以“具有一等内容值和显式宿主能力的函数式语言”重新组织计算模型,以“增量构建系统”重新组织 book/site。保留统一内容语义,但不要要求程序、闭包、挂载结果、调试协议和发布产物共享同一种物理树。

LLVM 类比在“前端汇合、语义明确的 IR、可验证的 lowering、后端分离”上成立;不能由 LLVM 生成的程序运行快,推导出一套保留完整文档身份的解释求值器应当同样快。更合适的组合是:函数式语言编译器/运行时 + 内容语义 IR + 构建图 + 事务发布器。不建议直接把整套内容语义降成 LLVM IR,也不建议先建设 JIT。

本次是当前工作树的只读源码评估,新增本文,不修改生产实现,不启动新的优化实验。性能数字引用已归档测量,没有在本次重新运行性能基准;源码事实、历史实测与待验证设计分别陈述。

1. 证据与测量边界

主要历史证据:

  • docs/_logs/2026-11-05-ten-rounds.md:独立空输出首次发布,最终同批 wall 中位数 11.5157 → 10.6997s;最大内核 RSS 4,844,646,400 → 3,572,097,024 B。十轮只有 r01/r04 接受。

  • docs/_logs/2026-11-05-icexiv-refresh-profile.md:与旧版重新编译对照,已有完整输出时 CLI 中位数 14.856 → 9.606s;Rust-only 采样峰值约 3.339 GiB。它不是上一条的首次发布,也不是同口径 RSS,不能混算。

  • 同一 refresh 的独立 coarse 诊断:求值约 40.7%、正文/链接/消费树约 27.5%、资产及模板加载约 12.1%、分析约 9.3%;这不是普通版耗时的精确拆分。

  • 同一 refresh 的独立分配观察器:约 1.985 亿次成功分配/重分配,约 15.05 GB requested 流量。不是存活堆,也没有分配栈,不能把全部流量归因给某个函数。

  • 最终树结构诊断:51,739 页、4,273,237 个节点,Item 192 B;children 容量约 813 MB。旧分析中的 Item 344 B、未共享签名等描述不能当作当前事实。

  • 完整输出 78,004 文件、451,812,631 B,其中 HTML 118,097,122 B。消费者内容、字段与资产必须全部保留。

本次核对的源码重点包括 core 的 Item/TypedExpr/Invocation,pipeline 的 analyze/evaluate、bind/capture/present/execute、实例缓存,Vault 输入批次与环境,HTML renderer,book 的准备与导航,SSG 的批处理与模板上下文,以及 CLI 的 serve/发布。也阅读了 icexiv-db 的 views.notc 和页面生成脚本;未修改消费者。

2. 当前架构哪些是好的

2.1 已经建立的边界值得保留

  • 多前端在 Lower 后汇合,共用 Resolve/Shape/Materialize 与类型检查;不是每个语法各自实现语义。

  • Pipeline 接收显式输入与 Context,本身不做文件系统 IO;宿主提供资源、授权、缓存与发布。

  • 函数有规范身份、签名、编译期实例与同签名目标映射,已有构建专门化执行路径的基础。

  • 内容调用、插值边界、注解、来源、错误恢复有明确语义,不只是“最后输出一段 HTML”。

  • 资源根、读取能力与发布挂载分离;Data 保留 backing 身份和预算,避免无意识复制全量 JSON。

  • 模块编译/绑定、Data parse、实例查找已有缓存;共享文本、共享签名、来源链 COW、PresentCheckpoint 与小字典优化已经存在。

  • 发布采用先完整准备、再替换的思路,失败保留旧站点;serve 的旧版本保留也是正确性资产。

这些不是应当推倒的包袱。重构要针对不合适的表示与调度,不是撤销已经明确的语言语义和安全边界。

2.2 当前并非“只有一个 Item IR”

事实上已有 Expr、RExpr、TypedExpr、Item、CheckedProgram 等表示。问题是阶段职责没有充分体现在运行时表示上:TypedExpr::Content 又嵌入 Vec,Item.pending 又携带 TypedExpr/Invocation。程序、内容配方与求值结果相互嵌套。

因此,改名为“函数式 IR”没有帮助;需要改变运行时的数据流、所有权和已知信息的利用方式。

3. 语言运行时:主要结构性问题

3.1 捕获仍然通过递归代换实现

当前 crates/notist-pipeline/src/code/eval.rs:

  • Runtime::Content 是 Type + Arc<Vec>,Scope 是 BTreeMap<String, Runtime>。

  • bind_content 遍历森林,构造新的 Item shell 和 pending。

  • capture_expr 递归复制表达式;捕获内容输入时克隆森林;if 的两支即使不会都执行,也可能都参与捕获复制。

  • Block/match/for 的部分路径克隆 Scope。

  • present 再执行已绑定内容,构造挂载结果。来源展开仍有逐后代访问,SourceChain 的改善没有取消这次遍历。

建议:共享 checked code,运行时只捕获自由变量环境。

概念上:ContentRecipe = CodeId + CaptureEnv;Mount = Recipe + mount-local 状态。不同挂载共享代码和不可变配方,但分别执行、分别计费、分别产生来源和结果。捕获成本目标应与自由变量数相关,而不是与整棵模板大小相关;实际收益仍须原型验证。

这不要求给用户语言引入一等函数、lambda 或 Haskell 式全局惰性求值。

3.2 静态已知的调用仍走动态协议

execute 当前仍为每次调用建立 target,查询 registry,按参数名查签名、生成 fields,Code 调用另建 inputs,再分派实现;原生实现又按名称读取 fields。

实例缓存已经减少了 key/record 查找,不能再次把“加实例缓存”当成新方向。但它没有消除每次调用的参数容器、名字协议和临时结果。

推荐的执行布局:

  • Context/编译单元局部的 FunctionId/InstanceId;外部仍投影为完整规范身份。

  • 词法变量解析为 frame slot,捕获使用确定的环境布局。

  • checked call 保存参数槽位、optional forwarding、具体返回类型和实际实现的分派信息。

  • 原生数据函数接受位置参数,不先构造可丢弃的公共 Dict。

  • 只有内容节点、查询、扩展或序列化确实需要 fields 时才形成语义字段;可观察的字段不能省略。

替换目标、Context 和版本是执行布局身份的一部分。不能根据源码拼写把名叫 text/data_require 的函数直接当 builtin,也不能误用另一份 Context 的实例编号。

3.3 内容值与计算描述应当分清

当前语义是严格数据求值与延迟、可重放内容计算的混合:未挂载内容不执行其 opaque 调用,同一内容两次挂载可发生两次逻辑读取;严格外层 let 又可以只读一次。

因此不能把所有内容改为“第一次求值后共享结果”的 thunk。应在语义模型中分别描述:

  1. 已有不可变内容值;

  2. 捕获环境的待执行内容配方;

  3. 每次挂载的动态实例;

  4. 完成后的内容结果。

这些概念可以先只存在于内部,不必马上变成新的用户语法。

3.4 效果模型决定优化是否合法

固定资源视图上的确定性,不等于只由实参决定的纯函数。读取能力、依赖、Pending、诊断、逻辑预算和挂载来源也是求值的一部分。

建议内部为操作/实例建立保守效果摘要:纯数据、资源读取、内容构造、挂载计算、可能失败以及必要的预算事件。未知扩展视为屏障;可先从 builtin 开始,不必立即做公开 effect 类型系统。

有了这些事实,才有依据实施:纯数据公共子表达式消除、死绑定删除、已知调用内联、部分求值、构造融合。不能消除未使用但原本严格执行的 read/panic,也不能把两个挂载合并成一个。

预算尤其需要单独决策:目前 tick 与解释路径耦合,优化会改变物理步骤。兼容路线是保留逻辑 charge 点及失败顺序,必要时让优化布局携带计费证据;新语言版本也可以重定义预算,但不能把改变额度伪装成实现加速。最坏情况下,精确重放 charge 本身会限制融合收益,这是实际约束,不是闭包转换能自动解决的。

4. Item:保留语义树,不必保留同一种物理表示

4.1 统一的语义不意味着统一的 struct

Item 同时承载构造类型、pending、checked identity、resolved target、evaluation、origin、字段、children、attrs 和 span。完成的文本叶子也为其它状态保留布局;Vec 则是递归拥有结构,根部 Arc 不会令逐层子树复制都廉价。

在当前物理表示下,427 万个 Item 的内联部分约 820 MB,尚未包含外部字典、身份、来源与字符串。因而“所有节点全驻留且所有节点仍是 192 B 的 Item”与 300 MB 目标不相容。这是当前表示的下界,不是内容语言的下界。

建议区分:

  • 程序表示:绑定、类型、控制流、效果、调用与内容构造操作。

  • 内容表示:元素/挂载身份、字段、注解、children 与来源引用。

  • 公开视图:完整 Item/wire/query 投影,保持约定的顺序和信息。

紧凑内容存储可以采用 NodeId、按节点种类的 payload、共享身份/来源表、稀疏冷元数据、按页分区。这里不预选 unsafe、自定义 GC 或某种 arena;需先验证布局、遍历、Drop、可变扩展与查询投影成本。

4.2 重新审视“单一权威树”

现有测试 evaluation_reads_the_supplied_item_tree_without_a_parallel_representation 要求修改传入 Item 后,求值读取的是修改后的树。这是防止运行陈旧程序的正确要求,但不必推导为“永远不能有派生执行表示”。

两个可选路线:

  • 兼容路线:每次从当前 Item 派生执行布局;只在输入、Context、扩展计划和版本匹配时复用,否则重新派生/走通用路径。

  • 新 API 路线:不可变 ProgramRevision,经编辑事务产生新 revision;执行计划明确属于某个 revision,公开 Item 是对应的内容/调试视图。直接可变访问需要迁移或显式失效。

不能在公开字段仍能任意修改时,用未经校验的隐藏缓存绕过变化。另一方面,重新哈希/重新 lowering 整棵树也有成本,不应把“加版本号”描述为免费解决方案。

可重新讨论的是 API 与物理表示政策;必须持续明确的是身份、注解、挂载、来源、错误和资源授权语义。 并不是所有现有实现契约都应当被永久冻结为语言公理。

5. book/site:这是另一个独立的大瓶颈

5.1 SSG 全站阶段屏障造成峰值

crates/notist-ssg/src/build.rs 当前流程是:发现与扩展 → 保存全部 analysis → 求值全部文档并保存 Document → 建立目标索引 → 逐页 HTML → 保存全部 Page.html → 模板 → 全量 BuildOutput.files。

已经改进的是 HTML 循环通过 into_iter 消费 Document;但在开始 HTML 之前,所有求值树仍然同时存活。

需要全局 routes/anchors,不代表必须保留全站完整 Item 树。可考虑:先固定输入视图,逐页求值并投影 PageFacts(路由、标题、属性、锚点、出链、所需组件等),完成跨页校验,再从按页内存块或私有临时产物取得内容进行渲染。

不能简单重复求值两遍来凑“两阶段”,否则读取、预算、hooks 和成本都可能变化。方案必须明确选择:保留紧凑结果、外存结果、可修补的链接中间产物,或在新契约下重新执行。也不能在分析生成文档之前读取到过时磁盘文本;现有先固定全部生成源码的正确性需求仍要保持。

5.2 451.8 MB 输出决定必须改变发布产物契约

book 和 site 的 BuildOutput 都是 BTreeMap<PathBuf, Vec>,Assets::copy 读取完整资源并保留字节。只要这些完整拥有字节在内存中,300,000,000 B 的应用常驻内存目标就不能靠求值优化达到;外加树、模板与缓存后只会更高。严格 RSS 是操作系统指标,不能将该 payload 下界机械等同于某次 RSS;但它已说明当前“全量字节驻留”API 不适合该预算。

建议新增 ArtifactStore/PublicationPlan:输出条目指向拥有版本的 blob/私有暂存文件,字节按有界块生产,完整验证后才提交。内存 BuildOutput 可以保留为小项目兼容适配器,不应继续是唯一核心 API。

流式不等于一边构建一边覆盖用户站点。暂存空间归本次构建所有;出现后续错误便丢弃暂存,旧发布仍可使用。静态资源引用也必须绑定经过授权与校验的准确版本,不能只保存一个可能变化的源路径。磁盘占用、page cache 与 cgroup memory 需另行报告,不能把内存压力藏到另一种指标里。

5.3 site 模板接口有真实的全局依赖

site.pages 中当前包含完整 Page,Page 又含 html。现有模板可读取其它页正文;因此不能宣称“只留标题即可完全兼容”。

可选方式是保留完整语义但让正文由有界缓存/外存提供,或者引入新接口把 site.pages 的摘要与显式 page-body 查询分开。后者是工具接口迁移,需要明示,不是内部等价优化。任意模板若枚举全站正文,依赖闭包本来就可能是全站。

当前共享 Tera Value 已减少页面/目录重复深拷贝,refresh 中模板渲染仅约 0.7%,不建议先换模板引擎。收益重点是生命周期与依赖范围,不是模板语法。

5.4 book 还有额外的规模问题

crates/notist-book/src/lib.rs 通过 prepared.iter 渲染,整个 prepared 及其树活到后面;ids 又借用了 prepared 的 outline。可以先拥有小型目录/锚点投影,再消费页面,但没有本次 book 性能实测,不能直接套用 icexiv 的收益比例。

crates/notist-book/src/layout.rs 中:navigation 每页转换整棵导航树,breadcrumbs 从根查找,adjacent 在线性列表中定位并查找前后页。N 页完整导航的计算存在 O(N²) 路径。

其中 adjacent/祖先查找可用索引消除不必要扫描;但如果每个 HTML 必须包含 N 个导航项,输出本身就是 O(N²),仅加 HashMap 不能解决。大型 book 需要共享导航数据、局部窗口或按需加载等新呈现策略;要承认它可能改变现有页面字节与主题接口,而不是承诺零语义成本。

book 和 site 应共享资源/产物存储、PageFacts 与构建调度基础设施,保留各自导航、路由、主题和恢复策略,不宜为了合并 crate 而强行抹平差异。

6. serve:watch 正确不等于增量构建

CLI serve 的 rebuild 每次重新 assemble;site assemble 创建新的 Vault。当前 observed 集合主要解决“哪些变化应触发重建”,不包含足够的“哪些页面/阶段应重算”的结构。全局 comemo 仍可能命中部分模块工作,不能称所有缓存都被清空;但它没有提供整站按页复用结果。

更理想的构建节点包括:Module、CheckedInstance、GeneratedSource、PageEvaluation、PageFacts、LinkResolution、HTML、Template、Asset、PublicationRevision。每个节点记录代码/配置版本、实际资源版本、授权身份、依赖与失败状态。

由此:

  • 只改 CSS:通常不应重新执行数百万次内容调用。

  • 只改模板:复用未受影响的求值结果;是否重渲染全部 HTML 由模板依赖决定。

  • 改单页:复用其它页面,必要时只更新引用它的锚点/标题消费者。

  • 改共享模块或广泛使用的 JSON 块:失效可以很大,这是真实依赖,不应假装永远 O(1)。

当前 JSON 是每 1000 条物品一个 backing;文件粒度依赖会让一处变化影响较多页面。先实现正确的文件粒度增量,再考虑在准确版本与授权前提下验证数据投影是否未变;不先做复杂的字段级差分系统。

任意 command extension 可能依赖声明的 watch、甚至不符合纯度契约;不应因为同一函数名/配置相同就跳过执行。可以先正常重跑扩展,再对生成源码按内容身份逐页复用。新增可缓存扩展契约需要显式约定输入、外部效果和确定性。

跨构建缓存也不能只重放一个 dependency set:它丢失逻辑请求的重复次数、顺序与配额失败点。优先在粗粒度纯输入节点或完整页计算边界建立契约;需要精确逻辑效果时保存/验证相应记录,而不是无限制全局 memoize。新批次仍需重新确认能力,不继承旧批次的授权。

serve 应发布不可变 revision,通过共享 blob 复用旧/新站点未变文件,而不是复制所有字节;仍要测量旧 revision、构建中 revision 与进行中的 HTTP 请求共同存活时的峰值。

7. LLVM 类比:应学的是优化合法性的基础

LLVM 与这里相似的是多前端、统一语义边界和多后端的组织方式。差别包括:

  • LLVM IR 主要表达计算、控制流与内存操作,不表达文档挂载、注解、章节塑形和资源能力。

  • LLVM 的优化/代码生成时间与所生成程序的执行时间是两个指标;LLVM 在高优化级别并不保证编译总是很快。

  • 当前 Notist 在构建期间实际执行用户程序、保留内容身份并输出近 8 万文件,不能拿“编译一段小程序”直接类比。

  • LLVM 能删除无可观察影响的中间结果;Notist 很多调用边界对 query、来源或预算可观察,不能直接擦除。不过物理节点、元数据和可观察身份并不必一一对应独立胖对象。

值得借鉴:明确操作语义、显式控制/数据依赖、按阶段 lowering、已知调用专门化、优化后的验证器,以及可追溯的调试映射。内容种类和 shape 证据应留在合适的高层 IR,不要过早降成丢失领域信息的低层指令。

一个小型 typed ANF/slot-based IR 足以验证核心思路,未必需要完整 SSA、MLIR 或 LLVM 依赖。若未来采样证明纯计算是主导成本,且跨许多页面能摊薄编译开销,再讨论字节码/原生 backend。

8. 性能上限与目标可行性

8.1 没有一个脱离契约的“最快秒数”

冷构建可以近似分解为前端/检查、执行、结果构造、全局校验、HTML/模板、资产 IO、发布及析构。必要下界来自真实输入读取、逻辑执行/计费、必须保留的节点信息、输出字节和文件操作。

合理目标是减少重复工作,让时间接近 O(必要计算 + 内容/元数据访问 + 输入输出字节 + 文件操作),而不是宣称只按输入文本长度线性。递归、用户循环、全站模板和全导航输出都可能使工作量本身更大。

有界内存方案的目标是:共享程序 + 全站小型索引 + 有界数据缓存 + K 页工作集 + 输出缓冲。全站元数据依然随页数增长,不应宣称绝对 O(1);如果所有内容/输出都继续内存驻留,则下界也依然存在。

8.2 3s 与 300MB 是两种不同判断

  • 300MB,当前 API/布局:不适合。 单是完整输出就约 452MB,或全站 Item 内联约 820MB。必须改变驻留/存储方式;无需删除字段、页面或资源。

  • 300MB,新的外存/有界管线:值得验证,但未证明。 模块、Data 缓存、模板全局正文、元数据和旧/新 revision 都要纳入;把负载移到 mmap、磁盘或内核 cache 后仍要公开统计口径。

  • 3s 完整首次发布:尚不能承诺。 最新空输出批次约 10.7s,要约 3.6 倍整体改善;必须同时处理执行、驻留、分配和 IO,不能由某个函数快路径推导出来。

  • serve 局部更新:架构空间更明确。 避免全站重算是改变工作量,不只是改善常数;但全局模块/模板依赖仍可能迫使大范围重建。

用 refresh coarse 的 40.7% 只作 Amdahl 思维实验:即便求值完全免费,整体也只有约 1.69 倍上限;求值加速四倍,整体约 1.44 倍。该诊断不是正常版精确计时,不能把比例换算成承诺秒数,但足以反驳“只换 VM 就能解决整站目标”。

在给出可行的 3s 目标前,应有同机器、同安全检查、同完整输出的 copy/stage/publish 地板测量,以及紧凑执行器的端到端原型。只知道磁盘标称带宽不足以估算 78,004 个文件的真实成本。

9. 建议的架构实验次序

这不是已批准实施的计划;以下用于选择下一次授权实验,不能延续已用尽的十轮微优化额度。

  1. 先决定兼容边界。 保留哪些语言/查询/wire/预算语义;是否允许 Item API 版本化、派生执行布局、外存产物,以及 site.pages 正文接口演进。不要在实现过程中悄悄决定。

  2. 做有界产物/生命周期原型。 保留全部内容,暂存输出、独立 PageFacts、按页释放/外存,记录完整 CLI 与 serve revision 峰值。它直接检验 300MB 的结构性条件,但不把潜在时间收益预先算入。

  3. 做 closure + slots + resolved-call 执行切片。 先覆盖 icexiv 的 item/catalog 所需语法并对照旧解释器,再扩展到完整语言。共享代码不共享挂载执行;检查 capture-copy、容器分配和端到端 CPU 是否实降。

  4. 建立按页构建图。 先使无变化、CSS、模板、单页编辑免除不必要求值,再逐步处理模块、数据、目录枚举、失败恢复与命令扩展。

  5. 最后按证据选择紧凑存储、融合与并行。 并行需独立 evaluator 状态、有界 K、线程安全输入/缓存、确定性的诊断与 hooks 顺序;不能先用更多核心放大内存和分配压力。字节码/JIT 仅在真实采样显示分派/纯计算仍主导时进入。

验收需把正确性与性能统计分开:全输出、完整语义投影、来源、失败、额度临界、授权、Pending→Ready、连续修订依赖为硬门禁;性能预先确定重复样本、噪声阈值与可接受的时间/内存权衡。此前以单批最大 RSS 的极小上涨或几十毫秒中位差即拒绝,是该活动的保守规则,不是证明架构没有优化空间;不能追溯修改旧结论,但新活动应避免把噪声当作严格算法定理。

10. 最终判断

不是“文档树方向错了”,而是文档树适合承载内容语义,不适合独自承担程序、运行时环境、执行计划与全站产物存储的所有职责。

从函数式语言视角重构,最有价值的是共享代码、显式环境、效果与 staging、静态已知调用和中间结构消除;从编译基础设施视角重构,最有价值的是多个目的明确的表示与可验证 lowering;从工具链视角重构,最有价值的是依赖图、有界生命周期和事务式外存产物。

已有优化证明确实能减少常数,但后续最大的空间在改变“做什么工作、做几次、同时保留多少”,而不只是把同一棵树更快地复制一遍。