| name | decisive-experiment |
| description | 把一个"两派都有道理、再论证下去没有新信息"的架构/设计之争,转成一个能出判决的可测量实验。写实验规格(事前判据 + 判决树 + kill criterion + 时间盒)、审已有规格的漏洞、把结论回填。当出现「A 方案 vs B 方案该选哪个」「这条路会不会无界增长」「这个假设成不成立」而争论已达推理极限时使用;也用于 review 别人写的实验规格 / 研究轨文档。不用于普通 benchmark、bug 复现、性能调优。 |
判决性实验:把架构之争变成可判决的度量
判据:下一个 agent 拿到你的规格,不问你就能照着做,且做完能出一个你不能事后翻盘的结论。
0. 先判断该不该做
只在四条全中时才立实验:
- 争的是结构性选择(分几层 / 抽象放哪 / 假设成不成立),不是参数调优;
- 双方都有站得住的论证,再论证不产生新信息;
- 存在一个能测出来的量,且它真的能分开两个方案;
- 你愿意接受与预判相反的结果。第 4 条不满足就别做,你只会做一场表演。
实证:本仓 plan/design-dynamic-core-experiment.md 这条轨推翻过预判两次
(③ 打平、④ 反转结论)。这套流程能产生信息,前提是第 4 条是真的。
1. 规格骨架(照抄这个章节表)
规格文件放 plan/design-<题目>-experiment.md,实现放 research/<题目>/。
封面表格:日期 / 目的 / 实现位置 / 前置阅读 / 来源纪律(净室?)。
若不属产品范围,第一行就写清楚("不进 must-ship,不改 PRD 能力状态"),否则会被当成排期工作。
| § | 内容 | 不写会怎样 |
|---|
| §0 | 背景 + 已经确定、不在本实验讨论范围内的事(编号列出) | 每次讨论重跑一遍前提 |
| §1 | 硬约束(违反则实验无效),逐条可判定 | 做出来的东西不回答原问题 |
| §2 | 最小实验内容表:每个维度选了什么 + 为什么这样选 | 范围爆炸 |
| §3 | 判据表 + 度量纪律(见 §2 检查清单) | 事后凭手感选 |
| §4 | 判决树 + kill criterion + 时间盒 | 沉没成本接管决策 |
| §5 | 目录结构(建议,可调整但要说明) | 产出散落 |
| §6 | 已排除的选项(选项 / 为什么排除,表格) | 同一个坏主意被反复重提 |
| §7 | 本实验不回答什么 | 范围蔓延;且这里就是下一个实验的清单 |
| §8 | 结论回填(做完写,见 §5) | 结论只活在某个 session 的上下文里 |
§1 里写一条病灶探测器:「任何为了方便而 <往内核加第五类原语 / 给 IR 开逃生舱口> 的
冲动,是本实验要检测的病,不是要满足的需求。真需要就记为发现,不要偷偷加。」
这条把"实现时的妥协"从暗账变成了主产出。
2. 判据设计检查清单
隔离变量的技巧:固定一根轴,只动另一根。例:测 ABI 中立性时只用一个 ISA、
用同 ISA 的两个不兼容 ABI(SysV64 vs Win64)——用两个 ISA 会把 ABI 差异和指令差异
混在一起,测不出泄漏来自哪侧。先问「我的对照组把什么混进来了」。
2.5 度量纪律:一条轨共用一把尺
为什么单列一节。 本仓 research/dynamic-core/ 跑到第 15 个实验时做了一次口径审计
(research/dynamic-core/COMPARABILITY.md):一条轨里长出了 5 种互不相通的体积口径
(flat-blob 相减 / object .text / 整个 stripped 二进制 / 运行时发射码 / 含 entry+panic 的 blob),
外加一条正交的 std+默认 panic vs no_std+panic=abort 轴。轨内自证的量级:同为
「x86-64 IR→原生降级器」,一个口径下 14819 B、另一个 3003 B,相差 4.9×——
有多少来自设计、有多少来自口径,事后拆不开。综合时把它们并排放,就是把结论建在沙上。
每条实验开工前先认这一节,别等第 16 个实验再发明第 6 种口径。
2.5.1 体积:任何字节数必须带四元口径标签
口径 = {边界, 工具, 构建, 目标/执行},四项缺一不可,写在数字旁边而不是文档开头。
- 边界(最重要,也是头号杀手). 明写哪些模块在里面、哪些不在,并必须显式回答两问:
(a) OS 接缝 / intent 层在不在? (b) 载荷、env 表、adapter、数据表在不在?
审计发现的所有不可比里,一半以上是这一项没写。
- 固定三档上报。 任何「这个技术多大」的问题给三个数,不是一个:
- L1 机制码(纯机制,不含 OS 层、不含数据)
- L2 机制 + OS 接缝(该路线跑通真实载荷所必须的全部代码)
- L3 整个投递足迹(code+data,flat 口径)
某一档没测就写 「未测定」,不许用另一档的数填格。
- 工具四选一并写出来:
llvm-size Berkeley .text / llvm-size -A 逐函数 /
flat-blob 相减 / 整个 stripped 文件。禁止跨工具相除。
- 构建:凡是要与内核/blob 数并排的,一律
no_std + -O -C panic=abort -C debuginfo=0;
std + 默认 panic 的数只能与同类比。
- 目标/执行:写清 ISA/OS,并写清被测的那个产物本身有没有被执行过。
2.5.2 比较卫生
- 任何跨实验的并排,先声明「两侧同口径」;做不到就写 「该轴未测定」——
注意「未测定」不等于「不成立」,别把口径缺失写成一条对自己不利(或有利)的结论。
- 表格的一列 = 一把尺。 某路线在该尺下没有数,写 "not measured in this 口径"。
- 新实验要与既有数比较,优先复用被比较方的口径(本仓最佳实践:Q10 逐字复用 Q2 的
构建与相减方法,于是那一对成了全轨唯一干净的跨实验体积比较)。
2.5.3 行数 / 倍数 / 百分比
- LOC 统一 「非空行、非注释行」,写出统计命令;比较两份代码的 LOC 前,
先声明两者覆盖的能力集相同,不同就按能力集分档报(本仓踩过:拿一个不做 spawn 的引擎,
去比一个包含 spawn 的每目标行数)。
- 强制交叉校准:新实验必须用自己的方法复算一个既有实验的文件并把结果贴出来对上。
- 倍数:基线内联写出;基线若不是 like-for-like(naive vs 优化、跨 ISA、含/不含 entry+panic),
报两个基线。不得跨口径相除;不得把「发射产物的字节」与「工具二进制的字节」
混进同一个比率。
- 百分比:写清分子/分母各是哪个产物;区间端点如实取整(29.3% 写 29%,不写 30%);
跨实验的百分比一律禁止——Δ 只除以它自己那次测量的基线。
2.5.4 provenance:三分标签不够,每个数字加一个执行状态
[实测] 这一个标签会同时承担两件完全不同的事:「在真机上跑过」 与
「交叉编译出来量了字节但从未运行」。文首写一句全局 posture 注是对的,但
它在单行被摘引时会丢失——而结论表、执行层判决、综合,全都是在单行摘引。
所以执行状态必须写在行内,四选一(可组合):
真机执行 / 仅字节测量 / 编码器验证 / 结构推断
机制跑过、但引用的字节来自另一个未执行的构建时,两个都写
(例:[实测·真机执行;字节为 Linux/ELF 仅测量])。估算值必须标「估算、未实现」,
不许与实测共用一个标签。
3. 判决树:写完必须与 §3 的优先级声明对账
这条是血的教训。 本仓真实发生过:§3 明写「斜率 ③④ 优先于截距 ①②」,
但 §4 的枚举式判决树只写了 ③→②→⑤/⑥,漏了 ④。
结果 ④ 实测不打平且方向相反时,产生了两种合法读法,结论不唯一。
写完 §4 立刻做三件事:
- 枚举:§3 的每个判据编号,在判决树里出现过吗?没出现的显式说明为什么不是节点。
- 对账:判决树的分支顺序 == §3 声明的性质优先级吗?不一致就改树,别改声明。
- 穷举:每个判据打平 / 不打平的组合,树都有出口吗?
判决树模板:
1. 主判据 = <斜率判据>。若 <明确的判负条件> → 直接判负,不看其它。
2. 主判据打平 → 看 <次判据>。
3. 也打平 → 看 <安全/性质判据>;此时偏好 <X>。
kill criterion: 若 <布尔门> 不过 → 命题证伪,立即停,写结论。
时间盒: 做到 <某个具体判据出数> 为止。在此之前不做 A、不做 B、不做优化。
时间盒必须钉在一个具体判据出数上,不是"两周"。
4. 失败模式清单(每份规格里挑相关的写进 §4)
| 死法 | 长什么样 | 防法 |
|---|
| 休眠模块 | 范围不断扩张,始终差一点能出结论,最后谁也没被证伪 | 时间盒钉在判据上;量完就停,等人决策 |
| 越做越大去覆盖每个泄漏 | 每发现一个反例就给抽象加一个特性 → 重演 LLVM IR / POSIX 膨胀 | 宁可要「只在某个子集内成立」的有边界结论,不要靠加特性硬撑的"全成立" |
| 事后改判据 | 看到数据觉得这个指标"不公平",换一个 | §3 事前钉死;改度量必须在 §8 单列并说明动机 |
| 测了个双胞胎 | 两个变体共用了本该有差异的那部分,差异被抹平 | §1 明写已知偏差,并把结论降级为"上界"而非"确值" |
| 写两个独立原型 | 90% 工作重复,且引入「哪个花的时间多」这个混淆变量 | 写一份可分离的实现,打包两次 |
| 只做一个 <轴上的点> | 量不出边际成本,斜率判据全废 | 边际判据要求至少两个点;只做一个 = 零信息量 |
| 自证指标 | 只量对自己有利的维度 | 判据里放至少一个对手方会引用的指标 |
5. 回填(§8)——做完必须写,否则等于没做
§8 必须含:
- 判决结果:按 §4 的树逐步走一遍("③ 打平 → 落 ②"),把路径写出来,不只写结论。
- 数字表:判据 × 变体,带单位与测量条件。
- 与规格不符之处("必然有"):逐条列。顺序改了、某个平台只量不跑、
某个原语被迫扩容——全部如实记,包括让结论变难看的那些。
- 诚实条款:明写「未为让结论好看而改度量」。若结论有两种读法,两种都写出来
并标明各自依据哪一节,不要挑一种当唯一结论。
- 推翻预期的地方单独点名——那是这次实验最值钱的部分。
- 若发现规格本身有 bug(判决树漏项、判据定义有歧义),在 §8 记下来并修规格,
不要只在结论里绕过去。
同时把结果目录的 RESULTS.md 写成第三方可复跑的形态:度量条件 → 逐判据数字 →
决策 trace → 偏差 → 复跑命令 + 独立参考值(如独立算出的哈希,用来证明两个变体
不是一起错的)。
6. 多个实验并行时
一条研究轨会分叉成多个实验(§7「不回答」的每一条都是候选)。
在轨目录的 README.md 维护 Q 编号索引:每个 Q = 问题一句话 + 状态 + 结论 +
规格文档 + 实现目录。没有索引,第三个实验开始就没人知道全貌。
状态只用四档:已判决 / 进行中 / 已立项未开工 / 候选(未立项)——
"候选"不得写结论,哪怕你觉得显然。
两条引用资格(本仓真实踩过):
- 没有
RESULTS.md 的 Q 不得标「已判决」。 结论散在 README + 规格 §8 + 提交信息里
≠ 有结论:索引承诺的「每条实测在该 Q 的 RESULTS.md 里有复跑命令」就落空了。
- 标着「进行中」的 Q,它的数字不得被别处当作已决结论引用。 本仓出现过:某个 Q 在板上
还是「进行中」、且没有
RESULTS.md,它的核心字节数已经被写进一张「三条路线已判决」的表。
写综合前先扫一遍:我引的每个数,它那个 Q 的状态是什么?