# 性能记录

本文件记录可复现的实测数字。所有数据都由仓库内的工具产生，不留"感觉很快"这种说法。

## 复现方式

```bash
moon run tools/bench_lookup --target native
```

工具会构建真实词典的索引，并用官方 golden 语料的输入文本（美国宪法中译本，
7735 个 UTF-16 码元）跑一遍 `s2t-lite`（`short_circuit(STPhrases, STCharacters)`）。

## 实测（2026-09-24，Windows，MoonBit `moon 0.1.20260920`）

| 指标 | 数值 |
| --- | --- |
| 索引构建 | 16–17 ms（STPhrases 49238 条 + STCharacters 4012 条） |
| 首字分组数 | 3534 组 |
| 单组最大条目数 | 653 |
| 单组平均条目数 | 13 |
| 转换吞吐 | 约 490 码元/ms（≈ 0.5 MB/s，UTF-16 计；两次运行实测 483 与 491） |
| 单次转换耗时 | 约 16 ms / 7735 码元 |

## 为什么这比线性扫描快

索引前，每个字符位置都要遍历整本词典：49238 条 × 7735 个位置 ≈ **3.8 亿次**候选比较。

索引后，每个位置只遍历"首字相同"的那一组，实测平均 13 条：
13 × 7735 ≈ **10 万次**候选比较。这是按实测组大小算出的量级差，约三个数量级。

组内按 key 长度降序排列，因此第一次命中就是最长匹配，不需要扫完整组。

## 已知取舍

- 索引在运行时构建（`Dict::new`），换来的是生成代码保持"纯数据"。如果后续把索引
  也放进生成期，启动开销可以进一步下降（记为后续优化项）。
- 最坏情况是命中大组（如首字为高频字的 653 条），此时单次查找仍要扫描该组；
  真正的上界要等 F5 打通官方语料后，用全量文本统计峰值。
- 目前的数字只覆盖 `s2t-lite`，未包含归一化与台湾/香港用词链路。

## 查找热路径优化：跳过不可匹配的连续片段（2026-09-30）

上游 OpenCC 在 `PrefixMatch::SkipUnmatchable` 里做了一件事：如果某个字符不可能作为任何词条的
首字，就整段跳过，而不是逐个字符去问词典。本项目补上了同样的捷径——`Node::can_start` 让
`apply` 与分词都能一次拷走整段"不可能命中"的文本。

两个语料的实测（`moon run tools/bench_lookup --target native`）：

| 语料 | 优化前 | 优化后 | 变化 |
| --- | --- | --- | --- |
| 中文密集（官方 golden 输入，7735 码元） | 491 码元/ms | 468 码元/ms | 基本持平（波动范围内） |
| ASCII 密集（合成标记文本，31890 码元） | 4724 码元/ms | **7503 码元/ms** | **1.59×** |

中文密集文本收益有限，因为常用汉字几乎都能作为某个词条的首字，没有可跳过的片段；
而标记、代码、日志这类文本里绝大多数字符不可能是词首，于是省掉了"逐字符查表 + 逐字符写缓冲"。
两次测量都在同一台机器、同一构建模式（native release）下进行，命令写在上面，可自行复现。

## 构建耗时

### 改造前：每个词典一个「逐条字面量」数组（2026-09-25 上午）

| 动作 | 耗时 |
| --- | --- |
| `moon test --target wasm-gc` | 3.4 s |
| `moon test --target native` | **341 s** |
| `moon run cmd/opencc --target native` 首次 | 约 3 min |
| `moon test --target wasm`（非 gc） | **编译失败**：`local count` 超后端上限 |

### 改造后：内嵌文本块 + 运行时建索引（同日）

| 动作 | 耗时 | 变化 |
| --- | --- | --- |
| `moon test --target wasm-gc` | **0.8 s** | 4.3× |
| `moon test --target native` | **7 s** | **49×** |
| `moon test --target wasm`（非 gc） | **1.6 s，26/26 通过** | 从"编译失败"到通过 |
| `moon run cmd/opencc --target native -- verify` 首次 | **3.4 s** | 约 55× |
| 生成的数据体积 | 2.1 MB → **1.68 MB** | 20% |

根因很直接：逐条字面量让 native 编译器要为 7.6 万条目生成巨大函数，而一个词典一个字符串块
只要处理 20 个常量。改造同时消掉了 plain wasm 后端的局部变量上限问题。

## 后端与产物（2026-09-25，改造后）

| 后端 | 库构建 | 测试 | CLI 产物 |
| --- | --- | --- | --- |
| wasm-gc | 通过 | 26/26 | `opencc.wasm` 1.31 MB |
| wasm（非 gc） | 通过 | 26/26 | — |
| js | 通过 | 需要 node 在 PATH | `opencc.js` 1.66 MB |
| native | 通过 | 26/26 | `opencc.exe` 1.54 MB |

## 按配置裁剪词典（2026-09-30）

每个词典是独立包，每个配置也是独立包（只 import 它用到的词典），所以"只想做某个方向"
的程序不会链进其它词典。三个可复现的实测（`moon build <path> --target wasm-gc --release`）：

| 构建 | 链入的词典数据 | wasm-gc 产物 |
| --- | --- | --- |
| `examples/t2s`（只用 t2s） | 180 KB | **249 KB** |
| `examples/s2t`（只用 s2t） | 1294 KB | **913 KB** |
| `cmd/opencc`（19 个配置都要） | 1680 KB | 1341 KB |

结论：反向或地区方向收益最大（t2s 相对全量 5.4× 更小），而 `s2*` 方向的体积由
`STPhrases`（1178 KB，占全量的 70%）决定，任何正向配置都躲不开它——这一点如实写在表里，
不假装裁剪能解决所有场景。

各配置的词典构成可以用 `tools/gen_dict --verify` 之后的 `config/<name>/moon.pkg` 直接看到。
