diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6b960..11a0e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Versions apply to the whole repository and are git tags (see [docs/release.md](d Kits are not released separately and carry no version of their own. ## [Unreleased] +- **Docs guides (en + zh):** plain-docs pass on performance guides — drop How-to-read / Scope-and-method coaching lists where the lead already says measure-first; replace heuristic-dogfood scope wording with lint-hit vs wall-time framing; prefer `Lint: id (default …)` lines; soften coaching bold; sync zh-CN guide-src (and zperf hand `zh-CN/zig-performance-guide.md`) to the English structure and voice. + +- **Docs zh-CN:** site Markdown Chinese coverage for shared docs, kit guides (`guide-src/zh-CN` + build-guides stem pairing), package landings, corpus/evidence-policy/rule-ids, active evidence notes, and root pages published as `*.zh.md` via `zh-CN/` parents. Tombstone evidence under `evidence/_removed/` intentionally left English-only. + +- **Docs SEO / GEO (en + zh):** description, Open Graph (`og:locale` + `alternateLocale`), Twitter Card, canonical and hreflang; static export forces blocking metadata into HTML (`htmlLimitedBots`); `sitemap.xml` pairs en/zh with language alternates; `robots.txt` allows common AI crawlers; `llms.txt` + `llms.zh.txt`; locale-aware JSON-LD (`WebSite` / `WebPage` / `SoftwareApplication`); post-build sets `` under `/zh/` for crawlers; 1200×630 `og.png`. README opens with an English-only value-prop hook. + - **Docs site:** page links no longer keep a trailing `.md` (that 404s on GitHub Pages). Assemble rewrites them to trailing-slash directory URLs relative to the page directory, fails the build if any `.md` hrefs remain, emits static files as root-absolute paths so the `/zh/` locale works, and publishes `LICENSE` / `NOTICE` as directories so `trailingSlash` can open them. - **All kits:** default matching is **`--scope all`** where the kit CLI supports it (opt in to **`--scope loop`** for the former in-loop-only match). Finding messages no longer imply non-loop sites are out of scope. Rule slugs containing `in-loop` were **renamed** to `*-hot` (including leftover `rs-map-new-in-loop` → `rs-map-new-hot`; see per-kit Renamed tables); old slugs are tombstoned under each kit `evidence/_removed/*.REMOVED.md` and must never be reused. See each kit `docs/rule-ids.md` Renamed table. jsperf also keeps adopter FP cuts + new hints from earlier commits on this branch. No severity escalations. diff --git a/README.md b/README.md index 0bb1198..a5f7693 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # XPerf -XPerf is a set of performance guides and heuristic linters, one kit per language under `packages/`. A lint hit means “look here,” not “this will be faster.” A rule stays a hint until paired fixtures and a measured before/after promote it. Rule IDs are semantic slugs (~169 active). +XPerf helps you find real performance smells across 19 languages — guides plus heuristic linters that stay honest: a hit means “look here,” not “this will be faster,” until paired fixtures and a measured before/after say otherwise. + +Docs site: [lbb00.github.io/xperf](https://lbb00.github.io/xperf/). Rule IDs are semantic slugs (~169 active). ## Languages diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md new file mode 100644 index 0000000..9837b8f --- /dev/null +++ b/docs/zh-CN/README.md @@ -0,0 +1,56 @@ +# XPerf 文档 + +**XPerf** 伞形(多工具包系列)的共享文档。各语言的指南和 Lint 文档存放在各自的包下。 + +GitHub Pages 会渲染此目录及每个工具包的指南。使用 `bash scripts/build-pages.sh serve` 进行预览。参见 [release.md](release.md)。 + +## 共享文档(本目录) + +| 文档 | 目的 | +|-----|---------| +| [branding.md](branding.md) | 显示名称 **XPerf** 与路径 `xperf` | +| [doc-style.md](doc-style.md) | 由 `scripts/doc-check.sh` 执行的 Markdown 规范 | +| [release.md](release.md) | 我们在 `v*` 标签上发布的内容 | +| [overview.md](overview.md) | XPerf 是什么:工具架构、共享循环、证据规则 | +| [playbook.md](playbook.md) | 操作循环:库存 → 证据 → 语料库 → 反向优化 → 内部测试 → 文档 | +| [kits.md](kits.md) | 检查入口点、默认值、语料库准备笔记(仅限短标签) | +| [quickstart.md](quickstart.md) | 每种语言的构建/运行命令 | +| [smell-catalog.md](smell-catalog.md) | 跨语言气味指数(ID为每套工具的标识符) | +| [rule-regression.md](rule-regression.md) | 正/负夹具 + 证据覆盖;诚实的缺口 | +| [rule-id-map.md](rule-id-map.md) | 历史数字 → 标识符映射(命令行接口不接受数字) | +| [roadmap-next.md](roadmap-next.md) | 以相同方法进行下一项工作 | +| [static-check-sources.md](static-check-sources.md) | 论坛、博客和 GitHub 上 linter 可以检测到的形状的来源 | + +## 各 package 入口 + +| 工具包 | 指南 | 语料/证据政策 | Lint 文档 | +|-----|-------|--------------------------|-----------| +| **zperf** | [zig-performance-guide.md](../../packages/zperf/docs/zh-CN/zig-performance-guide.md) | [evidence-policy.md](../../packages/zperf/docs/zh-CN/evidence-policy.md) · `packages/zperf/evidence/` | [zperf-lint/README](../../packages/zperf/zperf-lint/README.md) | +| **cperf** | [c-performance-guide.md](../../packages/cperf/docs/zh-CN/c-performance-guide.md) | [corpus.md](../../packages/cperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/cperf/docs/zh-CN/evidence-policy.md) | `packages/cperf/cperf-lint/` | +| **jsperf** | [js-performance-guide.md](../../packages/jsperf/docs/zh-CN/js-performance-guide.md) | [corpus.md](../../packages/jsperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/jsperf/docs/zh-CN/evidence-policy.md) | `packages/jsperf/jsperf-lint/` | +| **jvmperf** | [en/](../../packages/jvmperf/docs/en/) | [corpus.md](../../packages/jvmperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/jvmperf/docs/zh-CN/evidence-policy.md) | `packages/jvmperf/jvmperf-lint/` | +| **goperf** | [go-performance-guide.md](../../packages/goperf/docs/zh-CN/go-performance-guide.md) | [corpus.md](../../packages/goperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/goperf/docs/zh-CN/evidence-policy.md) | `packages/goperf/goperf-lint/` | +| **pyperf** | [python-performance-guide.md](../../packages/pyperf/docs/zh-CN/python-performance-guide.md) | [corpus.md](../../packages/pyperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/pyperf/docs/zh-CN/evidence-policy.md) | `packages/pyperf/pyperf-lint/` | +| **swiftperf** | [swift-performance-guide.md](../../packages/swiftperf/docs/zh-CN/swift-performance-guide.md) | [corpus.md](../../packages/swiftperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/swiftperf/docs/zh-CN/evidence-policy.md) | `packages/swiftperf/swiftperf-lint/` | +| **arktsperf** | [arkts-performance-guide.md](../../packages/arktsperf/docs/zh-CN/arkts-performance-guide.md) | [corpus.md](../../packages/arktsperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/arktsperf/docs/zh-CN/evidence-policy.md) | `packages/arktsperf/arktsperf-lint/` | +| **cppperf** | [cpp-performance-guide.md](../../packages/cppperf/docs/zh-CN/cpp-performance-guide.md) | [corpus.md](../../packages/cppperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/cppperf/docs/zh-CN/evidence-policy.md) | `packages/cppperf/cppperf-lint/` | +| **csharpperf** | [csharp-performance-guide.md](../../packages/csharpperf/docs/zh-CN/csharp-performance-guide.md) | [corpus.md](../../packages/csharpperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/csharpperf/docs/zh-CN/evidence-policy.md) | `packages/csharpperf/csharpperf-lint/` | +| **phpperf** | [php-performance-guide.md](../../packages/phpperf/docs/zh-CN/php-performance-guide.md) | [corpus.md](../../packages/phpperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/phpperf/docs/zh-CN/evidence-policy.md) | `packages/phpperf/phpperf-lint/` | +| **rubyperf** | [ruby-performance-guide.md](../../packages/rubyperf/docs/zh-CN/ruby-performance-guide.md) | [corpus.md](../../packages/rubyperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/rubyperf/docs/zh-CN/evidence-policy.md) | `packages/rubyperf/rubyperf-lint/` | +| **rustperf** | [rust-performance-guide.md](../../packages/rustperf/docs/zh-CN/rust-performance-guide.md) | [corpus.md](../../packages/rustperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/rustperf/docs/zh-CN/evidence-policy.md) | `packages/rustperf/rustperf-lint/` | +| **kotlinperf** | [kotlin-performance-guide.md](../../packages/kotlinperf/docs/zh-CN/kotlin-performance-guide.md) | [corpus.md](../../packages/kotlinperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/kotlinperf/docs/zh-CN/evidence-policy.md) | `packages/kotlinperf/kotlinperf-lint/` | +| **objcperf** | [objc-performance-guide.md](../../packages/objcperf/docs/zh-CN/objc-performance-guide.md) | [corpus.md](../../packages/objcperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/objcperf/docs/zh-CN/evidence-policy.md) | `packages/objcperf/objcperf-lint/` | +| **dartperf** | [dart-performance-guide.md](../../packages/dartperf/docs/zh-CN/dart-performance-guide.md) | [corpus.md](../../packages/dartperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/dartperf/docs/zh-CN/evidence-policy.md) | `packages/dartperf/dartperf-lint/` | +| **luaperf** | [lua-performance-guide.md](../../packages/luaperf/docs/zh-CN/lua-performance-guide.md) | [corpus.md](../../packages/luaperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/luaperf/docs/zh-CN/evidence-policy.md) | `packages/luaperf/luaperf-lint/` | +| **beamperf** | [beam-performance-guide.md](../../packages/beamperf/docs/zh-CN/beam-performance-guide.md) | [corpus.md](../../packages/beamperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/beamperf/docs/zh-CN/evidence-policy.md) | `packages/beamperf/beamperf-lint/` | +| **nimperf** | [nim-performance-guide.md](../../packages/nimperf/docs/zh-CN/nim-performance-guide.md) | [corpus.md](../../packages/nimperf/docs/zh-CN/corpus.md) · [evidence-policy.md](../../packages/nimperf/docs/zh-CN/evidence-policy.md) | `packages/nimperf/nimperf-lint/` | + +## 导航示意 + +```mermaid +flowchart TB + root[xperf] + root --> readme["README.md
family entry"] + root --> docs["docs/
overview · quickstart · kits
playbook · smell catalog · rule regression
roadmap · release · doc style"] + root --> kit["packages/kit/
README · guide · corpus
evidence/ · lint/"] +``` diff --git a/docs/zh-CN/branding.md b/docs/zh-CN/branding.md new file mode 100644 index 0000000..edd0548 --- /dev/null +++ b/docs/zh-CN/branding.md @@ -0,0 +1,6 @@ +# 品牌写法 + +| 形式 | 用法 | +|------|------| +| **XPerf** | 正文中的产品名:标题、句子、版权行(“XPerf contributors”)。 | +| **xperf** | 仓库目录、克隆路径、归档名(`xperf-0.1.0-packages.tar.gz`)。 | diff --git a/docs/zh-CN/doc-style.md b/docs/zh-CN/doc-style.md new file mode 100644 index 0000000..926d1c4 --- /dev/null +++ b/docs/zh-CN/doc-style.md @@ -0,0 +1,34 @@ +# 文档风格 + +由 `bash scripts/doc-check.sh` 与 `bash scripts/build-guides.sh --check` 强制执行(二者均在 CI 中运行)。在源头修正;不要放宽检查。 + +## 全部 Markdown + +- 首行必须是一个 `# H1`;标题层级不可跳级;仅使用 ATX(`#`)标题。 +- 无行尾空白、无 CRLF、恰好一个末尾换行。 +- 相对链接必须可解析。不要链接到仓库外的文件。 +- 列表使用 `-`;代码围栏在适用时带上语言标签。 +- 除 `zh-CN/` 目录外一律使用英文。 + +## 每个 package + +| 文件 | 要求 | +|------|------| +| `README.md` | 含 `## Run lint` 与 `## Layout`(建议顺序:Contents、Focus、Run lint、Layout) | +| `docs/corpus.md` | 先写 `## Pins`;其后章节为带日期的笔记 | +| `docs/evidence-policy.md` | 先 `## Gate`,再 `## Current map`(标题不含日期或版本;把“截至 ”写在该节首行) | +| `docs/rule-ids.md` | 一个 H1;墓碑放在 `## Removed (tombstone; never reuse)` 下 | +| `evidence/.md` | 含 `Claim`、`How measured`、`Runtime` 表与结果;或待定规则用 `Status` / `Reason` / `Unblock` | + +## 指南 + +`docs/guide-src/*.md`(或 `guide-src//*.md`)是唯一源。指南本身由生成得到: + +```sh +bash scripts/build-guides.sh # 从 guide-src 重写指南 +bash scripts/build-guides.sh --check # 若指南被手改则失败 +``` + +- 分片按字节拼接,因此仅当指南在该处需要空行时,分片才以空行结尾。 +- 分片内的链接相对**生成后的指南**书写,而不是相对 `guide-src/`。 +- 分片名为 `NN_topic.md`;`00_head.md` 打开指南。 diff --git a/docs/zh-CN/kits.md b/docs/zh-CN/kits.md new file mode 100644 index 0000000..d595074 --- /dev/null +++ b/docs/zh-CN/kits.md @@ -0,0 +1,31 @@ +# 套件状态 + +列出每个包的 Lint 入口点、默认严重性和语料库注释。规则 ID 仅为 **短标签**。计数与每个工具包的 `docs/rule-ids.md` 匹配(不包括墓碑)。启发式命中是审查提示,而非加速手段。 + +在 AGENT.md G3 门下,所有 **19** 个工具包对于 2026-10-22(亚洲/上海)已分级的规则集都是 **语料库就绪** 的。2026-10-23 添加的提示规则 **不** 包含在该裁定中。现在有几个带有微基准测试并保持 **提示** 状态。公平重运行仍在 1× 附近的规则已被移除。新的标识没有进行语料库分级。 + +| 软件包 | 代码检查 | 活跃 | 备注 | +|---------|------|-------:|-------| +| [zperf](../../packages/zperf/) | `zperf-lint` | 31 | Zig 0.17+。G3/r4 降级了 OK-idiom mass 规则;一些默认设置仍然启用(例如 `zp-page-allocator-small`, `zp-broad-runtime-safety`)。`zp-alloc-print-hot` 是未更改格式字符串的提示。 | +| [cperf](../../packages/cperf/) | `cperf-lint` | 12 | C11 检查器 + 预设。`cp-alloc-hot` / `cp-realloc-grow-one` 错误;strlen, sprintf, fopen, clock, wide-bool-flag, `regcomp`, `alloca`-in-loop, 以及大值传递结构体提示;`cp-printf-hot` / `cp-strcat-hot` 在默认应用预设下关闭。2026-10-23 的提示有微基准测试,并不属于语料库裁决的一部分。 | +| [jsperf](../../packages/jsperf/) | `jsperf-lint` | 11 | 纯节点 ≥18,无 npm 依赖。`jp-delete-array-hole` 错误;map-filter / regex-ctor / JSON / DOM-query / `Intl.DateTimeFormat` / `readFileSync` / 时钟提示;在应用预设下关闭 alloc / 控制台默认设置。`jp-string-plus-in-loop` 已移除(`push` + `join` 并非约 2×)。 | +| [jvmperf](../../packages/jvmperf/) | `jvmperf-lint` | 29 | Java (Gradle)。在测试中固定使用 JDK **21**。在 G3/G4 之后混合出现错误/提示/关闭;请参阅工具包证据政策。| +| [goperf](../../packages/goperf/) | `goperf-lint` | 10 | `gp-append-nil-slice` / `gp-defer-hot` 加上正则表达式 / JSON / 反射名称查找提示;`gp-make-hot`–`gp-fmt-hot` 关闭。`time.Parse` 已被移除。 | +| [pyperf](../../packages/pyperf/) | `pyperf-lint` | 6 | `pp-list-plus-rebind` 加 JSON / deepcopy 提示;`pp-str-plus-hot` / `pp-print-hot` 关闭。`re.compile` 和 `datetime.strptime` 已被移除。 | +| [swiftperf](../../packages/swiftperf/) | `swiftperf-lint` | 8 | `sp-array-no-reserve` 提示(小构建者约 5×;一个大填充约 1.6×)。循环打印保持关闭(约 10× 输出到 /dev/null)。强制解包,字符串 `+=`,以及 `map`/`joined` 已移除。日期格式化器 / JSON 解码 / `NSRegularExpression` 提示仍然存在。2026-10-23 的提示不属于语料库判定。若缺少 Swift SDK,则进行文本检查路径。 | +| [arktsperf](../../packages/arktsperf/) | `arktsperf-lint` | 4 | 节点在 `.ets`/`.ts`/`.js` 上进行 lint 检查。`at-includes-hot` / `at-json-hot` 提示;分配 / 控制台默认关闭。 | +| [cppperf](../../packages/cppperf/) | `cppperf-lint` | 6 | 专用 C++17(≠ cperf)。regex、endl、clock、ifstream、按值 const 和 vector-bool 提示。字符串追加、字符串流以及若干 jitter id **已删除**。短 `const` 字符串保持在约 1×,并仍有提示。 | +| [csharpperf](../../packages/csharpperf/) | `csharpperf-lint` | 10 | 需要 `dotnet`。`cs-regex-hot`、LINQ 实例化、`string.Format` 和 JSON 提示;列表添加 / 字符串拼接 / 关闭控制台。 | +| [phpperf](../../packages/phpperf/) | `phpperf-lint` | 3 | `ph-array-merge-rebind` 错误 (G4); `ph-string-dot-hot` 关闭; `ph-echo-hot` 提示. | +| [rubyperf](../../packages/rubyperf/) | `rubyperf-lint` | 6 | 正则表达式 / JSON / 文件读取提示;字符串加 / 数组加 / 延迟输出。`Date.parse` 和 `Hash#merge` 已移除。 | +| [rustperf](../../packages/rustperf/) | `rustperf-lint` | 7 | `rs-format-rebind-hot` 错误;println / collect / 正则提示;`rs-alloc-hot` 关闭。已去除摊销 `+=`、`Vec::new` 加上 push,以及循环中的 `HashMap::new`。 | +| [kotlinperf](../../packages/kotlinperf/) | `kotlinperf-lint` | 3 | Kotlin-first (≠ jvmperf JMH)。所有三个规则默认关闭;一个抖动 ID **已移除**。 | +| [objcperf](../../packages/objcperf/) | `objcperf-lint` | 8 | Objective-C(≠ swiftperf)。`oc-nsstring-append-hot` / `oc-nslog-hot` 关闭;日期格式化器、JSON 和正则表达式提示。两个抖动 ID **已移除**。| +| [dartperf](../../packages/dartperf/) | `dartperf-lint` | 4 | `dt-growable-add-empty` 提示;list-add / string-plus / 打印 默认关闭。 | +| [luaperf](../../packages/luaperf/) | `luaperf-lint` | 3 | `lu-string-concat-hot` 错误 (G4); `lu-table-insert-hot` 关闭; `lu-print-hot` 提示. | +| [beamperf](../../packages/beamperf/) | `beamperf-lint` | 4 | Elixir + Erlang。list-concat / map-merge / enum-chain 提示;`be-io-puts-hot` 关闭。一个抖动ID **已移除**。 | +| [nimperf](../../packages/nimperf/) | `nimperf-lint` | 4 | 需要 `nim`。`nm-seq-no-capacity` 提示;默认关闭 seq-add / string / echo。 | + +没有编造的发布网址。 + +历史数字 → slug 映射(命令行界面**不**接受数字):[rule-id-map.md](rule-id-map.md)。 diff --git a/docs/zh-CN/overview.md b/docs/zh-CN/overview.md new file mode 100644 index 0000000..8221d03 --- /dev/null +++ b/docs/zh-CN/overview.md @@ -0,0 +1,46 @@ +# XPerf 概览 + +**XPerf** 是各语言性能 kit 的总伞项目。每个 kit 可独立使用:一份有测量依据的指南、示例、fixtures,以及用该语言(或面向该语言)编写的启发式 linter。根目录放共享方法文档与跨 kit 的 smell 索引——不是「以 Zig 为主、其它语言外挂」的树。 + +## 架构 + +```mermaid +flowchart TB + method["docs/ shared method
playbook · smell catalog
rule regression · roadmap"] + kits["19 kits
zperf · cperf · jsperf · jvmperf · goperf
pyperf · swiftperf · arktsperf · cppperf · csharpperf
phpperf · rubyperf · rustperf · kotlinperf · objcperf
dartperf · luaperf · beamperf · nimperf"] + evidence["evidence/
JSONL before and after"] + outside["out of tree, not in git
corpora · STATIC-VS correlates"] + method --> kits --> evidence --> outside +``` + +**共享闭环**(见 [playbook.md](playbook.md)):盘点 → 证据 → corpus lint → 反向优化 → dogfood → 文档。只有在成对 fixtures **并且** 有测得的 before/after JSONL 时,严重级别才升为默认 `error`。 + +**跨语言索引:** [smell-catalog.md](smell-catalog.md) 把同一类 *smell* 映射到各 kit。规则 ID 是 **按 kit** 的 slug;jsperf 与 jvmperf 都用 `jp-` 前缀但 slug 集合不相交(jsperf 的 `jp-alloc-hot` ≠ 任何 jvmperf 规则)。某一运行时上的 YES 不能直接搬到另一运行时——移植建议时请重新测。 + +## 各 kit 状态 + +截至 2026-10-22(Asia/Shanghai),**全部 19 个 kit 已通过 AGENT.md 的 corpus-ready(G3)门槛**。启发式 ≠ 保证加速。 + +| Kit | Lint 入口 | 指南 | 备注 | +|-----|------------|------|------| +| **zperf** | `zperf-lint`(Zig AST) | 英文 · **corpus-ready** | G3/r4;默认开启的 TP 集;OK-idiom 大量 off;allocPrint-in-loop 为 hint | +| **cperf** | `cperf-lint`(C11) | 英文 v0 · **corpus-ready** | cp-alloc / cp-realloc 为 error;strlen、热调用、wide-bool-flag、alloca-in-loop、大结构体按值传递为 hint;cp-printf / cp-strcat 为 off | +| **jsperf** | `jsperf-lint`(纯 JS) | 英文 v0 + V8/启动 · **corpus-ready** | jp-delete-array-hole 为 error;string+/map-filter/regex/JSON/DOM/readFile/clock 为 hint;alloc/delete-shape 为 off | +| **jvmperf** | `jvmperf-lint`(Java) | 英文 · **corpus-ready** | G3/G4 默认;JMH/JOL;STATIC-VS-HOTSPOT | +| **goperf** | `goperf-lint`(Go) | 英文 v0 · **corpus-ready** | append-nil / defer / regexp / JSON / reflect / open / clock 为 hint;make/string+/fmt 为 off | +| **pyperf** | `pyperf-lint`(纯 Python) | 英文 v0 · **corpus-ready** | list-plus 以及 JSON/deepcopy/open 为 hint;str+/print 为 off;re.compile 与 datetime-format 已移除 | +| **swiftperf** | `swiftperf-lint`(Swift PM) | 英文 v0 · **corpus-ready** | reserve / map-joined hint,以及 date-formatter / JSON / regex / file-read / clock / value-for-key hint;无 SDK 时 text-lint 对等 | +| **arktsperf** | `arktsperf-lint`(纯 Node) | 英文 v0 · **corpus-ready** | at-includes / at-json 为 hint;alloc/delete/console 为 off | +| **cppperf** | `cppperf-lint`(C++17) | 英文 v0 · **corpus-ready** | regex、endl、clock、ifstream、const-by-value、vector-bool 等 hint | +| **csharpperf** | `csharpperf-lint`(C#) | 英文 v0 · **corpus-ready** | 构建器 / 热 I/O / 热调用 hint | +| **phpperf** | `phpperf-lint`(PHP) | 英文 v0 · **corpus-ready** | 字符串与数组重绑定 / echo hint | +| **rubyperf** | `rubyperf-lint`(Ruby) | 英文 v0 · **corpus-ready** | 构建器 / 热 I/O / 热调用 | +| **rustperf** | `rustperf-lint`(Rust) | 英文 v0 · **corpus-ready** | 分配 / 字符串 / 热调用 | +| **kotlinperf** | `kotlinperf-lint`(Kotlin) | 英文 v0 · **corpus-ready** | ≠ jvmperf JMH;集合与字符串 | +| **objcperf** | `objcperf-lint`(Objective-C) | 英文 v0 · **corpus-ready** | ≠ swiftperf;集合 / 字符串 / formatter | +| **dartperf** | `dartperf-lint`(Dart) | 英文 v0 · **corpus-ready** | list / string | +| **luaperf** | `luaperf-lint`(Lua) | 英文 v0 · **corpus-ready** | table / string | +| **beamperf** | `beamperf-lint`(Elixir/Erlang) | 英文 v0 · **corpus-ready** | list / binary | +| **nimperf** | `nimperf-lint`(Nim) | 英文 v0 · **corpus-ready** | seq / string / I/O | + +命令与默认值见 [kits.md](kits.md);历史数字 ID → slug 见 [rule-id-map.md](rule-id-map.md)。 diff --git a/docs/zh-CN/playbook.md b/docs/zh-CN/playbook.md new file mode 100644 index 0000000..c5b627d --- /dev/null +++ b/docs/zh-CN/playbook.md @@ -0,0 +1,73 @@ +# XPerf 使用手册 + +用于 **XPerf** 系列的操作方法。每个套件使用相同的循环;证据和语料库保持在套件本地。 + +**代理:** 遵循根 [AGENT.md](../../zh-CN/AGENT.md) 门 **G0–G7**。此操作手册是人工叙述;如果其与 AGENT.md 在流程上有分歧,**以 AGENT.md 为准**。特别是:没有 **TP/FP 分级 (G3)** 的语料是不完整的。 + +## 循环 + +```mermaid +flowchart TB + inventory --> evidence --> corpus["corpus lint"] --> triage["TP / FP triage"] + triage --> reverse["reverse-opt"] --> dogfood --> docs +``` + +1. **清单** — 列出指南章节、缺失的证据、lint规则覆盖范围。 +2. **证据** — 在 `-O2` / ReleaseFast / 等效位置下的 JSONL(或本地语言)微基准;记录机器和标志。在默认 `error`(代理 **G4**)之前必须完成。 +3. **语料库 lint** — 浅克隆 ≥3 个真实项目;运行检查器(代理 **G2**)。 +4. **TP/FP 分类** — 对残余样本进行抽样;标记 TP / FP / OK-惯用法 / 文档 / 跳过路径;编写 `TRIAGE-*.md`(代理 **G3** — **必填**,非可选)。 +5. **反向优化** — 收紧规则、跳过路径、为 FP 类添加好的固定测试;重新运行语料库差异。 +6. **自用测试** — 检查器 + 示例清理;不良固定测试为故意设置(代理 **G0/G1**)。 +7. **文档** — 干练语气审查;更新 `corpus.md` + 注册表。 + +## 语料循环(软件包) + +语料库按套件命名(第一次内部测试轮次 2026-10-21 亚洲/上海;后续轮次在每个套件的 `docs/corpus.md` 中)。克隆保留在仓库外;软件包只保留 `docs/corpus.md` 和指南引用。 + +| 包 | Lint | 内部使用的语料库 (r1–r3) | 与发现相关的指南章节 | +|---------|------|---------------------------|----------------------------------| +| **cperf** | `cperf-lint` cp-alloc-hot–cp-strcat-hot | quickjs, lua, gcc(sparse), sqlite `src/` | `01_allocation`(cp-alloc-hot, cp-realloc-grow-one), `02_hot_io`(cp-printf-hot dump/codegen) | +| **jsperf** | `jsperf-lint` jp-alloc-hot–jp-map-filter-chain | Node `lib/`, Preact `src/`, quickjs `*.js`, TypeScript `packages/typescript/src` + V8 基准测试 | `01`…`03`, **`04_v8_optimizations`** (lint≠JIT; `STATIC-VS-V8.md`) | +| **zperf** | `zperf-lint` | zig 标准库 + 选定应用程序(参见 zperf 文档) | 完整英文 | +| **jvmperf** | `jvmperf-lint` 29 个活跃规则,`packages/jvmperf/docs/rule-ids.md` 中的标识 | okhttp, retrofit, kotlinx.coroutines, guava `src`, vert.x, fastjson2, jetty-core, netty buffer+common | 带 JMH/JOL 证据的英文; `packages/jvmperf/docs/corpus.md` | + +重新运行:路径/目录跳过 → 规则抑制 → 套件 `docs/corpus.md` 中的前/后表格。不要将剩余计数视为壁钟时间声明。 + +## 每套包裹的交付物 + +- 使用章节草稿的指南 → 随时间测量的声明 +- 使用 `--format text|json`、`--preset app|engine|all`、`--severity RULE=error|hint|off` 进行检查;仅在发现 **错误** 时退出非零(可选 `--strict-hints`、`--no-fail`) +- `fixtures/good` / `fixtures/bad` 用于 2–3 个入门规则 +- Git 之外的研究笔记 + +## 严重性和预设 (cperf / jsperf 0.3.x) + +默认 **`app`**:保留与测量到的痛点相关的规则作为 `error`(C:cp-alloc-hot 和 cp-realloc-grow-one;JS:jp-delete-array-hole);将噪声大、语料库密集的规则降级到 `hint` 或 `off`(C:cp-printf-hot 和 cp-strcat-hot 关闭;JS:jp-alloc-hot、jp-console-hot 和 jp-delete-shape 关闭,jp-map-filter-chain 提示)。JS `jp-string-plus-in-loop` 在 `push` + `join` 出现约 2 倍失误后被移除。**`engine`** 保持相同思路,并为引擎树增加一些提示。**`all`** 恢复“每个发现都失败”(0.3 之前的行为)。提示仍然打印;除非 `--strict-hints`,否则不会导致 CI 失败。优先选择证据门而不是残留计数。 + +## 还有什么 + +见 [roadmap-next.md](roadmap-next.md) — 接下来的循环使用相同的方法(证据门、严重性、动态伴侣、自有语料库)。 + +## 跨套件气味 + +参见 [smell-catalog.md](smell-catalog.md) — 循环内分配 / 布局 / 热 IO / 多孔 / 删除映射跨 zperf / cperf / jsperf / jvmperf,以及更新的套件(每套件的 ID)。 + +证据相关:jsperf `STATIC-VS-V8`,cperf `STATIC-VS-O2`(保存在仓库之外)。 + +## 规则回归 + +存在成对的正/负例,以便我们之后可以问:这个规则是否仍然**有意义**(仍然与速度相关)并且仍然**生效**(在编译器/运行时升级后仍然有效)? + +两层: + +1. **Lint 测试夹具** — 在每个包的 `*-lint/fixtures/` 下: + - `bad/` 必须生成规则(正向:气味仍然匹配)。 + - `good/` 必须保持干净(负向:合法/提升/抑制的模式不会误报)。 + - 在工具链更新(Zig、cc、Node、JDK)时重新运行 `make check` / `npm test` / `gradlew test`。夹具通过 ≠ 实时声称。 + +2. **证据(速度门)** — 在 `packages/*/evidence/` 下: + - 仅在 `.md` + 在 `.jsonl` 之前/之后,才保留或升级规则至默认 **`error`**(每个套件保留自己的 `docs/evidence-policy.md`,例如 [cperf](../../packages/cperf/docs/zh-CN/evidence-policy.md))。 + - 当 **Node/V8**、**gcc/clang** 或 **JDK** 的主要版本变动时,重新运行基准测试;如果比率变化则更新中位数。 + - 剩余语料库计数和去优化/编译日志行计数 **不是** 速度声明,不会升级严重性。 + +跨套件气味图:[smell-catalog.md](smell-catalog.md)。每条规则的真实覆盖情况(固定装置 + 证据缺口):[rule-regression.md](rule-regression.md)。 diff --git a/docs/zh-CN/quickstart.md b/docs/zh-CN/quickstart.md new file mode 100644 index 0000000..2c1e414 --- /dev/null +++ b/docs/zh-CN/quickstart.md @@ -0,0 +1,78 @@ +# 快速开始 — 运行代码检查工具 + +从各自的目录构建并运行每个工具包。不同语言的要求不同;如果本页内容有出入,以工具包的 README 为准。 + +```sh +# Zig (Zig 0.17+) — package root is zperf-lint/ (build.zig lives there) +cd packages/zperf/zperf-lint && zig build -Doptimize=ReleaseFast +./zig-out/bin/zperf-lint ../examples + +# C (C11) — default --preset app +cd packages/cperf/cperf-lint && make && ./cperf-lint path/to/sources +./cperf-lint path/to/sources --preset all +make self-lint + +# JS (Node ≥18, zero npm deps) — default --preset app +cd packages/jsperf && node jsperf-lint/bin/jsperf-lint.js path/to/sources +npm run self-lint + +# JVM — pin JDK 21 for lint tests (CI uses Temurin 21) +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 # adjust to your install +cd packages/jvmperf/jvmperf-lint && ./gradlew test jar +bin/jvmperf-lint path/to/sources +# Optional JMH/JOL jars (not in git): bash ../scripts/fetch-lib.sh + +# Go +cd packages/goperf/goperf-lint && go build -o goperf-lint ./cmd/goperf-lint +./goperf-lint path/to/sources + +# Python (stdlib only) +cd packages/pyperf && PYTHONPATH=pyperf-lint python3 -m pyperf_lint path/to/sources + +# Swift (needs toolchain; smoke skips if absent) +cd packages/swiftperf/swiftperf-lint && swift build -c release +"$(swift build -c release --show-bin-path)/swiftperf-lint" path/to/sources + +# ArkTS / HarmonyOS (Node ≥18, zero deps) +cd packages/arktsperf && node arktsperf-lint/bin/arktsperf-lint.js path/to/sources + +# C++17 +cd packages/cppperf/cppperf-lint && make && ./cppperf-lint path/to/sources + +# C# (needs dotnet SDK; smoke skips if absent) +cd packages/csharpperf/csharpperf-lint && dotnet run -c Release -- path/to/sources + +# PHP +cd packages/phpperf && php phpperf-lint/phpperf-lint.php path/to/sources + +# Ruby +cd packages/rubyperf && ruby rubyperf-lint/rubyperf-lint.rb path/to/sources + +# Rust +cd packages/rustperf/rustperf-lint && cargo build --release +./target/release/rustperf-lint path/to/sources + +# Kotlin (needs kotlinc) +cd packages/kotlinperf && bash scripts/smoke-lint.sh + +# Objective-C (clang text scan of .m/.h) +cd packages/objcperf/objcperf-lint && make && ./objcperf-lint path/to/sources + +# Dart (needs dart SDK) +cd packages/dartperf/dartperf-lint && dart pub get && dart run bin/dartperf_lint.dart path/to/sources + +# Lua +cd packages/luaperf && lua luaperf-lint/luaperf-lint.lua path/to/sources + +# Elixir / Erlang (beamperf) +cd packages/beamperf && elixir beamperf-lint/beamperf_lint.exs path/to/sources + +# Nim (needs nim) +cd packages/nimperf && bash scripts/smoke-lint.sh +``` + +检查软件包是否存在(如果核心工具包缺失,则返回非零值): + +```sh +bash scripts/sync-check.sh +``` diff --git a/docs/zh-CN/release.md b/docs/zh-CN/release.md new file mode 100644 index 0000000..a4a88f0 --- /dev/null +++ b/docs/zh-CN/release.md @@ -0,0 +1,52 @@ +# 发布 + +## 我们发布什么 + +每次打 `v*` 标签,GitHub Actions 会构建一份 **源码分发**: + +- 产物:`xperf--packages.tar.gz`(例如:`xperf-v0.1.0-packages.tar.gz`) +- 内容:`packages/`、`docs/`、`site-app/`、`pages/`、`mkdocs.yml`、根文档(`README.md`、`LICENSE`、`NOTICE`、`AGENT.md`、`CONTRIBUTING.md`、`CHANGELOG.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md`)以及 `scripts/` +- 校验和:`xperf--packages.tar.gz.sha256` + +语料不打进源码包。不含 bench 二进制。v0.1 不上传 npm / crates.io / Maven。 + +## 暂不发布的内容 + +| 类型 | 原因 | +|------|------| +| 全部 19 个 kit 的注册表 package | 工具链各异;没有统一二进制 | +| 每个 kit 的预编译 lint 二进制 | 支持路径是源码运行(Node/Make/Go/Zig/Gradle/Cargo/…) | + +## 如何发版 + +1. 变更合入 `main`(CI 绿灯)。 +2. 打标签:`git tag v0.1.0 && git push origin v0.1.0` +3. 工作流 [release.yml](../../.github/workflows/release.yml) 运行 `scripts/pack-source-dist.sh`,上传产物并创建 GitHub Release。 + +本地演练:`bash scripts/pack-source-dist.sh v0.1.0 /tmp/xperf-dist` + +`workflow_dispatch` 可在不打新标签时重新打包。 + +## 文档站 + +GitHub Pages 从 `main` 构建指南。工作流:[pages.yml](../../.github/workflows/pages.yml)。源码包不含生成后的 HTML。 + +构建会把仓库名读进站点 URL(`https://.github.io//`)。若仓库改名,请更新 README 链接。 + +本地预览: + +```sh +bash scripts/build-pages.sh serve +``` + +打开 `http://localhost:3000/`。需要 Node.js 22+。命令会把 Markdown 复制到 `site-app/content`(已 gitignore)并启动文档应用。你打开的第一页会即时编译。 + +侧栏语言控件为 English / 中文。英文页在 `/`。中文页在 `/zh/`。没有中文译文的页面保持英文并会标明。 + +## CI 与发布 + +| 工作流 | 时机 | 任务 | +|--------|------|------| +| [ci.yml](../../.github/workflows/ci.yml) | PR / 推送到 main | 各 kit smoke/测试 + `sync-check` | +| [pages.yml](../../.github/workflows/pages.yml) | PR / 推送到 main | 文档站。推送到 `main` 还会部署 GitHub Pages | +| [release.yml](../../.github/workflows/release.yml) | `v*` 标签(或手动) | 源码包 + 校验和 → GitHub Release | diff --git a/docs/zh-CN/roadmap-next.md b/docs/zh-CN/roadmap-next.md new file mode 100644 index 0000000..73d6500 --- /dev/null +++ b/docs/zh-CN/roadmap-next.md @@ -0,0 +1,65 @@ +# XPerf —— 还缺什么(同一 playbook) + +来自闭环:**指南 ↔ 实测证据 ↔ 语料 dogfood ↔ 反向优化 ↔ 目标语言实现 ↔ 诚实边界**。 + +## 已经验证(保持) + +- 同语言检查器(Zig / C / JS)+ self-lint = 0。 +- 语料反向优化提升的是**精度**,本身不等于应用变快。 +- 静态味道 ↔ 运行时:**部分**(jsperf STATIC-VS-V8;**cperf STATIC-VS-O2**);每个规则族都要重新验证。 +- 跨 kit 目录:[smell-catalog.md](smell-catalog.md)。 +- 引擎语料(gcc/Node)作为「找加速」很弱;作为误报磨坊很强。 + +## 下一步高杠杆 + +### 1. 发现 → 基准门禁(**jp-delete-shape / jp-delete-array-hole / cp-alloc-hot 已完成**) +每条**保留**的规则在命名运行时(cc -O2 / Node 版本 / Zig ReleaseFast)上需要 ≥1 对夹具的 JSONL before/after。 +以 `packages/*/evidence/.jsonl` + 指南引用交付。 +在此之前,把 lint 只当评审提示(已是事实;做成机械门槛)。 + +### 2. 严重级别 / 模式(cperf/jsperf 中**已完成**) +- `error`:仅有实测同意的规则(如 jsperf jp-delete-array-hole、cperf cp-alloc-hot)。 +- `hint` / 默认 off:cp-printf-hot、jp-console-hot、jp-alloc-hot。 +- 配置:`--preset app|engine|all` + `--severity RULE=…` + `--strict-hints`。 + +### 3. 动态配套(不是更多静态规则) +| Kit | 配套 | 目的 | +|-----|------|------| +| jsperf | `--trace-deopt` / Perf harness(已起步) | JIT 真相 | +| cperf | 对 examples 包一层 `perf stat` / cachegrind | I/O vs 分配迷思 | +| zperf | 现有 bench + 可选 tracy | 保持 | +| jvmperf | JMH + EaProbe / `STATIC-VS-HOTSPOT`(已起步) | HotSpot 墙钟与 B/op;不是 PrintCompilation 计数 | + +静态回答「可能伤」;动态回答「在这里确实伤了」。 + +### 4. 盯自己的热代码,而不只是上游引擎 +把语料指向**用户/产品树**(以及 jsperf 的小程序 / 嵌入 JS)。 +成功指标:带基准的可行动真阳性,不是残留计数 ↓。 + +### 5. 跨 kit「同一味道,三种语言」(**目录已落地**) +见 [smell-catalog.md](smell-catalog.md)。规则出现时填空单元格(C 布局规则)。 +逼出诚实的可移植声明(V8 ≠ QuickJS ≠ C)。 + +### 6. jvmperf 槽位 +已在 `packages/jvmperf/` **落地**(`jvmperf-lint`、指南、JMH)。证据门禁与对照:`docs/evidence-policy.md`。 + +### 7. CI dogfood +PR 上跑 `make self-lint` / `npm run self-lint` / `zig build`;语料 lint 在钉死的 SHA 上**夜间**跑(不是每次提交)。 + +### 8. 负面结果作为一等文档 +把负面结果当一等文档发布 —— jsperf STATIC-VS-V8 + **cperf STATIC-VS-O2**(`#if 0` cp-printf-hot)已完成;继续扩展。 + +## 明确的非目标(目前) + +- 完整 AST/类型检查级分析。 +- 声称引擎 lint 合计 = 性能收益。 +- 仅从源码检测 JIT 层级 / IC 状态。 + +## 建议顺序 + +1. ~~严重级别拆分 + app preset(c/js)~~ —— **完成**。 +2. ~~jp-delete-shape / jp-delete-array-hole / cp-alloc-hot 的证据 JSONL~~ —— **完成**;见各 kit 的 `evidence/`。 +3. 一份产品/嵌入语料,而不是更多 gcc。 +4. ~~cperf 静态 vs `-O2`~~ —— **完成**(对照套件保留在仓库外)。 +5. ~~跨 kit smell 目录草稿~~ —— **完成**:[smell-catalog.md](smell-catalog.md);填剩余证据单元格。 +6. 动态配套(对 cperf examples 跑 `perf stat`)。 diff --git a/docs/zh-CN/rule-id-map.md b/docs/zh-CN/rule-id-map.md new file mode 100644 index 0000000..cba5173 --- /dev/null +++ b/docs/zh-CN/rule-id-map.md @@ -0,0 +1,250 @@ +# 规则 ID 映射(历史 — 数字 → 标识名) + +**别名尚未实现。** 规范 ID 仅为 **语义标识符**。此文件是 2026-10-22 重命名的一次性迁移参考;CLI **不**接受左侧的数字代码。 + +已移除的 ID(jitter 2026-10-22)**没有 slug**,不得重复使用:`CX001`、`CX003`、`CX004`、`BE002`、`KT004`、`OC001`、`OC004`、`PP004`。 + +## arktsperf + +| Old | Slug | +|-----|------| +| `AT001` | `at-alloc-hot` | +| `AT002` | `at-object-delete` | +| `AT003` | `at-console-hot` | +| `AT004` | `at-includes-hot` | +| `AT005` | `at-json-hot` | + +## 光束性能 + +| Old | Slug | +|-----|------| +| `BE001` | `be-list-concat-reduce` | +| `BE003` | `be-io-puts-hot` | +| `BE004` | `be-map-merge-hot` | +| `BE005` | `be-enum-map-filter-chain` | + +已移除(仅碑文): +- `BE002` + +## cperf + +| Old | Slug | +|-----|------| +| `CP001` | `cp-alloc-hot` | +| `CP002` | `cp-realloc-grow-one` | +| `CP003` | `cp-printf-hot` | +| `CP004` | `cp-strlen-in-cond` | +| `CP005` | `cp-strcat-hot` | + +## cppperf + +| Old | Slug | +|-----|------| +| `CX002` | `cx-string-append-unreserved` | + +已移除(仅墓碑): +- `CX001` +- `CX003` +- `CX004` + +## csharp性能 + +| Old | Slug | +|-----|------| +| `CS001` | `cs-list-add-no-capacity` | +| `CS002` | `cs-字符串-加-热` | +| `CS003` | `cs-console-hot` | +| `CS004` | `cs-regex-hot` | + +## dartperf + +| Old | Slug | +|-----|------| +| `DT001` | `dt-list-add-empty` | +| `DT002` | `dt-字符串-加-热` | +| `DT003` | `dt-print-hot` | +| `DT004` | `dt-growable-add-empty` | + +## goperf + +| Old | Slug | +|-----|------| +| `GP001` | `gp-make-hot` | +| `GP002` | `gp-字符串-加-热` | +| `GP003` | `gp-fmt-hot` | +| `GP004` | `gp-append-nil-slice` | +| `GP005` | `gp-defer-hot` | + +## jsperf + +| Old | Slug | +|-----|------| +| `JP001` | `jp-alloc-hot` | +| `JP002` | `jp-console-hot` | +| `JP003` | `jp-delete-shape` | +| `JP004` | `jp-删除数组空洞` | +| `JP005` | `jp-string-plus-in-loop` | +| `JP006` | `jp-地图-过滤-链` | + +## jvm性能 + +| Old | Slug | +|-----|------| +| `JP001` | `jp-封装原始集合` | +| `JP002` | `jp-string-concat-hot` | +| `JP003` | `jp-regex-hot` | +| `JP004` | `jp-format-hot` | +| `JP005` | `jp-unpresized-collection` | +| `JP006` | `jp-boxed-stream` | +| `JP007` | `jp-异常控制流` | +| `JP008` | `jp-legacy-synchronized-type` | +| `JP009` | `jp-多布尔字段` | +| `JP010` | `jp-枚举-键-哈希-集合` | +| `JP011` | `jp-collection-chain` | +| `JP012` | `jp-value-class-boxing` | +| `JP013` | `jp-lambda-to-non-inline-hot` | +| `JP014` | `jp-lazy-default-mode` | +| `JP015` | `jp-数据-复制-热` | +| `JP016` | `jp-android-sparse-array` | +| `JP017` | `jp-log-hot` | +| `JP018` | `jp-sleep-polling` | +| `JP019` | `jp-finalize` | +| `JP020` | `jp-昂贵-分配-热点` | +| `JP021` | `jp-hot-heavy-alloc` | +| `JP022` | `jp-hot-format-log` | +| `JP023` | `jp-热点系统调用` | +| `JP024` | `jp-位图解码-禁止重用` | +| `JP025` | `jp-hot-post-lambda` | +| `JP026` | `jp-ui-text-append` | +| `JP027` | `jp-baos-tobytearray-copy` | +| `JP028` | `jp-hot-payload-copy` | +| `JP030` | `jp-客户端-工厂-重建` | +| `JP031` | `jp-string-intern-hot` | +| `JP032` | `jp-非静态字段更新器` | + +## kotlin性能 + +| Old | Slug | +|-----|------| +| `KT001` | `kt-list-add-hot` | +| `KT002` | `kt-字符串-加-热` | +| `KT003` | `kt-println-hot` | + +已移除(仅保留墓碑): +- `KT004` + +## luaperf + +| Old | Slug | +|-----|------| +| `LU001` | `lu-table-insert-hot` | +| `LU002` | `lu-string-concat-hot` | +| `LU003` | `lu-print-hot` | + +## nimperf + +| Old | Slug | +|-----|------| +| `NM001` | `nm-seq-add-hot` | +| `NM002` | `nm-string-amp-in-loop` (**已删除**, 墓碑) | +| `NM003` | `nm-echo-hot` | +| `NM004` | `nm-seq-no-capacity` | + +## objcperf + +| Old | Slug | +|-----|------| +| `OC002` | `oc-nsstring-append-热` | +| `OC003` | `oc-nslog-hot` | + +已移除(仅墓碑): +- `OC001` +- `OC004` + +## phpperf + +| Old | Slug | +|-----|------| +| `PH001` | `ph-字符串-点-热` | +| `PH002` | `ph-数组-合并-重新绑定` | +| `PH003` | `ph-echo-hot` | + +## pyperf + +| Old | Slug | +|-----|------| +| `PP001` | `pp-str-plus-hot` | +| `PP002` | `pp-list-plus-rebind` | +| `PP003` | `pp-print-hot` | + +已移除(仅保留墓碑): +- `PP004` + +## rubyperf + +| Old | Slug | +|-----|------| +| `RB001` | `rb-字符串-加-热` | +| `RB002` | `rb-array-plus-rebind` | +| `RB003` | `rb-puts-hot` | +| `RB004` | `rb-hash-merge-in-loop` (**已移除**, 墓碑) | + +## rustperf + +| Old | Slug | +|-----|------| +| `RS001` | `rs-alloc-hot` | +| `RS002` | `rs-string-plus-in-loop` | +| `RS003` | `rs-println-hot` | +| `RS004` | `rs-格式-重新绑定-热` | +| `RS005` | `rs-vec-no-capacity` | +| `RS006` | `rs-collect-vec-hot` | + +## swiftperf + +| Old | Slug | +|-----|------| +| `SP001` | `sp-force-unwrap-in-loop` | +| `SP002` | `sp-string-plus-in-loop` | +| `SP003` | `sp-print-hot` | +| `SP004` | `sp-array-no-reserve` | +| `SP005` | `sp-map-joined-chain` | + +## zperf + +| Old | Slug | +|-----|------| +| `ZP001` | `zp-alloc-hot` | +| `ZP002` | `zp-append-without-reserve` | +| `ZP003` | `zp-页面分配器-小` | +| `ZP004` | `zp-debug-allocator` | +| `ZP005` | `zp-大结构按值传递` | +| `ZP006` | `zp-print-hot` | +| `ZP007` | `zp-unbuffered-io` | +| `ZP008` | `zp-missing-free` | +| `ZP009` | `zp-extern-struct-填充` | +| `ZP010` | `zp-广义运行时安全` | +| `ZP011` | `zp-packable-bools` | +| `ZP012` | `zp-usize-索引-字段` | +| `ZP013` | `zp-aos-单字段` | +| `ZP014` | `zp-捕获-不可达-分配` | +| `ZP015` | `zp-超大枚举标签` | +| `ZP016` | `zp-可选标量字段` | +| `ZP017` | `zp-bool-array` | +| `ZP018` | `zp-静态字符串映射` | +| `ZP019` | `zp-runtime-div-hot` | +| `ZP020` | `zp-queue-ordered-remove` | +| `ZP021` | `zp-shift-on-read` | +| `ZP022` | `zp-每包分配` | +| `ZP023` | `zp-不变浮点除法` | +| `ZP024` | `zp-map-put-without-ensure` | +| `ZP025` | `zp-catch-unreachable-io` | +| `ZP026` | `zp-竞技场-重置-全部免费` | +| `ZP027` | `zp-创建-销毁-热` | +| `ZP028` | `zp-固定大小分配-热点` | + +## 2026-10-27 作用域重命名(`*-in-loop` → `*-hot`) + +默认匹配现在是 **`--scope all`**(也报告循环之外的情况)。包含 `in-loop` 的标识符已被 **重命名**,以使名称不再暗示仅循环作用域。旧的标识符已在每个工具包的 `evidence/_removed/*.REMOVED.md` 下作废,并且 **绝不能再次使用**。活动 ID 列在每个工具包的 `docs/rule-ids.md` 中。在工具包支持的情况下,可以选择 `--scope loop` 以匹配以前仅限循环的情形。 + +上面的数字 → slug 行保持了 **历史** 2026-10-22 的映射(许多目标在 2026-10-27 又被重新命名)。 diff --git a/docs/zh-CN/rule-regression.md b/docs/zh-CN/rule-regression.md new file mode 100644 index 0000000..fe183f6 --- /dev/null +++ b/docs/zh-CN/rule-regression.md @@ -0,0 +1,379 @@ +# 规则回归覆盖 + +截至 **2026-10-22**(亚洲/上海)。空/反向深度分析 **已落地**(rb-hash-merge-in-loop 固定装置/消息;cx-string-append-unreserved / at-includes-hot 文档;测量为无用的墓碑;无严重性升级)。证据填充批次 A:goperf/pyperf/cppperf/phpperf/rustperf/luaperf/rubyperf;nim 被阻止。合同:[playbook.md § 规则回归](playbook.md)。每套件关卡:`packages/*/docs/evidence-policy.md`。气味:[smell-catalog.md](smell-catalog.md) + +**柱** + +- **坏 / 好**:`packages//*-lint/fixtures/` 下的 lint 固件 +- **证据**:`packages//evidence/` 下的 `.md` 和 `.jsonl`(速度门) +- **严重性**:包默认 / 策略(cperf·jsperf `--preset app`; jvmperf 策略映射; zperf 历史上所有发现都会导致 CI 失败) +- **待定措施**:在提交/升级到 `error` 之前需要前/后 JSONL(兄弟自己的基准 — 该矩阵不会发明比率) + +固定装置测试证明规则仍然**有效**;证据证明它仍然**意味着速度**。语料库残余计数 ≠ 两者。 + +--- + +**范围 (2026-10-27):** 默认匹配是 `--scope all`(或相应的套件等效项)跨套件;选择 `--scope loop` 来启用之前仅限循环的行为。以前包含 `in-loop` 的标识符已重命名为 `*-hot` 并被作废;详见每个套件的 `docs/rule-ids.md` 重命名表。 + +^ cperf(cp-alloc-hot – cp-printf-hot – cp-strlen-in-cond – cp-strcat-hot) (英语). + +测试夹具使用描述性名称(而不是 `cp-alloc-hot.c`)。默认值来自 `docs/evidence-policy.md` / `--preset app`。 + +| 规则 | 错误设置 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| cp-alloc-hot | `bad/cp-alloc-hot_malloc_loop.c` | 是的 (`arena_hoisted.c`,…) | **是** `evidence/cp-alloc-hot.*` | **错误** | 测量门存在 | +| cp-realloc-grow-one | `bad/cp-realloc-grow-one_realloc_grow.c` | 是 (`capacity_doubling.c`, `realloc_wrapper.c`) | **是** | **错误** | 测得约 12.6 倍 realloc 增长相比翻倍 | +| cp-printf-hot | `bad/cp-printf-hot_printf_loop.c` | 是 (`dump_*`, `ifdef_dump_*`, `treeview_*`, …) | **是** | **关闭** | 热 ~4.8× unbuf/buf;`#if 0` ~1× → 保持关闭 | +| cp-strlen-in-cond | `bad/cp-strlen-in-cond_strlen_cond.c` | 是的 (`cp-strlen-in-cond_strlen_hoisted.c`, …) | 还没有 | 提示 | 没有实际运行时间基准;仅语料真阳性 | +| cp-strcat-hot | `bad/cp-strcat-hot_strcat_loop.c` | 是的 (`cp-strcat-hot_strcat_once.c`) | 还没有 | **关闭** | G3 降级(CLI/路径 OK-惯用法);没有实际运行时间基准 | +| cp-sprintf-hot | `bad/cp-sprintf-hot_sprintf_loop.c` | 是 | jsonl | **提示** | 微基准测试在一个形状上约 13.9×;保持提示;未经过语料库分类 | +| cp-fopen-hot | `bad/cp-fopen-hot_fopen_loop.c` | 是 | jsonl | **提示** | 单个形状微基准约423.4×;保持提示;未进行语料库分流 | +| cp-gettime-hot | `bad/cp-gettime-hot_gettime_loop.c` | 是 | jsonl | **提示** | 微基准 ~33.2× 在一个形状上;保持提示;未经过语料库筛选 | +| cp-wide-bool-flags | `bad/cp-wide-bool-flags_ints.c` | 是的 (`cp-wide-bool-flags_bits.c`) | jsonl | **提示** | 微基准 ~19.1× 在一个形状上; 保持提示; 未经过语料库分类 | + +--- + +## jsperf(slug ID) + +描述性夹具名称。默认值:`--preset app`(jp-console-hot=off)。 + +| 规则 | 错误夹具 | 良好 / 消极 | 证据 md+jsonl | 严重性 默认 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| jp-alloc-hot | `bad/jp-alloc-hot_new_in_loop.js`, `jp-alloc-hot_push_object.js` | 是 (`prealloc.js`, `jp-alloc-hot_reuse_buffer.js`, …) | **是** | **关闭** (应用) | ~19× / ~6.4× 实际时间(分配/GC);G3 OK-习惯降级 | +| jp-console-hot | `bad/jp-console-hot_console_loop.js` | 是 (`jp-console-hot_console_outside_loop.js`) | **是** | **关闭**(应用)/ 提示 | 如果丢弃 stdout 大约 1328×;依赖环境 → 不是 CI 错误 | +| jp-delete-shape | `bad/jp-delete-shape_delete.js` | 是 (`no_delete.js`, …) | **是** `evidence/jp-delete-shape.*` | **关闭** (应用) | G3 OK-惯用语 降级 | +| jp-delete-array-hole | `bad/jp-delete-array-hole_holey_delete.js` | 是 (`no_holey_delete.js`,…) | **是** `evidence/jp-delete-array-hole.*` | **错误** | 已测量 | +| jp-string-plus-in-loop | 已移除(`evidence/_removed/fixtures/` 下的测试用例) | 纪念碑 | `_removed/` 下的 jsonl | **已移除** | 匹配形状时 `push`+`join` 并非约 2 倍 | +| jp-map-filter-chain | `bad/jp-map-filter-chain_map_filter_chain.js` | 是 (`jp-map-filter-chain_single_map.js`, 注释夹具) | **是** | 提示 | 35000 行 ~4.6×/9.8×;3 行过滤+映射 1.28×;保持提示 | +| jp-regex-ctor-hot | `bad/jp-regex-ctor-hot_ctor_loop.js` | `good/jp-regex-ctor-hot_ctor_once.js`, pattern-varies | jsonl | 提示 | Node 26 修复模式 ~4.4×;循环绑定未报告 | +| jp-json-hot | `bad/jp-json-hot_json_loop.js` | `good/jp-json-hot_json_once.js`, 文本变化 | jsonl | 提示 | Node 26 未更改文本约 140×;未报告 `rows[i]` | +| jp-dom-query-hot | `bad/jp-dom-query-hot_query_loop.js` | `good/jp-dom-query-hot_query_once.js`, selector-varies | jsonl | 提示 | Node 26 小型 linkedom 文档 ~16×;循环绑定未报告 | +| jp-intl-datetimeformat-hot | `bad/jp-intl-datetimeformat-hot_format_loop.js` | `good/jp-intl-datetimeformat-hot_format_once.js`, 地区变化 | jsonl | 提示 | Node 26 ~36×;循环绑定未报告 | +| jp-readfile-sync-hot | `bad/jp-readfile-sync-hot_read_loop.js` | `good/jp-readfile-sync-hot_read_once.js`,for-of 头部,路径变化 | jsonl | 提示 | 仅正文;Node 26 当路径保持不变时 ~114× | +| jp-clock-hot | `bad/jp-clock-hot_clock_loop.js` | `good/jp-clock-hot_clock_once.js`,await 循环,嵌套函数/箭头函数 | jsonl | 提示 | 紧密循环 ~26×/~19×;`await` 循环未报告;跳过嵌套函数体 | +| jp-query-all-star | `bad/jp-query-all-star_star_query.js`, `jp-query-all-star_in_template.js` | `good/jp-query-all-star_narrow.js`, template narrow | 尚无 | 提示 | full-tree `*`; 消息未说明缓存该节点; 看到模板脚本 | +| jp-execjs-in-timer | `bad/jp-execjs-in-timer_interval.js` | `good/jp-execjs-in-timer_once.js` | 还没有 | 提示 | 在 setInterval/setTimeout 中构建脚本;安装一次 + 短时间清理是干净的 | +| jp-clone-per-event | `bad/jp-clone-per-event_structured.js`、`jp-clone-per-event_json_roundtrip.js` | `good/jp-clone-per-event_field.js`、无关克隆 | 还没有 | 提示 | 仅事件类参数(`e`/`event`/…) | + +--- + +## zperf(zp-alloc-hot–zp-fixed-size-alloc-hot) + +所有28个配对固定装置。证据位于 `packages/zperf/evidence/`(Zig **0.17.0** `-Ofast`,2026-10-21)。政策:`packages/zperf/docs/evidence-policy.md`。CLI 在任何发现上仍然失败;**候选错误** = 预设生效时政策升级。 + +| 规则 | 坏 | 好 | 证据 | rules.zig sev | 备注 | +|------|-----|------|----------|---------------|-------| +| zp-alloc-hot | 是 | 是 | **是** | 警告 | ~**1.4×** GPA 对比 arena — 温和;保持警告 | +| zp-append-without-reserve | 是 | 是 | **是** | 信息 | ~**2.1×** 添加保留 | +| zp-page-allocator-small | 是 | 是 | **是** | 警告 | ~**341×** 页面 vs GPA — **候选错误** | +| zp-debug-allocator | 是 | 是 | **不适用** | 警告 | 仅限静态(模式切换) | +| zp-large-struct-by-value | 是 | 是 | **不确定** | 信息 | ReleaseFast 省略的微基准 | +| zp-print-hot | 是 | 是 | **是** | 警告 | ~**6e4×** 打印对比静默 — 环境输入/输出,不升级 | +| zp-unbuffered-io | 是 | 是 | **是** | 警告 | ~**49×** 空与 4KiB — **候选错误** | +| zp-missing-free | 是 | 是 | **不适用** | 警告 | 正确性(泄漏) | +| zp-extern-struct-padding | 是 | 是 | **是** | 警告 | ~**1.2×** 填充步幅 — 适中 | +| zp-broad-runtime-safety | 是 | 是 | **待定** | 警告 | 需要 ReleaseSafe 措施 | +| zp-可打包布尔值 | 是 | 是 | **待定** | 信息 | 密度 | +| zp-usize-index-field | 是 | 是 | **待定** | 信息 | 密度 | +| zp-aos-single-field | 是 | 是 | **是** | 提示 | ~**6.6×** AoS 对 SoA | +| zp-catch-unreachable-alloc | 是 | 是 | **不适用** | 警告 | 正确性(未定义行为) | +| zp-oversized-enum-tag | 是 | 是 | **不适用** | 信息 | 静态大小/ABI | +| zp-可选标量字段 | 是 | 是 | **待定** | 信息 | 密度 | +| zp-布尔数组 | 是 | 是 | **是** | 信息 | ~**30×** 布尔数组 vs 位集 | +| zp-静态字符串映射 | 是 | 是 | **是** | 信息 | ~**2.5×** 静态字符串映射 | +| zp-runtime-div-hot | 是 | 是 | **是** | 提示 | ~**12×** 模与掩码 +| zp-queue-ordered-remove | 是 | 是 | **是** | 警告 | ~**466×** orderedRemove 与 ring — **候选错误** | +| zp-读取时移动 | 是 | 是 | **是** | 警告 | ~**17×** copyForwards 与 cursor — **潜在错误** | +| zp-per-packet-alloc | 是 | 是 | **是**(共享 zp-alloc-hot) | 警告 | 热路径分配;相同 ~1.4× | +| zp-不变浮点除法 | 是 | 是 | **是** | 提示 | ~**2.0×** 浮点除法 vs 倒数 | +| zp-map-put-without-ensure | 是 | 是 | **是** | 信息 | ~**2.8×** 映射确保 | +| zp-catch-unreachable-io | 是 | 是 | **不适用** | 警告 | 正确性 | +| zp-arena-reset-free-all | 是 | 是 | **是** | 警告 | ~**3.2×** free_all 对比 retain | +| zp-create-destroy-hot | 是 | 是 | **是** | 警告 | ~**11×** 创建/销毁 vs 池 — **潜在错误** | +| zp-fixed-size-alloc-hot | 是 | 是 | **是** | 警告 | ~**8.5×** 固定分配 vs 池 — **候选错误** | + +--- + +## jvmperf (jp-boxed-primitive-collection–jp-non-static-field-updater) — 摘要 + +ID 是 **jvmperf** `jp-*` 标识,而不是 jsperf。示例:大多是 `bad/.*` + 匹配的 `good/`;某些集合/装箱规则仅限 Kotlin;jp-boxed-stream / jp-non-static-field-updater 仅限 Java。**vt-unsafe-threadlocal-pool** 仅为指导,在 `jvmperf-lint/fixtures/guide/` 下(不随包发布)。 + +| 规则 | 不良设备 | 良好 / 消极 | 证据 md+jsonl | 严重性 默认 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| jp-boxed-primitive-collection | 是(java+kt) | 是 | **是** | **错误** | ~5.96× STATIC-VS-HOTSPOT | +| jp-string-concat-hot | 是 (java+kt) | 是 | **是** | **错误** | J23 ~14.2× | +| jp-regex-hot | 是 (java+kt) | 是 | **是** | 提示 | J26 ~1.36× | +| jp-format-hot | 是(Java+KT) | 是 | **是** | **错误** | 约17× | +| jp-unpresized-collection | 是 (java+kt) | 是 | **是** | 提示 | 部分 ~1.6× | +| jp-boxed-stream | 是(Java) | 是 | **是** | **错误** | J32 ~149× | +| jp-exception-control-flow | 是(Java+KT) | 是 | **是** | **错误** | J32 ~64× | +| jp-legacy-synchronized-type (**已移除**) | 是 (java+kt,已归档) | 是 | **是** (~1.08×) | — | 测量无胜利;墓碑 `_removed/` | +| jp-多个布尔字段 (**已移除**) | 是 (java+kt,已归档) | 是 | **是** (1.0×) | — | COH 无占用优势;墓碑 `_removed/` | +| cx-sstream-in-loop (**已移除**) | 已归档 | 已归档 | -O2 下 1.24× | — | 字符串流重用;墓碑 | +| pp-regex-compile-in-loop (**已移除**) | 已存档 | 已存档 | 小于 3× | — | CPython 缓存 `re.compile`; 墓碑 | +| rs-map-new-in-loop | `bad/rs-map-new-in-loop_map_loop.rs` | 是 | **是** | 提示 | 两次安静的 rustc 1.99 运行大约 1.51× 和 1.53×;已恢复 | +| pp-datetime-format-in-loop (**已删除**) | 已归档 | 已归档 | 不提升 | — | 更改输入无法跳过调用 | +| gp-time-parse-in-loop (**已删除**) | 已归档 | 已归档 | 不提升 | — | 输入更改无法跳过调用 | +| rb-date-parse-in-loop (**已删除**) | 已归档 | 已归档 | 不提升 | — | 更改输入无法跳过调用 | +| jp-enum-key-hash-collection | 是 (java+kt) | 是 | **是** | **错误** | EnumMap ~2.1× | +| jp-collection-chain | 是 (kt) | 是 | **是** | **错误** | JDK 27 / Kotlin 2.4.21 ~265× | +| jp-value-class-boxing | 是 (kt) | 是 | **是** | **错误** | JDK 27 ~4.0× 时间,~5× B/操作 | +| jp-lambda-to-non-inline-hot | 是 (kt) | 是 | **是** | **错误** | JDK 27 分配 ~244×;时间 ~0.47× | +| jp-lazy-default-mode | 是 (kt) | 是 | 仅 md 存根 | 提示 | 待定 — 情境懒加载 | +| jp-data-copy-hot | 是 (kt) | 是 | **是** | **错误** | JDK 27 ~208× 时间,分配仍然 ~10001× | +| jp-android-sparse-array | 是 | 是 | md 桩 | 提示 | 待定 ART | +| jp-log-hot | 是 | 是 | **是** | **错误** | J32 ~28×(调试关闭) | +| jp-sleep-polling | 是 | 是 | md 存根 | 提示 | 待定的睡眠设计 | +| jp-finalize | 是 | 是 | md stub | 提示 | 待完成/Cleaner | +| jp-expensive-alloc-hot | 是 | 是 | **是** | 提示 | 负 tiny-pool | +| jp-热-重-分配–jp-用户界面-文本-追加 | 是 | 是 | 中等存根 | 提示 | 待定 ART/设备 | +| jp-baos-tobytearray-copy | 是 | 是 | **是** | **错误** | J32 ~12.8× | +| jp-热负载复制 | 是 | 是 | **是** | **错误** | J32 ~15.8× | +| vt-unsafe-threadlocal-pool | 仅作指南 | 指南 | **md 存根** ([vt-unsafe-threadlocal-pool.md](../../packages/jvmperf/evidence/zh-CN/vt-unsafe-threadlocal-pool.md)) | — | 未发布 / 先指南 | +| jp-client-factory-recreate | 是 | 是 | **是** | **错误** | J32 HttpClient ~5e5× | +| jp-string-intern-hot | 是 | 是 | **是** | **错误** | J32 intern ~6.6× | +| jp-non-static-field-updater | 是 (Java) | 是 | **是** | 提示 | 创建 ~165×;增加 ~1.0× | + +今天的 CLI 仍可能在任何发现上失败;策略严重性是保持/升级合同,直到预设到位。 + +--- + +--- + +**脚手架证据合同:** 在 JSONL 存在之前,每条脚手架规则必须有一个命名的待处理存根 `packages//evidence/.md`(不仅仅是一个文件夹的 README)。这满足了注册表的一致性,而无需发明比例。 + +## goperf (gp-make-hot–gp-defer-hot) — 语料库就绪 + +| 规则 | 坏的装置 | 好的装置 | 证据 | 默认 | 备注 | +|------|-------------|--------------|----------|---------|-------| +| gp-make-hot | `bad/gp-make-hot_make_loop.go` | 是 | **是** | **关** | G4 ~265× make-each;保持关闭(G3 可用成语) | +| gp-string-plus-hot | `bad/gp-string-plus-hot_string_plus.go` | 是 | **是** | **关闭** | G4 ~2919× += 与 Builder;保持关闭 | +| gp-fmt-hot | `bad/gp-fmt-hot_fmt_loop.go` | 是 | **是** | **关闭** | G4 ~1.8–2.8× fmt 对比 strconv;保持关闭 | +| gp-append-nil-slice | `bad/gp-append-nil-slice_append_grow.go` | `good/gp-append-nil-slice_presize.go` | **是** | 提示 | G4 ~12.2× append 增长 vs 预设大小 | +| gp-defer-hot | `bad/gp-defer-hot_defer_loop.go` | `good/gp-defer-hot_close_each.go` + 闭包 | **是** | 提示 | G4 ~33× 循环中的 defer 与 每次关闭 | +| gp-regexp-compile-hot | `bad/gp-regexp-compile-hot_compile_loop.go` | `good/gp-regexp-compile-hot_compile_once.go` | jsonl | 提示 | 微基准 ~7.1× 对一个形状;保留提示;未经过语料库分流 | +| gp-json-marshal-hot | `bad/gp-json-marshal-hot_marshal_loop.go` | `good/gp-json-marshal-hot_marshal_once.go` | jsonl | 提示 | 微基准测试在一个形状上大约快 368.4 倍;保留提示;未经过语料库筛选 | +| gp-reflect-hot | `bad/gp-reflect-hot_reflect_loop.go` | `good/gp-reflect-hot_reflect_once.go` | jsonl | 提示 | 微基准 ~1×,无实际开销;保持提示 | + +## pyperf(pp-str-plus-hot–pp-print-hot)——语料库准备就绪 + +| 规则 | 坏的装置 | 好的装置 | 证据 | 默认 | 备注 | +|------|-------------|--------------|----------|---------|-------| +| pp-str-plus-hot | `bad/pp-str-plus-hot_str_plus.py` | 是 | **是** | **关闭** | G4 ~5.5× += 与 join;保持关闭 | +| pp-list-plus-rebind | `bad/pp-list-plus-rebind_list_plus.py` | 是 | **是** | 提示 | G4 ~1117× list+ 重新绑定;保持提示 | +| pp-print-hot | `bad/pp-print-hot_print_loop.py` | 是 | **是** | **关闭** | G4 ~3.4× 打印循环;保持关闭 | +| pp-json-hot | `bad/pp-json-hot_json_loop.py` | `good/pp-json-hot_json_once.py` | jsonl | 提示 | 微基准约81.6×在一个形状上;保持提示;未经过语料库筛选 | +| pp-deepcopy-hot | `bad/pp-deepcopy-hot_deepcopy_loop.py` | `good/pp-deepcopy-hot_deepcopy_once.py` | jsonl | 提示 | 微基准测试在一个形状上约快 21.8×;保留提示;未按语料库分类 | +| *(已移除 — 无标识)* 循环中重新临时处理 | `bad/pp004_re_adhoc.py`(已归档) | `good/pp004_re_compiled.py` | **是** | — | 墓碑;G4 ~1.35× / 热启动可能会逆转 | + +## swiftperf (sp-force-unwrap-in-loop–sp-map-joined-chain) — 语料库就绪 + +| ID | 坏的装置 | 好的 | 证据 | 默认 | 备注 | +|----|-------------|------|----------|---------|-------| +| sp-force-unwrap-in-loop | force_loop | 是 | **已阻止** | **关闭** | G4 阻止 — Linux 上没有 `swiftc` | +| sp-string-plus-in-loop | string_plus | 是 | **已阻止** | **关闭** | G4 已阻止 — 无 Swift 工具链 | +| sp-print-hot | 打印循环 | 是 | **阻止** | **关闭** | G4 阻止 — 无 Swift 工具链 | +| sp-array-no-reserve | append_no_reserve | 是 | **阻塞** | 提示 | G4 阻塞 — 无 Swift 工具链 | +| sp-map-joined-chain | map_joined | 是 | **阻塞** | 提示 | G4 阻塞 — 无 Swift 工具链 | +| sp-dateformatter-hot | `sp-dateformatter-hot_formatter_loop.swift` | 是 | jsonl | 提示 | 微基准测试 ~29.8× 在一个形状上;保持提示;未进行语料库筛选 | +| sp-json-decode-hot | `sp-json-decode-hot_decode_loop.swift` | 是 | jsonl | 提示 | 微基准 ~1958.0× 在一个形状上;保持提示;未经过语料库分类 | +| sp-regex-hot | `sp-regex-hot_regex_loop.swift` | 是 | jsonl | 提示 | 微基准测试 ~16.4× 单一形状;保持提示;未经过语料库分类 | + +r4 n=32 TP 65.6% FP+DOC+SKIP 0% → **语料库准备好:是**(文本检查回退;当 Swift 存在时使用 SPM)。G4 2026-10-22:**阻止**(没有假 JSONL)。2026-10-23 的三个提示不在该裁定范围内。 + +## arktsperf(在 at-alloc-hot–在 at-json-hot)—— 语料库准备就绪 + +| 编号 | 不良装置 | 良好 | 证据 | 默认 | 注意事项 | +|----|-------------|------|----------|---------|-------| +| at-alloc-hot | new_loop | 是 | **是**(节点代理) | **关闭** | G4 ~33× 新建与重用;保持关闭 | +| at-object-delete | 删除 | 是 | **是**(不确定) | **关闭** | G4 ~1.0×(填充主导);孤立删除适中;保持关闭 | +| at-console-hot | console_loop | 是 | **是**(节点代理) | **关闭** | G4 ~208×;保持关闭 | +| at-includes-hot | includes_loop | 是 (+ `at-includes-hot_string_lut`) | **是** (节点代理) | 提示 | G4 ~29× includes→Set; String.indexOf→LUT (Map~1×); 保持提示 | +| at-json-hot | json_loop | 是 | **是**(节点代理) | 提示 | G4 ~2.2× 解析 / ~1.7× 字符串化;保持提示 | + +r3 n=56 TP 66.1% FP+DOC+SKIP 0% → **语料准备就绪:是**。G4 是 **Node/V8 代理**,不是 ArkTS VM — 不会升级为错误。 + +## cppperf(`cx-string-append-unreserved`)— 语料库就绪 + +活动规则:**cx-string-append-unreserved**(默认 **提示**)。Vector-push / iostream / `.at()` 规则 **已移除**(墓碑)。专用 C++17(≠ cperf)。 + +| 规则 | 错误夹具 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| *(已移除 — 无标识)* 向量在未保留空间的情况下推入 | `bad/cx001_push_loop.cpp`(已存档) | 是 | **是** | — | 墓碑;G4 ~1.58× @-O2 | +| cx-string-append-unreserved | `bad/cx-string-append-unreserved_string_plus.cpp` | 是的 (`cx-string-append-unreserved_append_reserve.cpp`) | **是的** | **提示** | G4 ~1.09× 字符 @-O2; 块 ~1.8×; 保持提示 | +| cx-regex-ctor-hot | `bad/cx-regex-ctor-hot_regex_loop.cpp` | `good/cx-regex-ctor-hot_regex_once.cpp` | jsonl | **提示** | 微基准 ~1.55×,小;保持提示 | +| cx-endl-hot | `bad/cx-endl-hot_endl_loop.cpp` | `good/cx-endl-hot_newline.cpp` | jsonl | **提示** | 微基准 ~1×,无明显成本;保持提示 | +| *(已移除 — 无标识)* 循环中的 iostream | `bad/cx003_iostream_loop.cpp`(已存档) | 是 | **是** | — | 墓碑;G4 ~1.01× | +| *(已移除 — 无标识)* `.at()` 在循环中 | `bad/cx004_at_loop.cpp`(已存档) | 是 | **是** | — | 墓碑;G4 ~0.85× 反向 @-O2 | + +--- + +## csharpperf (cs-list-add-no-capacity–cs-regex-hot) — 语料库就绪 + +cs-list-add-no-capacity–cs-console-hot 默认 **关闭**,G3 后。**cs-regex-hot** 默认 **提示**。G4 在 .NET 8.0.426(2026-10-22);**无严重性升级**。 + +| 规则 | 不良设备 | 良好 / 消极 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| cs-list-add-no-capacity | `bad/cs-list-add-no-capacity_add_loop.cs` | 是 (`cs-list-add-no-capacity_capacity.cs`) | **是** | **关闭** | G4 ~13× 列表容量;保持关闭 | +| cs-string-plus-hot | `bad/cs-string-plus-hot_string_plus.cs` | 是 (`cs-string-plus-hot_builder.cs`) | **是** | **关** | G4 ~3758× string+=;保持关闭 | +| cs-console-hot | `bad/cs-console-hot_console_loop.cs` | 是 (`cs-console-hot_console_outside.cs`) | **是** | **关闭** | G4 ~6× 控制台为空;保持关闭 | +| cs-regex-hot | `bad/cs-regex-hot_regex_loop.cs` | 是 (`cs-regex-hot_compiled_regex.cs`) | **是** | **提示** | G4 ~2.6× IsMatch 对比 Compiled;保持提示 | +| cs-linq-materialize-hot | `bad/cs-linq-materialize-hot_tolist_loop.cs` | `good/cs-linq-materialize-hot_tolist_once.cs` | jsonl | 提示 | .NET 10 ~38.9× 在一个形状上;保持提示;非语料库三角化 | +| cs-format-hot | `bad/cs-format-hot_format_loop.cs` | `good/cs-format-hot_format_once.cs` | jsonl | 提示 | .NET 10 ~5.0× 在一个形状上;保持提示;非语料库分类 | + +--- + +## phpperf (ph-string-dot-hot–ph-echo-hot) — 语料库就绪 + +ph-array-merge-rebind 默认 **错误**,在 G4 之后(`ph-array-merge-rebind.md`+`jsonl`)。ph-string-dot-hot 默认 **关闭**(G3 OK-惯用语重)和 ph-echo-hot **提示**;两者都有测量的 JSONL。 + +| 规则 | 不良设备 | 良好 / 消极 | 证据 md+jsonl | 严重性 默认 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| ph-string-dot-hot | `bad/ph-string-dot-hot_str_dot.php` | 是(`ph-string-dot-hot_implode.php`) | **是** | **关闭** | G4 ~3.3× .= 与 implode 对比;保持关闭 | +| ph-array-merge-rebind | `bad/ph-array-merge-rebind_array_merge.php` | 是 (`ph-array-merge-rebind_push.php`) | **是** | **错误** | array_merge 重新绑定;G4 2026-10-22 | +| ph-echo-hot | `bad/ph-echo-hot_echo_loop.php` | 是 (`ph-echo-hot_echo_outside.php`) | **是** | 提示 | G4 ~4.4× 每次回显;保持提示 | + +--- + +## rubyperf (rb-string-plus-hot–rb-file-read-hot) — 语料库准备就绪 + +rb-string-plus-hot–rb-puts-hot 在 G3 之后默认 **关闭**。**rb-hash-merge-in-loop** 已于 2026-10-23 删除:字面量构建比 `merge` 更慢,并且分配更多。 + +| 规则 | 错误夹具 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| rb-string-plus-hot | `bad/rb-string-plus-hot_str_plus.rb` | 是 | **是** | **关** | G4 ~32× += 与 <<; 保持关闭 | +| rb-array-plus-rebind | `bad/rb-array-plus-rebind_array_plus.rb` | 是 | **是** | **关闭** | G4 ~181× array+ 重新绑定;保持关闭 | +| rb-puts-hot | `bad/rb-puts-hot_puts_loop.rb` | 是 | **是** | **关** | G4 ~5.1× 每次 puts;保持关闭 | +| rb-hash-merge-in-loop (**已移除**) | 已归档 | 已归档 | 墓碑 | — | Ruby 4.0.8 直接量比 merge 慢 (~0.78×) 且对象数量约为 2× | +| rb-regexp-new-hot | `bad/rb-regexp-new-hot_regexp_loop.rb` | `good/rb-regexp-new-hot_regexp_once.rb` | jsonl | 提示 | 微基准测试在一个形状上大约快 7.7 倍;保留提示;未经过语料库筛选 | +| rb-json-hot | `bad/rb-json-hot_json_loop.rb` | `good/rb-json-hot_json_once.rb` | jsonl | 提示 | 微基准约10.5×在一个形状上;保持提示;未经过语料库筛选 | + +## rustperf (rs-alloc-hot–rs-collect-vec-hot) — 语料库准备就绪 (r4) + +rs-vec-no-capacity/rs-collect-vec-hot 重新设计 2026-10-22。r4 clap 残差 **6×TP** / FP+DOC+SKIP **0%** → **语料库就绪:是**。rs-format-rebind-hot `format!` 重新绑定 → **错误** (~99×);rs-string-plus-in-loop/rs-vec-no-capacity/rs-collect-vec-hot → **提示**;rs-alloc-hot → **关闭**(G4 测量,保持关闭);rs-println-hot → **提示**(已测量)。 + +| 规则 | 错误设置 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| rs-alloc-hot | `bad/rs-alloc-hot_clone_loop.rs` | 是 (`rs-alloc-hot_with_capacity.rs`) | **是** | **关闭** | 分配/克隆;G4 ~2.7× Vec / ~1.1× 克隆 — 保持关闭 | +| rs-string-plus-in-loop | `bad/rs-string-plus-in-loop_string_plus.rs` | 是 (`rs-string-plus-in-loop_write.rs`, `rs-string-plus-in-loop_tiny_sep.rs`) | **是** | 提示 | 仅 `+=` / `push_str`;约 1× 均摊 +| rs-println-hot | `bad/rs-println-hot_println_loop.rs` | 是 (`rs-println-hot_println_outside.rs`) | **是** | 提示 | G4 ~12.6× 每次写入与缓冲区; 保持提示 | +| rs-format-rebind-hot | `bad/rs-format-rebind-hot_format_rebind.rs` | 是 (`rs-format-rebind-hot_write.rs`, `rs-format-rebind-hot_let_format.rs`, `rs-format-rebind-hot_brace_wrap.rs`) | **是**(别名 G4) | **错误** | `format!` 重绑定重用;约 99× 对比 `write!` | +| rs-vec-no-capacity | `bad/rs-vec-no-capacity_vec_new_push.rs` | 是的(`rs-vec-no-capacity_with_capacity.rs`, `rs-vec-no-capacity_cfg_test.rs`) | **是的** | 提示 | G4 ~1.0× 大 / ~2.1× 小 构建器;保持提示 | +| rs-collect-vec-hot | `bad/rs-collect-vec-hot_collect_in_loop.rs` | 是(`rs-collect-vec-hot_count_no_collect.rs`) | **是** | 提示 | G4 ~1.8× collect 对比 count;保持提示 | +| rs-regex-new-hot | `bad/rs-regex-new-hot_regex_loop.rs` | `good/rs-regex-new-hot_regex_once.rs` | 仅限 md | 提示 | 待处理措施;未进行语料库分类 | + +--- + +## kotlinperf (kt-list-add-hot–kt-println-hot) — 语料库就绪 + +kt-list-add-hot–kt-println-hot 默认 **关闭**(G3 OK-习语)。空集合添加规则 **已移除**(墓碑)。G4 运行在 kotlinc 2.0.21 / JRE 21(2026-10-22);kit-local `kt-*` ≠ jvmperf JMH。**无升级**。 + +| 规则 | 不良设备 | 良好 / 消极 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| kt-list-add-hot | `bad/kt-list-add-hot_list_loop.kt` | 是 (`kt-list-add-hot_capacity.kt`) | **是** | **关闭** | G4 ~2.1× 空 vs 容量 @2e6;保持关闭 | +| kt-string-plus-hot | `bad/kt-string-plus-hot_string_plus.kt` | 是的 (`kt-string-plus-hot_buildString.kt`) | **是的** | **关闭** | G4 ~1170× += 与 buildString;保持关闭 | +| kt-println-hot | `bad/kt-println-hot_println_loop.kt` | 是的 (`kt-println-hot_println_outside.kt`) | **是** | **关闭** | G4 约 99 次 println;保持关闭 | +| *(已移除 — 无标识)* 空列表 + 添加 | `bad/kt004_empty_add.kt`(已存档) | 是 | **是**(不确定) | — | 墓碑;G4 ~0.84–1.26× | + +--- + +## objcperf (oc-nsstring-append-hot–oc-nslog-hot) — 语料库准备就绪 + +激活:oc-nsstring-append-hot / oc-nslog-hot(默认 **关闭**)。日期格式化器、JSON 和正则表达式规则是 **提示**,不属于 2026-10-22 语料库裁定的一部分。数组添加规则 **已移除**(墓碑)。G4 在 **GNUstep**(非 Apple 运行时) 2026-10-22。与 swiftperf 区别。**不升级**。 + +| 规则 | 错误设置 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| *(已移除 — 无标识)* NSMutableArray 添加 | `bad/oc001_add_loop.m`(存档) | 是 | **是**(gnustep-proxy) | — | 墓碑;G4 ~1.18× / dive ~1× | +| oc-nsstring-append-hot | `bad/oc-nsstring-append-hot_append_loop.m` | 是 (`oc-nsstring-append-hot_mutable.m`) | **是** (gnustep) | **关闭** | G4 ~569× 拼接;保持关闭 | +| oc-nslog-hot | `bad/oc-nslog-hot_nslog_loop.m` | 是 (`oc-nslog-hot_nslog_outside.m`) | **是** (fprintf 代理) | **关闭** | G4 ~191×;保持关闭 | +| oc-dateformatter-hot | `bad/oc-dateformatter-hot_alloc_loop.m` | 是 | jsonl | 提示 | 微基准 ~30.4× 在一个形状上;保持提示;未经过语料库整理 | +| oc-json-hot | `bad/oc-json-hot_json_loop.m` | 是 | jsonl | 提示 | 微基准测试 ~15.8× 单一形状;保持提示;未经过语料库筛选 | +| oc-regex-hot | `bad/oc-regex-hot_regex_loop.m` | 是 | jsonl | 提示 | 微基准 ~14.9× 在一个形状上;保持提示;未经过语料库筛选 | +| *(已移除 — 无标识)* 空数组 + 添加 | `bad/oc004_empty_add.m`(已存档) | 是 | **是**(gnustep-proxy) | — | 墓碑;G4 ~1.18× / dive ~1× | + +--- + +## dartperf(dt-list-add-empty–dt-growable-add-empty)—— 语料库准备就绪 + +dt-list-add-empty–dt-print-hot 默认 **关闭**。**dt-growable-add-empty** 默认 **提示**。Dart 3.13.6 上的 G4(2026-10-22)。**无升级**。 + +| 规则 | 不良设备 | 良好 / 消极 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| dt-list-add-empty | `bad/dt-list-add-empty_add_loop.dart` | 是的 (`dt-list-add-empty_filled.dart`) | **是** | **关闭** | G4 ~3.1× 填充与空添加;保持关闭 | +| dt-string-plus-hot | `bad/dt-string-plus-hot_string_plus.dart` | 是 (`dt-string-plus-hot_buffer.dart`) | **是** | **关闭** | G4 ~247× 字符串+;保持关闭 | +| dt-print-hot | `bad/dt-print-hot_print_loop.dart` | 是的 (`dt-print-hot_print_outside.dart`) | **是的** | **关闭** | G4 ~128× 写入;保持关闭 | +| dt-growable-add-empty | `bad/dt-growable-add-empty_empty_add.dart` | 是 (`dt-growable-add-empty_filled.dart`) | **是** | **提示** | G4 ~2.4×; 保持提示 | + +--- + +## luaperf(lu-table-insert-hot–lu-print-hot)—— 语料库准备就绪 + +lu-table-insert-hot 默认 **关闭**(G3 OK-习语)。lu-string-concat-hot 默认 **错误**,在 G4 之后(`lu-string-concat-hot.md` + `jsonl`)。lu-print-hot G4 有证据;默认保持 **提示**(依赖 I/O 环境)。 + +| 规则 | 错误夹具 | 良好 / 负面 | 证据 md+jsonl | 默认严重性 | 备注 | +|------|-------------|-----------------|-------------------|------------------|-------| +| lu-table-insert-hot | `bad/lu-table-insert-hot_insert_loop.lua` | 是(`lu-table-insert-hot_index.lua`) | **是** | **关闭** | G4 ~4.7× 插入 vs 索引;保持关闭 | +| lu-string-concat-hot | `bad/lu-string-concat-hot_concat_loop.lua` | 是 (`lu-string-concat-hot_concat.lua`) | **是** | **错误** | string ..; G4 2026-10-22 | +| lu-print-hot | `bad/lu-print-hot_print_loop.lua` | 是 (`lu-print-hot_print_outside.lua`) | **是** | 提示 | 循环中打印;G4 2026-10-22 保持提示 | + +--- + +## beamperf(be-list-concat-reduce–be-enum-map-filter-chain)— 语料库准备就绪 + +| 编号 | 不良装置 | 良好 | 证据 | 默认 | 注意事项 | +|----|-------------|------|----------|---------|-------| +| be-list-concat-reduce | `bad/be-list-concat-reduce_list_plus.exs` (+ `.erl`) | 是 | **是** | 提示 | G4 ~453× ++ 与 cons;保持提示 | +| *(已移除 — 无标识)* `<>` 字符串追加 | `bad/be002_string_append.exs`(已存档) | 是 | **是**(混合) | — | 墓碑;chunk<>获胜;tiny/digits可能反转 | +| be-io-puts-hot | `bad/be-io-puts-hot_io_loop.exs` | 是 | **是** | **关** | G4 ~3810× IO.puts;保持关闭 | +| be-map-merge-hot | `bad/be-map-merge-hot_merge_in_loop.exs` | 是 | **是** | 提示 | G4 ~6.9× Map.merge 对比 put;KW ~2×;保持提示 | +| be-enum-map-filter-chain | `bad/be-enum-map-filter-chain_enum_chain.exs` | 是 | **是** | 提示 | G4 ~4.5× map|>filter 一次传递;保持提示 | + +r3 n=4 TP 75% FP+DOC+SKIP 0% → **语料库准备好:是**。G4 be-list-concat-reduce–be-enum-map-filter-chain 在 OTP 27 / Elixir 1.18(2026-10-22)上;无需升级。 + +## nimperf (nm-seq-add-hot–nm-seq-no-capacity) — 语料库就绪 + +| 规则 | 坏设备 | 好设备 | 证据 | 默认 | 备注 | +|------|-------------|--------------|----------|---------|-------| +| nm-seq-add-hot | `bad/nm-seq-add-hot_add_loop.nim` | 是 | 待定 | **关闭** | G3 OK-惯用法;**G4 阻塞** — Debian apt 中没有 `nim` | +| nm-string-amp-in-loop (**已移除**) | 已存档 | 已存档 | 墓碑 | — | Nim 2.2.12 `join` 比 `&=` 慢 (~0.79×,之前 ~0.69×) | +| nm-echo-hot | `bad/nm-echo-hot_echo_loop.nim` | 是 | 待定 | **关闭** | G4 阻止(没有 nim 工具链) | +| nm-seq-no-capacity | `bad/nm-seq-no-capacity_empty_add.nim` | `good/nm-seq-no-capacity_ofcap.nim` | 待处理 | 提示 | G4 阻塞(没有 Nim 工具链) | + +## 批次备注 + +**批次 B G4 (2026-10-22):** arkts Node-proxy JSONL;测量了 beam/csharp/dart/kotlin;objc **GNUstep**(非 Apple);swift **被阻止**(无 swiftc)。默认设置未更改——本轮不升级为 `error`。 + +## 诚实指出的差距 + +**磁盘审计 2026-10-21 ~19:31 亚洲/上海** — 每个跳过 / 待定 / 不适用 / 不确定的规则现在都有一个命名为 `evidence/.md` 的存根(没有编造的 JSONL)。 + +1. **cperf**:差 + 好 + `证据/cp-*.{ md,jsonl}` 适用于已发运的规则——**完成**. +2. **jsperf**:差 + 好 + `证据/jp-*.{md,jsonl}`}` 运规则——**完成**. +3. **zperf**:所有现行规则的配对固定装置。 测量过的JSONL + 登记根(待付/N/A/未结)——见`packages/zperf/docs/evidence-policy.md'和`packages/zperf/evidence/'。 +4. **jvmperf固定装置**:运出`jp-*`bad+good(朗镜)'。 **vt-不安全线上本地池** 指南仅载于`固定/指南/' + [`vt-不安全线上本地池.md'] (./packages/jvmperf/证据/vt-不安全线上本地池.md)——未发运。 +5. **jvmperf证据**:为运出的带有数字的规则测量误差/差错;**为ART/device和情况规则花根**。 见`packages/jvmperf/docs/evidence-policy.md'。 +6. ** 尚待执行的措施**(诚实——需要利用/env):zperf密度/安全根;jvm Android ART设备规则。 +7. ** Naming**:c/js好固定装置是描述性的——测试表明套间清洁性,而不是一个文件-每个规则。 +8. ** 不** 将实体堆放或剥离/汇编线计数视为*含义*的回归。 + +## 跨语言检查(2026-10-23) + +在其他可运行的工具链上,对已经在 C 或 C++ 中有成本的文件打开和时钟采样形状进行了计时。仅当比率很大且标准库有可重用的句柄或可以放在循环外的样本时,才添加提示。变化的路径无法提升。需要新的时间戳的循环没有可以提升的内容。默认情况下保持提示。 + +已检查且未添加: + +| 形状 | 比例 | 为什么没有圆珠 | +| --- | ---: | --- | +| Python `time.perf_counter_ns` | 2.866 | 循环旁的小 | +| Ruby `Process.clock_gettime` | 2.332 | 循环旁的小项 | +| Python `getattr` | 1.442 | 无明显成本 | +| Ruby `send` | 1.938 | 没有明显成本 | +| JavaScript `Reflect.get` | 8.168 | 几个纳秒;不是 Go 的 `FieldByName` 形式 | + +名称查找确实达到了标准,提示:C# `GetField` 加上 `GetValue` 在 .NET 10 上大约比直接访问字段慢 53 倍(`cs-field-by-name-hot`;缓存的 `FieldInfo.GetValue` 仍约慢 42 倍),Objective-C `valueForKey:` 大约慢 124 倍(`oc-value-for-key-hot`),Swift `value(forKey:)` 大约慢 35 倍(`sp-value-for-key-hot`)。 + +Zig 0.17 的 `Io.Dir.openFileAbsolute` 大约是 2134×,`Io.Clock.awake.now` 大约是 42×;两者都是带 JSONL 的提示。Zig 对一个不变对象的 `json.parseFromSlice` 大约是 2683×,而解析一次(`zp-json-hot` 提示)则比较。第一次 Swift `Date()` 运行和第一次 Objective-C `NSDate` 运行被丢弃,因为编译器移除了循环后的结果。重新运行并保留结果大约是 244× 和 769×,这两个提示现在都有 JSONL。 + +没有标准库对象,因此没有 slug 和没有数字:C、C++ 和 Rust 的标准库中没有 JSON 解析器。Zig 0.17 的标准库中没有正则表达式编译器。 + +未在此主机上测量,因此没有数值,也没有新的标识:JVM 和 Kotlin(无 JDK / kotlinc)、PHP、Dart、Lua、Nim、Elixir、ArkTS(不要复制 Node 数字)。 diff --git a/docs/zh-CN/smell-catalog.md b/docs/zh-CN/smell-catalog.md new file mode 100644 index 0000000..2ed955a --- /dev/null +++ b/docs/zh-CN/smell-catalog.md @@ -0,0 +1,183 @@ +# XPerf 跨语言气味目录 + +**XPerf** 总览图:跨套件相同的 *性能异常*。规则 ID 是 **每套件独有**(不共享)。在一个运行时上的 YES / 测量比 **不** 能转移到另一个运行时 —— 在迁移建议前需重新基准测试。 + +| 工具包 | 语言 | Lint ID | +|-----|----------|----------| +| [zperf](../../packages/zperf/) | Zig | zp-alloc-hot–zp-alloc-print-hot | +| [cperf](../../packages/cperf/) | C | cp-alloc-hot–cp-struct-by-value | +| [jsperf](../../packages/jsperf/) | JS/TS (V8) | jp-alloc-hot–jp-clone-per-event | +| [jvmperf](../../packages/jvmperf/) | Java/Kotlin | 29 个活跃的 `jp-*` 标识符, `jp-boxed-primitive-collection`…`jp-non-static-field-updater`(参见其 `docs/rule-ids.md`; **≠** jsperf) | +| [goperf](../../packages/goperf/) | Go | gp-make-hot–gp-time-now-hot | +| [pyperf](../../packages/pyperf/) | Python | pp-str-plus-hot–pp-open-hot | +| [swiftperf](../../packages/swiftperf/) | Swift | sp-array-no-reserve, sp-print-hot,以及 sp-dateformatter-hot / sp-json-decode-hot / sp-regex-hot / sp-file-read-hot / sp-clock-hot / sp-value-for-key-hot | +| [arktsperf](../../packages/arktsperf/) | ArkTS | at-alloc-hot–at-json-hot | +| [cppperf](../../packages/cppperf/) | C++17 | `cx-regex-ctor-hot`、`cx-endl-hot`、`cx-clock-hot`、`cx-ifstream-hot`、`cx-const-by-value`、`cx-vector-bool`(已移除字符串追加和字符串流) | +| [csharpperf](../../packages/csharpperf/) | C# | cs-list-add-no-capacity–按名称访问字段热 | +| [phpperf](../../packages/phpperf/) | PHP | ph-string-dot-hot–ph-echo-hot | +| [rubyperf](../../packages/rubyperf/) | Ruby | rb-string-plus-hot–rb-file-read-hot | +| [rustperf](../../packages/rustperf/) | Rust | rs-alloc-hot–rs-clock-hot (`rs-map-new-hot` 恢复,大约 1.5×) | +| [kotlinperf](../../packages/kotlinperf/) | Kotlin | `kt-list-add-hot`–`kt-println-hot`(已移除 empty-add) | +| [objcperf](../../packages/objcperf/) | Objective-C | `oc-nsstring-append-hot` / `oc-nslog-hot`(都关闭),加上 `oc-dateformatter-hot` / `oc-json-hot` / `oc-regex-hot` / `oc-file-read-hot` / `oc-clock-hot` / `oc-value-for-key-hot`(已移除 array-add) | +| [dartperf](../../packages/dartperf/) | Dart | dt-list-add-empty–dt-growable-add-empty | +| [luaperf](../../packages/luaperf/) | Lua | lu-table-insert-hot–lu-print-hot | +| [beamperf](../../packages/beamperf/) | Elixir + Erlang | be-list-concat-reduce–be-enum-map-filter-chain | +| [nimperf](../../packages/nimperf/) | Nim | nm-seq-add-hot–nm-seq-no-capacity | + +**前缀重叠:** jsperf 和 jvmperf 都使用 `jp-` 前缀,但它们的 slug 集合是分开的。例如 `jp-boxed-primitive-collection` (~5.96×) 仅存在于 jvmperf,而 `jp-alloc-hot` (~19×,在应用预设下关闭) 仅存在于 jsperf。引用 `jp-*` 规则时请始终注明工具包名称。 + +--- + +## 如何阅读 + +1. 选择一个**气味** → 在将某个发现作为速度声明处理之前,先打开每个工具包的规则 + `packages//evidence/.md`。 +2. **证据**列引用的比率已在 evidence-policy / JSONL / STATIC-VS-* 中存在 — **不是这里凭空创造的**。 +3. 单元格: + - rule id + 简短备注 = 已发布的 lint(或指南示例) + - **n/a** = 运行时物理不同;不要强行映射 + - **pending** = 规则或存根存在;尚无真实的时钟 JSONL + - **guide** = 仅章节/示例(尚无 lint) +4. 跨语言移植必须**重新基准测试** ([roadmap-next.md](roadmap-next.md) §5)。 +5. 固定装置触发 ≠ 速度;deopt / PrintCompilation 行数 ≠ 升级 ([rule-regression.md](rule-regression.md))。 + +--- + +## 目录 + +| 气味 | zperf | cperf | jsperf | jvmperf | 证据 | 备注 | +|-------|-------|-------|--------|---------|----------|-------| +| **alloc-in-loop / arena / pool** | **zp-alloc-hot** 大约是 arena 的 1.4 倍 GPA;**zp-per-packet-alloc**(热路径,和 zp-alloc-hot 相同);**zp-create-destroy-hot** 创建/销毁约比 pool 快 11 倍;**zp-fixed-size-alloc-hot** 固定 `alloc(u8,N)` 比 pool 快约 8.5 倍 | **cp-alloc-hot** 大约是 arena 的 10.8 倍 malloc 循环(**错误**) | **jp-alloc-hot** 大约是一次的 ~19× / ~6.4× `new`/push(在应用预设下关闭 — 分配/GC,不是去优化) | **jp-expensive-alloc-hot** 循环中昂贵的工厂(提示;tiny-pool **负**);**jp-hot-heavy-alloc** 热重分配(**等待 ART**);**jp-hot-payload-copy** 框架缓冲重用约 15.8 倍(**错误**) | z: zp-alloc-hot, zp-create-destroy-hot, zp-fixed-size-alloc-hot JSONL;c: cp-alloc-hot;js: jp-alloc-hot;jvm: jp-expensive-alloc-hot, jp-hot-payload-copy (+ jp-hot-heavy-alloc stub) | 相同的 *思路*(提取/重用);Zig smp 已经很便宜 → 固定大小优先使用 pool。jvm jp-expensive-alloc-hot ≠ “总是 pool”。 | +| **缓冲区增长 / 容量 / 构建器** | **zp-append-without-reserve** 约为 append reserve 的 2.1 倍;**zp-map-put-without-ensure** 约为 map ensure 的 2.8 倍;**zp-shift-on-read** 约为 cursor 的 17 倍 (**候选错误**) | **cp-realloc-grow-one** 约为 realloc+1 的 12.6 倍,相对于翻倍 (**错误**) | **jp-string-plus-in-loop** (**已移除** 2026-10-23;`push`+`join` 并不是约 2 倍); V8 数组增长方式不同;预分配似乎在 jp-alloc-hot 的良好场景下有效 | **jp-string-concat-hot** 约为字符串 `+=` 的 14.2 倍,相对于构建器 (**错误**);**jp-unpresized-collection** 约为无预设填充的 1.6 倍 (**提示** 部分); **jp-baos-tobytearray-copy** 约为 BAOS `toByteArray` 的 12.8 倍,相对于 `writeTo` (**错误**) | z: zp-append-without-reserve, zp-shift-on-read, zp-map-put-without-ensure; c: cp-realloc-grow-one; jvm: jp-string-concat-hot, jp-unpresized-collection, jp-baos-tobytearray-copy | 最接近的跨套件对:cp-realloc-grow-one ↔ zp-append-without-reserve ↔ jvm jp-string-concat-hot。与 JS 空洞数组不同。 | +| **热点 IO / 打印 / 日志 / 格式化** | **zp-print-hot** ~6e4× 打印对比安静模式(**环境 I/O**,不升级); **zp-unbuffered-io** ~49× 空 Writer 对比 4KiB(**候选错误**) | **cp-printf-hot** ~4.8× 无缓冲/缓冲 热操作;`#if 0` ~1× → **关闭**(默认应用预设) | **jp-console-hot** ~1328× 控制台对比安静模式(**应用关闭 / 引擎提示 — 环境**) | **jp-format-hot** ~17× `String.format` 热操作(**错误**);**jp-log-hot** ~28× 未保护的日志拼接(**错误**);**jp-hot-format-log** 热格式化/日志(**待 ART**) | z: zp-print-hot, zp-unbuffered-io; c: cp-printf-hot; js: jp-console-hot; jvm: jp-format-hot, jp-log-hot (+ jp-hot-format-log stub) | 优先使用缓冲 I/O / 级别保护。不要单独升级环境绑定的打印。 | +| **形状 / 布局 / SoA / 密度** | **zp-aos-single-field** 约 6.6× AoS→SoA;**zp-packable-bools** / 相关密度存根(**待定**);**zp-large-struct-by-value** 按值(**不确定** `-Ofast`);**zp-bool-array** 约 30× bool[] 对比 bitset | **指南** SoA 示例(`p01_soa.c`);**无 CP 规则** | 指南 **§04** 映射 / IC / 多态(静态 VS V8 约 12×);**无布局 JP** | **jp-many-boolean-fields**(**已移除** 2026-10-22;COH 16=16 无收益);Kotlin 其他地方布局气味 | z: zp-aos-single-field, zp-bool-array JSONL + 待处理存根;移除外部填充(约 1.2×);js: STATIC-VS-V8;jvm: jp-many-boolean-fields | V8 隐藏类 ≠ C 填充 ≠ Zig `extern` ≠ HotSpot 对象头. | +| **有孔/稀疏元素** | **不适用** — Zig 切片 ≠ V8 元素类型 | **不适用** — C 的“稀疏”指 CSR/哈希,是不同的问题 | **jp-delete-array-hole** ~3.2× 有孔 `delete a[i]` vs 紧凑(**错误**);静态 case2 ~5.5× | **jp-android-sparse-array** Android SparseArray(**待 ART 支持**) — **不是** V8 有孔 | js:jp-delete-array-hole(+ 静态 VS V8);jvm:仅 jp-android-sparse-array 桩 | **不要**将 jp-delete-array-hole 映射到 CP 或 ZP。Android Sparse* ≠ V8 有孔。 | +| **删除 / 移除 / 墓碑** | **zp-queue-ordered-remove** ~466× `orderedRemove` 与 ring (**候选错误**) | **不适用** | **jp-delete-shape** ~474× `delete` 属性 (**在应用程序预设下关闭 — G3 OK-习惯用法降级**) | **不适用** 作为 V8/Zig 移除;最接近的重用气味是 buffer/pool | js: jp-delete-shape; z: zp-queue-ordered-remove | 相关 *中间结构删除的成本*,不是相同的气味 (JS 墓碑 ≠ Zig memmove)。 | +| **同步 / 锁 / 受争用的类型** | **不适用**(无 ZP 同步规则) | **不适用** | **不适用** | **jp-legacy-synchronized-type**(**已移除** 2026-10-22;约 1.08× / HT 负) | jvm: `_removed/` 墓碑 | 受争用的锁成本取决于工作负载;单独的微基准不会升级。 | +| **装箱 / 值装箱 / 流装箱** | **不适用**(无托管装箱) | **不适用** | **不适用**(数字是值;对象在 alloc jp-alloc-hot 下) | **jp-boxed-primitive-collection** 约 5.96× 装箱集合(**错误**);**jp-boxed-stream** 约 149× 装箱流(**错误**);**jp-value-class-boxing** 约 5× 每操作值类装箱(**错误**) | JVM: jp-boxed-primitive-collection, jp-boxed-stream, jp-value-class-boxing; STATIC-VS-HOTSPOT | JVM 特定;jsperf 没有装箱规则。 | +| **字符串 / intern / concat 系列** | **不适用** 作为 JVM intern;Zig 有 zp-static-string-map StaticStringMap ~2.5× | **不适用** | **不适用** 作为 intern(jp-string-plus-in-loop **已移除** 2026-10-23;在 jp-alloc-hot 下的分配成本) | **jp-string-concat-hot** concat(**错误**);**jp-string-intern-hot** ~6.6× `intern()` 在循环中(**错误**);**jp-format-hot** format(**错误**) | jvm: jp-string-concat-hot, jp-format-hot, jp-string-intern-hot; z: zp-static-string-map | 与 jp-format-hot 的 hot-IO/format 行重叠;此处列出用于字符串表 / 构建器系列。 | +| **正则 / 解析控制流** | **不适用** | **不适用** | **jp-map-filter-chain** (**提示**;35000 行 ~4.6×–9.8×,3 行过滤+映射 ~1.3×) | **jp-regex-hot** ~1.36× 循环中正则编译 (**提示**);**jp-exception-control-flow** ~64× 异常作为控制流 (**错误**) | jvm: jp-regex-hot, jp-exception-control-flow | 正则的墙较弱 → 保持提示;异常作为控制流较强。 | +| **集合链 / 复制 / lambda 分配(Kotlin)** | **不适用** | **不适用** | **不适用** | **jp-collection-chain** ~265× 急切链(**关闭**);**jp-lambda-to-non-inline-hot** ~244× 每操作字节非内联 lambda(**关闭**);**jp-data-copy-hot** ~208× 循环中的 `copy`(**错误**);**jp-lazy-default-mode** 惰性模式(**待定**) | jvm:jp-collection-chain, jp-lambda-to-non-inline-hot, jp-data-copy-hot (+ jp-lazy-default-mode 存根) | 以 Kotlin 为中心;每操作字节通常比每操作纳秒更清晰。 | +| **枚举 / 映射 专用化** | **zp-static-string-map** StaticStringMap ~2.5×(字面键) | **不适用** | **不适用** | **jp-enum-key-hash-collection** ~2.1× EnumMap 对比 HashMap 枚举键(**错误**) | z: zp-static-string-map; jvm: jp-enum-key-hash-collection | 编译时 / 序号映射 — 类似的*想法*,不同的 API。 | +| **客户端 / 更新器 / 工厂重建** | **不适用** | **不适用** | **不适用** | **jp-客户端-工厂-重建** ~5e5× HttpClient 新建 vs 重用 (**错误**); **jp-非静态字段-更新器** 创建 ~165× / 稳定增长 ~1.0× (**提示**) | JVM: jp-客户端-工厂-重建, jp-非静态字段-更新器 | 工厂重用; 稳态可能平稳 (jp-非静态字段-更新器)。 | +| **Android / ART 设备气味** | **不适用** | **不适用** | **不适用** | 仅 **jp-android-sparse-array**、**jp-hot-heavy-alloc–jp-ui-text-append** 存根(**等待 ART/设备**):SparseArray、热分配、格式/日志、系统调用、Bitmap、Handler post、UI 文本 | jvm 证据存根 jp-android-sparse-array … jp-ui-text-append | 在 ART/设备工具箱之前,没有 box JSONL。 | +| **正确性 / 仅限静态(非速度门槛)** | **zp-debug-allocator** 模式切换;**zp-missing-free** 内存泄漏;**zp-catch-unreachable-alloc** `catch unreachable`;**zp-oversized-enum-tag** 枚举标签大小 — 全部 **不适用** 墙 | — | — | **jp-finalize** finalize(**设计/正确性待定**);**vt-unsafe-threadlocal-pool** 仅指导 VT/pool 神话(**未发布**) | z: stubs zp-debug-allocator / zp-missing-free / zp-catch-unreachable-* / zp-oversized-enum-tag;jvm: jp-finalize + vt-unsafe-threadlocal-pool | 不要为这些发明速度 JSONL。 | + +--- + +**目录行:** 15 个气味家族(z/c/js/jvm)+ go/py/swift/arkts/cpp/csharp/php/ruby/rust/kotlin/objc/dart/lua/beam/nim 的版块。**所有 19 套工具包均可用于语料库**(AGENT.md G3, 2026-10-22 Asia/Shanghai)— 结果仍为启发式,不保证加速效果。 + +**jvmperf 策略快照:** 查看 `packages/jvmperf/docs/evidence-policy.md` 以获取当前的错误/提示/关闭/待处理映射(slug ID)。仅供参考:`vt-unsafe-threadlocal-pool`(未发布)。已移除无效测量:`jp-legacy-synchronized-type`,`jp-many-boolean-fields`。 + +--- + +## 更新的套件 + +默认设置大多是**关闭**或**提示**。以下所有套件在 G3 下都是**语料库就绪**的。大多数规则都有测量的 JSONL(见 [rule-regression.md](rule-regression.md));nimperf 没有,因为 G4 被阻止(没有工具链)。在升级严重性之前请重新基准测试。 + +| 气味 | goperf | pyperf | swiftperf | arktsperf | 备注 | +|-------|--------|--------|-----------|-----------|-------| +| **循环中分配 / 竞技场 / 池** | **gp-使热** 循环中的 `make`/`new` (**关闭**) | 作为 make **不适用**;最接近的是 pp-list-plus-rebind 列表增长 | **不适用** (ARC) | **在分配时变热** 循环中的对象/`new`/push`{` (**关闭**) | 与 cp-alloc-hot / js jp-alloc-hot 相同的*理念*;每个工具包的证据策略中的比例(swift: 无,G4 阻塞) | +| **缓冲区增长 / 字符串构建器** | **gp-string-plus-hot** 字符串 `+=` (**关闭**); **gp-append-nil-slice** 在 nil 切片上 append (**提示**) | **pp-str-plus-hot** 字符串 `+=` (**关闭**); **pp-list-plus-rebind** 列表 `+` 重新绑定 (**提示**) | **sp-array-no-reserve** 在不使用 `reserveCapacity` 的情况下 append (**提示**); 字符串 `+=` 被移除 (~1×,reserve 更慢) | *(折叠到 at-alloc-hot)* | 最接近 jvm jp-string-concat-hot / cp-realloc-grow-one 系列 | +| **热 IO / 打印 / 日志** | **gp-fmt-hot** 循环中的 `fmt.*printf` (**关闭**) | **pp-print-hot** `print(` (**关闭**) | **sp-print-hot** `print(` (**关闭**) | **at-console-hot** `console.*` (**关闭**) | 与环境绑定;未升级 | +| **删除 / 形状** | **不适用** | **不适用** | **不适用** | 没有 ArkTS 删除规则(已移除;节点代理约 1×) | 与 jsperf jp-delete-shape 的想法一致;ArkTS 虚拟机可能不同 | +| **强制解包 / 不安全可选** | **不适用** | **不适用** | 不允许强制解包的规则(已移除;与 `if let` 同时) | **不适用** | Swift 特定 | +| **循环中的 defer / 链 / 查找** | **gp-defer-hot** 循环中的 `defer`(**提示**) | **不适用** | 无 `map`/`joined` 规则(已移除;约 1.2×) | **at-includes-hot** `includes` 查找(**提示**);**at-json-hot** `JSON.parse`/`stringify`(**提示**) | 特定于 Kit;不暗示跨 Kit 映射 | + +### cpp / csharp / php / ruby + +| 气味 | cppperf | csharpperf | phpperf | rubyperf | 备注 | +|-------|---------|------------|---------|----------|-------| +| **缓冲区增长 / 容量** | **已移除**(向量在未预留容量的情况下推入 — 墓碑)| **cs-list-add-no-capacity** `List.Add` 无容量 (**关闭**) | **ph-array-merge-rebind** `array_merge` 重新绑定 (**错误**) | **rb-array-plus-rebind** 数组 `+` / 拼接 (**关闭**)。`Hash#merge` 已移除(在 Ruby 4.0.8 中,字面构建更慢且分配更多) | 与 cp-realloc-grow-one / jvm jp-unpresized-collection 相同 *理念*;php 是唯一有 `错误` 的(G4)| +| **字符串构建器** | 没有 C++ 字符串追加规则(已移除;~1.1×) | **cs-string-plus-hot** 字符串 `+=`(**关闭**) | **ph-string-dot-hot** `.=`(**关闭**) | **rb-string-plus-hot** 字符串 `+=`(**关闭**) | 最接近 jvm jp-string-concat-hot / gp-string-plus-hot | +| **热 IO / 打印** | **已移除**(循环中的 iostream — 墓碑)| **cs-console-hot** `Console.Write*` (**关闭**) | **ph-echo-hot** `echo`/`print` (**提示**) | **rb-puts-hot** `puts`/`print` (**关闭**) | 受环境限制;未升级 | +| **循环中的正则表达式** | — | **cs-regex-hot** 循环中的正则表达式(**提示**) | — | — | 最接近 jvm jp-regex-hot;弱墙,保持提示 | +| **边界检查索引** | **已移除**(循环中的 `.at()` — 墓碑;反向 @-O2) | — | — | — | 当索引已验证时,优先使用 `operator[]`;规则未发布 | + +**cppperf vs cperf:** cppperf 专用于 C++17;cperf 保持 C11(+ 轻量 C++)。 + +### rust / kotlin / objc / dart / lua / beam + +| 气味 | rustperf | kotlinperf | objcperf | dartperf | luaperf | beamperf | 备注 | +|-------|----------|------------|----------|----------|---------|----------|-------| +| **缓冲区增长 / 容量** | **rs-alloc-hot** 分配/克隆 (**关**);**rs-collect-vec-hot** 循环中收集 (**提示**);带大小的 `Vec::new` 加上 push 已移除 (~1× 大填充) | **kt-list-add-hot** 列表分配 (**关**) | **已移除** (NSMutableArray 添加 — 墓碑) | **dt-list-add-empty** List.add (**关**);**dt-growable-add-empty** 空可增长 + 添加 (**提示**) | **lu-table-insert-hot** table.insert (**关**) | **be-list-concat-reduce** 列表 `++` (**提示**);**be-map-merge-hot** `Map.merge` (**提示**);**be-enum-map-filter-chain** 先 map 后 filter (**提示**) | 与 cp-realloc-grow-one 相同的 *想法* | +| **字符串构建器** | **rs-format-rebind-hot** `format!` 重绑定 (**错误**, ~99×); 平均分摊 `+=` 已移除 (~1×) | **kt-string-plus-hot** 字符串 `+=` (**关闭**) | **oc-nsstring-append-hot** stringByAppendingString (**关闭**) | **dt-string-plus-hot** 字符串 `+` (**关闭**) | **lu-string-concat-hot** `..` (**错误**) | **已移除** (`<>` 追加 — 墓碑; 混合/反向) | 最接近 JVM jp-string-concat-hot; Rust 区分平摊与 format! | +| **热 IO / 打印** | **rs-println-hot** println! (**提示**) | **kt-println-hot** println (**关闭**) | **oc-nslog-hot** NSLog (**关闭**) | **dt-print-hot** print (**关闭**) | **lu-print-hot** print (**提示**) | **be-io-puts-hot** IO.puts / io:format (**关闭**) | 环境绑定;未升级 | + +**kotlinperf** 是以 Kotlin 为优先的启发式方法;测量的 JVM/JMH 证据保留在 **jvmperf** 中(不要重复)。**objcperf** ≠ **swiftperf**。**beamperf** 涵盖 `.ex`/`.exs`/`.erl`。 + +### 尼姆 + +| 气味 | nimperf | 备注 | +|-------|---------|-------| +| **缓冲区增长 / 容量** | **nm-seq-add-hot** 循环中序列 `.add` (G3 后 **关闭**);**nm-seq-no-capacity** 空序列 + `.add` (**提示**) | Nim/Arraymancer/nimbus 上的 OK-惯用法;可选。没有 JSONL(G4 被阻止,没有 `nim`) | +| **字符串构建器** | **已移除** (`&=` / `s = s & x` — 墓碑;`join` 更慢) | 最接近 jvm jp-string-concat-hot / rb-string-plus-hot | +| **热 IO / 打印** | **nm-echo-hot** 在循环中 `echo` (**关闭**) | 环境绑定;未升级 | + +## 空 / 稀薄的细胞(诚实) + +| Gap | Why | +|-----|-----| +| cperf **SoA lint** | 仅供指南示例。**cp-wide-bool-flags** 是四个全宽布尔标志的提示,并不是 SoA 规则 | +| jsperf **打印** 映射但严重性 **关闭**(应用)| 已测量,环境绑定 | +| jsperf **buffer-grow** 专用规则 | 通过 jp-alloc-hot 预分配测试工具粗略覆盖;由于 join 基准测试错过了约 2 倍,jp-string-plus-in-loop 已被移除 | +| zperf **同步/锁定** | 无规则 | +| 跨套件 **有孔** 外部 js | 物理差异 → 设计上 **不适用** | +| jvm **jp-android-sparse-array** … **jp-ui-text-append** | 需要 ART/设备 | +| zperf **zp-可打包布尔值** / **zp-usize-索引字段** / **zp-可选标量字段** 密度 | 待定密度测试工具 | +| jvm **jp-sleep-polling** sleep-polling | 待定 — 设计/情境,不是微观 | +| **vt-unsafe-threadlocal-pool** | 首先指南;不在 `Rule.RULES` 中 | +| nimperf **证据** | G4 被阻止(测量主机上没有 Nim 工具链);仅为存根,没有 JSONL | +| arktsperf / objcperf **证据** | 在代理上测量(Node/V8,GNUstep),而非目标运行时 | +| swiftperf **在 Linux CI 上构建** | 需要 Swift 工具链;缺失时跳过测试 | +| csharpperf **无需 SDK 构建** | 需要 `dotnet`;缺失时跳过测试 | +| arktsperf **设备基准测试** | 首选 HarmonyOS 设备;仅使用节点代理不会提升权限 | +| cppperf 与 cperf 重叠 | 故意拆分:C++ 工具包与 C11 工具包分开 | + +--- + +## 证据指示 + +| 工具包 | 策略 | 关联 / 批次(不在仓库中) | +|-----|--------|----------------------------------| +| 共享 | [规则回归.md](rule-regression.md) | 本目录 | +| zperf | [packages/zperf/docs/evidence-policy.md](../../packages/zperf/docs/zh-CN/evidence-policy.md) | `packages/zperf/evidence/` | +| cperf | [packages/cperf/docs/evidence-policy.md](../../packages/cperf/docs/zh-CN/evidence-policy.md) | `STATIC-VS-O2`(不在仓库中) | +| jsperf | [packages/jsperf/docs/evidence-policy.md](../../packages/jsperf/docs/zh-CN/evidence-policy.md) | `STATIC-VS-V8`(不在仓库中) | +| jvmperf | [packages/jvmperf/docs/evidence-policy.md](../../packages/jvmperf/docs/zh-CN/evidence-policy.md) | `STATIC-VS-HOTSPOT`,`EVIDENCE-BATCH`(不在仓库中) | + +上面命名的相关和批处理笔记**不**属于此 git 仓库——它们仅作为普通名称列出,以便 GitHub 界面不会出现 404。 + +--- + +## 热编译 / 解析 / 查询 (2026-10-23) + +这些蛞蝓是**提示**。固定装置是成对的。没有 JSONL,它们不是 2026-10-22 语料裁决的一部分。 + +| 套件 | 译名 | 气味 | +|-----|------|-------| +| pyperf | pp-json-hot | 循环中的 `json.loads` / `dumps` | +| pyperf | pp-deepcopy-hot | 循环中的 `deepcopy` | +| jsperf | jp-regex-ctor-hot | `new RegExp`(稳定模式;默认作用域全部) | +| jsperf | jp-json-hot | `JSON.parse` / `stringify`(稳定文本;默认范围全部) | +| jsperf | jp-dom-query-hot | `querySelector` / `getElementById`(稳定选择器;默认范围全部) | + +| jsperf | jp-query-all-star | `querySelectorAll('*')` 全树遍历(包括模板) | +| jsperf | jp-execjs-in-timer | 在定时器内构建了 `executeJavaScript`/`evaluate` | +| jsperf | 每事件的 jp 克隆 | `structuredClone` / 事件类参数的 JSON 往返转换 | +| goperf | gp-regexp-compile-hot | 循环中的 `regexp.Compile` | +| goperf | gp-json-marshal-hot | 循环中的 `json.Marshal` / `Unmarshal` | +| goperf | gp-reflect-hot | 循环中的 `reflect.TypeOf` / `ValueOf` | +| cppperf | cx-regex-ctor-hot | 在循环中构造的 `std::regex` | +| cppperf | cx-endl-hot | 循环中的 `endl` 刷新 | +| csharpperf | cs-linq-materialize-hot | 循环中的 `ToList` / `ToArray` / `ToDictionary` | +| csharpperf | cs-format-hot | 循环中的 `string.Format` | +| rubyperf | rb-regexp-new-hot | 循环中的 `Regexp.new` | +| rubyperf | rb-json-hot | 循环中的 `JSON.parse` / `generate` | +| rustperf | rs-regex-new-hot | 循环中的 `Regex::new` | + +## 相关 + +- [overview.md](overview.md) — 伞状状态 +- [playbook.md](playbook.md) — 共享循环 + 回归合同 +- [roadmap-next.md](roadmap-next.md) — 空单元格的下一个填充 +- [docs/README.md](README.md) — 文档索引 diff --git a/docs/zh-CN/static-check-sources.md b/docs/zh-CN/static-check-sources.md new file mode 100644 index 0000000..aa74c39 --- /dev/null +++ b/docs/zh-CN/static-check-sources.md @@ -0,0 +1,117 @@ +# 静态检查来源汇总 + +面向 linter 能看见的性能与内存形态的公开来源。这里的一行不是本树的测量。下列数字是来源自己的数字。新 slug 仍需要本地 before/after,且默认保持 hint。 + +处置: + +- **already-ruled** —— 该 kit 已有 slug 覆盖该形态。 +- **measure-first** —— 来源描述了扫描器能看见的形态,而本树尚无对应 slug。 +- **rejected** —— 来源自身结果很小、反向,或不是稳定规则。 +- **no-compiler** —— 本机跑不了该语言,故形态未测。 + +## cperf + +- 循环内的 `alloca` 会把每块保留到函数返回。[Stack Overflow](https://stackoverflow.com/questions/5407546/memory-allocated-with-alloca-gets-freed-at-end-of-function-or-at-end-of-scope)。**already-ruled**(`cp-alloca-hot`)。本地栈覆盖 1280000 字节对比 64。触摸时间约 1×。 +- 逐字节 `fgetc` / `fputc`。问题报告相对 4096 字节 `fread` 约 10×。[Stack Overflow](https://stackoverflow.com/questions/13225014/why-fgetc-too-slow)。**rejected**。本地块读更快,且行匹配器分不清字节解析器与批量读。 +- 循环 `malloc`、按一增长的 `realloc`、`strcat`、`sprintf`、`fopen`、`gettimeofday`,以及一长串布尔字段,均 **already-ruled**。 + +## cppperf + +- 循环内构造 `std::function` 在捕获装不进小缓冲时会堆分配一次。[The cost of captures](https://zvxryb.github.io/blog/2018/02/18/the-cost-of-captures/)。**rejected**。本地 sink 重跑让提升后的 `std::function` 更慢(约 0.6×)。 +- 按值复制 `shared_ptr` 会 bump 原子计数。[GotW #91](https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/) 与 [按值传递写稿](https://ddanilov.me/shared-ptr-is-evil/)。**rejected**。本地复制约 4.8×,且变化值的 `make_shared` 无法提升。 +- `const std::string` / `const std::vector` 按值,在拷贝会分配时相对 const 引用约 15–21×。**already-ruled**(`cx-const-by-value`)。4 字符字符串约 1×,hint 仍开火。 +- 遍历 `vector` 相对字节向量约 12×,且打包容器用更少字节。**already-ruled**(`cx-vector-bool`)。 +- 无 reserve 的 `std::string` 追加、`endl`、正则构造、时钟与 `ifstream` 均 **already-ruled**。字符串流在公平重跑后已移除。 + +## csharpperf + +- 循环内用 `+` 建字符串会分配。[Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/strings/common-tasks/concatenate)。**already-ruled**(`cs-string-plus-hot`)。 +- C# 10 插值不再落到旧的装箱 `string.Format` 路径。[Microsoft .NET blog](https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/)。在本地 .NET 8 跑出相反结论前,作为「插值很慢」规则 **rejected**。 +- `File` 读、`Stopwatch.GetTimestamp`、`GetField`、JSON、正则与 `ToList` 均 **already-ruled**。 + +## goperf + +- 循环里 `s += part` 会复制整串。预增长的 `strings.Builder` 只分配一次。[String concatenation vs strings.Builder](https://vvivid.dev/posts/go_string_concat_vs_string_builder/)。**already-ruled**(`gp-string-plus-hot`)。 +- 短片段列表上 `bytes.Buffer` vs `strings.Builder` 在该机约 1.5×。[brandur.org](https://brandur.org/fragments/bytes-buffer-vs-strings-builder)。**rejected**。 +- 循环里的 `fmt`、单载荷 `json`、`os.Open`、`time.Now`、`regexp` 编译与 `FieldByName` 均 **already-ruled**。 + +## jsperf + +- 对象上的 `delete` 会把它迁到 dictionary 属性。[V8 fast properties](https://v8.dev/blog/fast-properties)。**already-ruled**(`jp-delete-shape`)。 +- 删除数组元素或读洞会翻转元素存储。[web.dev V8 tips](https://web.dev/articles/speed-v8)。**already-ruled**(`jp-delete-array-hole`)。 +- 预分配巨大数组(该页引用超过 64K)在该页被点名。仅对扫描器能看见、又不误伤普通数组的字面量巨大 `new Array(n)` 做 **measure-first**。 +- 正则构造、单载荷 `JSON.parse`、`readFileSync`,以及 `Date.now` / `performance.now` 均 **already-ruled**。 + +## pyperf + +- 对某些 shape,CPython 上 `str` 的 `+=` 可以快于 `''.join`。[String concatenation in Python](https://blog.ganssle.io/articles/2019/11/string-concat.html)。`str` 已 **already-ruled**(`pp-str-plus-hot`)。不要仅凭旧的平方故事再加第二条 join 规则。 +- `[None] * n` 对比 `append` 在帖子计时约 1.3×。[Stack Overflow](https://stackoverflow.com/questions/63599687/is-it-better-to-pre-allocate-array-in-python-or-use-arr-append)。**rejected**。 +- 用 `+=` 建 `bytes` 对比 `b''.join` 被报差距很大。[Fast bytes concatenation](https://www.guyrutenberg.com/2020/04/04/fast-bytes-concatenation-in-python/)。**measure-first**。 +- 单载荷的 `open` 与 `json.loads` 均 **already-ruled**。本地测过 `perf_counter_ns` 与 `getattr` 且未加入。 + +## rubyperf + +- 反复 `+` 会分配。缓冲要用 `<<`。[fast-ruby concatenation](https://github.com/fastruby/fast-ruby/blob/main/code/string/concatenation.rb)。**already-ruled**(`rb-string-plus-hot`)。 +- `# frozen_string_literal: true` 在该文极小字面量循环上约 1.2–1.3×。[FastRuby.io](https://www.fastruby.io/blog/ruby-3-4-new-defaults.html)。在真实文件本地跑出更大收益前 **rejected**。 +- 单载荷的 `File.read`、`Regexp.new` 与 `JSON.parse` 均 **already-ruled**。本地测过 `Process.clock_gettime` 与 `send` 且未加入。 + +## rustperf + +- `collect` 会分配。返回迭代器,或 `extend` 已有 `Vec`。[The Rust Performance Book](https://nnethercote.github.io/perf-book/iterators.html)。部分 **already-ruled**(`rs-collect-vec-hot`)。 +- 切片上的 `iter().cloned().collect()` 应是 `to_vec()`。[Clippy `iter_cloned_collect`](https://github.com/rust-lang/rust-clippy/blob/master/clippy_lints/src/methods/iter_cloned_collect.rs)。**measure-first**。公开声明是「更快」,不是比值。 +- 循环内对堆值 `.clone()` 会深拷贝。[Clones and allocations](https://rs4ts.dev/21-performance/03-optimization/)。**measure-first**。Clippy 的 `redundant_clone` 历史上会跳过循环体([rust-clippy#17495](https://github.com/rust-lang/rust-clippy/pull/17495))。 +- `File::open`、`Instant::now` 与 `Regex::new` 均 **already-ruled**。标准库没有 JSON 解析器。 + +## swiftperf + +- 空数组上一次 `reserveCapacity` 避免几何增长。每轮以 `count + k` 调用会反复拷贝。[Hacking with Swift](https://www.hackingwithswift.com/articles/128/array-performance-append-vs-reservecapacity)。缺 reserve **already-ruled**(`sp-array-no-reserve`)。循环内 reserve 是 **measure-first**。 +- `DateFormatter`、`JSONDecoder`、`NSRegularExpression`、`contentsOfFile`、`Date()` 与 `value(forKey:)` 均 **already-ruled**。 + +## objcperf + +- 一长串便利构造(`[NSMutableString string]`)会堆起 autorelease 对象。Apple 文档建议循环内 `@autoreleasepool` 削峰值。[Autorelease pool blocks](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html) 与 [Stack Overflow](https://stackoverflow.com/questions/16162878/autorelease-pool-blocks)。对缺失 pool 做 **measure-first**。每轮创建 pool 很便宜([mikeash.com](https://www.mikeash.com/pyblog/autorelease-is-fast.html)),故规则不得误伤已有 pool。 +- `stringByAppendingString`、`NSLog`、日期格式化器、JSON、正则、文件读、`NSDate` 与 `valueForKey:` 均 **already-ruled**。 + +## zperf + +- 每轮新建 `ArrayList` 会分配。复用一个 list 并调用 `clearRetainingCapacity` 在该帖与栈缓冲相当。[ziglang/zig#16982](https://github.com/ziglang/zig/issues/16982)。循环内堆分配 **already-ruled**(`zp-alloc-hot`、`zp-create-destroy-hot`)。不要为同一形态再加 slug。 +- `std.fmt.allocPrint` 分配新切片。[lib/std/fmt.zig](https://github.com/ziglang/zig/blob/master/lib/std/fmt.zig)。格式串与参数不变时 **already-ruled**(`zp-alloc-print-hot`)。本地 ReleaseFast 中位数:通用分配器约 50×,arena 约 12×,常量串约 14×。`bufPrint` 与字节拷贝约 ≤7×,未加入。 +- 文件打开、`Io.Clock` 与单载荷 `json.parseFromSlice` 均 **already-ruled**。0.17 标准库没有正则编译器。 + +## jvmperf + +- 循环内用 `StringBuilder` 代替 `+` 是经典形态。[byroot 的比较](https://byroot.github.io/ruby/performance/2025/10/28/string-literals.html) 重述了它。**already-ruled**(`jp-string-concat-hot`)。 +- `SparseArray` 避免装箱整型键,面向几百条目。[Android SparseArray](https://developer.android.com/reference/kotlin/android/util/SparseArray)。**already-ruled**(`jp-android-sparse-array`)。 +- 装箱流、正则、format 及其他 JVM slug 保持原样。本机没有能跑新 bench 的 JDK,故新 JVM 形态为 **no-compiler**。 + +## kotlinperf + +- 传给非 inline 函数的 lambda 会分配闭包。`inline` 去掉该对象。[Kotlin inline functions](https://kotlinlang.org/docs/inline-functions.html)。**no-compiler**(此处无 `kotlinc`)。JVM 侧已有 `jp-lambda-to-non-inline-hot`。 +- Android 上的 `SparseArray` 与 jvmperf 同一说明。JVM 侧 **already-ruled**,未为 Kotlin 重测。 +- 字符串 `+`、`println` 与 `List` 增长在本 kit **already-ruled**。 + +## phpperf + +- PHP 字符串可变。多参数 `echo` 可避免拼接。[Stack Overflow](https://stackoverflow.com/questions/124067/php-string-concatenation-performance)。循环里的 `.` **already-ruled**(`ph-string-dot-hot`)。 +- 大数组 `implode` 在该 SitePoint 笔记里可能比追加用更多峰值内存。[SitePoint](https://www.sitepoint.com/high-performance-string-concatenation-in-php/)。**no-compiler**。 + +## dartperf + +- 跨语言建串仓库含 Dart,并点名 `StringBuffer`。[langs_string_build_test](https://github.com/shagrouni/langs_string_build_test)。该仓库不是本树的受控 bench。循环字符串 `+` **already-ruled**(`dt-string-plus-hot`)。其余 Dart 形态 **no-compiler**。 + +## luaperf + +- 收集行再 `table.concat` 在 Programming in Lua 里把 350 KB 读取从 40 秒降到 0.5 秒。[PIL 11.6](https://www.lua.org/pil/11.6.html)。**already-ruled**(`lu-string-concat-hot`)。本机无 `lua`,书中数字未重跑。超出该 slug 的均为 **no-compiler**。 + +## beamperf + +- 二进制 `<>` 会拷贝。iolist 是块列表,IO 函数可直接写而不建单一二进制。[Elixir IO data](https://hexdocs.pm/elixir/1.18.4/IO.html) 与 [iolist 笔记](https://www.openmymind.net/Elixir-A-Little-Beyond-The-Basics-Part-2-iolist/)。循环列表拼接 **already-ruled**(`be-list-concat-reduce`)。本机无 Erlang/Elixir,新 iolist slug 为 **no-compiler**。 + +## nimperf + +- 同一跨语言仓库计时 Nim 建串,未单独隔离 `setLen`。[langs_string_build_test](https://github.com/shagrouni/langs_string_build_test)。字符串 `&` 与 seq 增长 **already-ruled**。其余 Nim 形态 **no-compiler**。 + +## arktsperf + +- 若 `+` 任一操作数是字符串,规范说表达式非常量时会创建新字符串对象。[ArkTS spec, additive expressions](https://yoyocat.github.io/ArkTSSpec/7_expressions.html)。本 kit 尚无 string-plus slug。**no-compiler**。不要把 JavaScript 计时抄到 ArkTS。 +- 循环分配、`delete`、`console`、`includes` 与 JSON 均 **already-ruled**。 diff --git a/packages/arktsperf/docs/arkts-performance-guide.md b/packages/arktsperf/docs/arkts-performance-guide.md index af3903d..cd22ac1 100644 --- a/packages/arktsperf/docs/arkts-performance-guide.md +++ b/packages/arktsperf/docs/arkts-performance-guide.md @@ -2,17 +2,12 @@ Less GC churn and shape pollution on embed / HarmonyOS hot paths. -> **Scope: ArkTS (TypeScript-like) for HarmonyOS and similar embeds.** Claims that assert speedups name a local or device bench, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. Related JS notes: [jsperf](../../jsperf/docs/js-performance-guide.md). +> **Scope: ArkTS (TypeScript-like) for HarmonyOS and similar embeds.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. Related JS notes: [jsperf](../../jsperf/docs/js-performance-guide.md). -## How to read this - -1. **Measure first** on device or a faithful runtime when possible. -2. Treat each chapter as a checklist. -3. Do not assume V8 numbers transfer to ArkTS VMs — re-bench. +Profile first on device or a faithful runtime when you can (`DevEco` profiler, simple timers). Do not assume V8 numbers transfer to ArkTS VMs — re-bench there. ## Contents -- [How to read this](#how-to-read-this) - [1. Allocation](#1-allocation) - [2. Shapes and delete](#2-shapes-and-delete) - [3. Bridge / console chatter](#3-bridge--console-chatter) @@ -21,27 +16,27 @@ Less GC churn and shape pollution on embed / HarmonyOS hot paths. ## 1. Allocation -Object / array literals and `new` inside tight UI or animation loops increase GC pressure. Reuse buffers when profiles show pressure. +Object and array literals, and `new`, inside tight UI or animation loops raise GC pressure. Reuse buffers when the profile shows pressure. -Companion lint: **at-alloc-hot**. +Lint: `at-alloc-hot` (default off). ## 2. Shapes and delete -`delete` and mixed property shapes make polymorphic paths; prefer stable object layouts (same idea as V8 hidden classes — **re-bench** on ArkTS). +`delete` and mixed property shapes push polymorphic paths. Prefer stable object layouts (same idea as V8 hidden classes — re-bench on ArkTS). A Node proxy of `delete` versus `Map.delete` was about 1×, so there is no lint for it. Re-bench on an ArkTS VM before adding one. ## 3. Bridge / console chatter -`console.log` and frequent sync cross-language / worker bridge calls on the hot path stall frames. Gate debug I/O. +`console.log` and frequent sync cross-language or worker bridge calls on the hot path stall frames. Gate debug I/O. -Companion lint: **at-console-hot**. +Lint: `at-console-hot` (default off). ## 4. Lookups and JSON in loops `Array.includes` / `indexOf` over a list inside a hot loop is linear per iteration; build a `Set` once and use `has`. For short strings, a dense lookup table beats `Map` (a `Map` often loses on short haystacks). Parsing or stringifying many small JSON values in a loop repeats allocation and parsing work; parse one array blob or reuse buffers. -Companion lint: **at-includes-hot**, **at-json-hot** (both default **hint**). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. +Lint: `at-includes-hot`, `at-json-hot` (both default hint). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. ## Lint map diff --git a/packages/arktsperf/docs/guide-src/00_head.md b/packages/arktsperf/docs/guide-src/00_head.md index 6536dcb..82d38fe 100644 --- a/packages/arktsperf/docs/guide-src/00_head.md +++ b/packages/arktsperf/docs/guide-src/00_head.md @@ -2,17 +2,12 @@ Less GC churn and shape pollution on embed / HarmonyOS hot paths. -> **Scope: ArkTS (TypeScript-like) for HarmonyOS and similar embeds.** Claims that assert speedups name a local or device bench, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. Related JS notes: [jsperf](../../jsperf/docs/js-performance-guide.md). +> **Scope: ArkTS (TypeScript-like) for HarmonyOS and similar embeds.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. Related JS notes: [jsperf](../../jsperf/docs/js-performance-guide.md). -## How to read this - -1. **Measure first** on device or a faithful runtime when possible. -2. Treat each chapter as a checklist. -3. Do not assume V8 numbers transfer to ArkTS VMs — re-bench. +Profile first on device or a faithful runtime when you can (`DevEco` profiler, simple timers). Do not assume V8 numbers transfer to ArkTS VMs — re-bench there. ## Contents -- [How to read this](#how-to-read-this) - [1. Allocation](#1-allocation) - [2. Shapes and delete](#2-shapes-and-delete) - [3. Bridge / console chatter](#3-bridge--console-chatter) diff --git a/packages/arktsperf/docs/guide-src/01_allocation.md b/packages/arktsperf/docs/guide-src/01_allocation.md index 7677d62..977bf09 100644 --- a/packages/arktsperf/docs/guide-src/01_allocation.md +++ b/packages/arktsperf/docs/guide-src/01_allocation.md @@ -1,6 +1,6 @@ ## 1. Allocation -Object / array literals and `new` inside tight UI or animation loops increase GC pressure. Reuse buffers when profiles show pressure. +Object and array literals, and `new`, inside tight UI or animation loops raise GC pressure. Reuse buffers when the profile shows pressure. -Companion lint: **at-alloc-hot**. +Lint: `at-alloc-hot` (default off). diff --git a/packages/arktsperf/docs/guide-src/02_shapes.md b/packages/arktsperf/docs/guide-src/02_shapes.md index b999374..eead25f 100644 --- a/packages/arktsperf/docs/guide-src/02_shapes.md +++ b/packages/arktsperf/docs/guide-src/02_shapes.md @@ -1,6 +1,6 @@ ## 2. Shapes and delete -`delete` and mixed property shapes make polymorphic paths; prefer stable object layouts (same idea as V8 hidden classes — **re-bench** on ArkTS). +`delete` and mixed property shapes push polymorphic paths. Prefer stable object layouts (same idea as V8 hidden classes — re-bench on ArkTS). A Node proxy of `delete` versus `Map.delete` was about 1×, so there is no lint for it. Re-bench on an ArkTS VM before adding one. diff --git a/packages/arktsperf/docs/guide-src/03_bridge.md b/packages/arktsperf/docs/guide-src/03_bridge.md index 3a7d207..9dd07dc 100644 --- a/packages/arktsperf/docs/guide-src/03_bridge.md +++ b/packages/arktsperf/docs/guide-src/03_bridge.md @@ -1,6 +1,6 @@ ## 3. Bridge / console chatter -`console.log` and frequent sync cross-language / worker bridge calls on the hot path stall frames. Gate debug I/O. +`console.log` and frequent sync cross-language or worker bridge calls on the hot path stall frames. Gate debug I/O. -Companion lint: **at-console-hot**. +Lint: `at-console-hot` (default off). diff --git a/packages/arktsperf/docs/guide-src/04_lookups.md b/packages/arktsperf/docs/guide-src/04_lookups.md index 03a78af..a4ca43c 100644 --- a/packages/arktsperf/docs/guide-src/04_lookups.md +++ b/packages/arktsperf/docs/guide-src/04_lookups.md @@ -2,5 +2,5 @@ `Array.includes` / `indexOf` over a list inside a hot loop is linear per iteration; build a `Set` once and use `has`. For short strings, a dense lookup table beats `Map` (a `Map` often loses on short haystacks). Parsing or stringifying many small JSON values in a loop repeats allocation and parsing work; parse one array blob or reuse buffers. -Companion lint: **at-includes-hot**, **at-json-hot** (both default **hint**). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. +Lint: `at-includes-hot`, `at-json-hot` (both default hint). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. diff --git a/packages/arktsperf/docs/guide-src/zh-CN/00_head.md b/packages/arktsperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..7c07624 --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,16 @@ +# ArkTS 性能指南(v0) + +在嵌入式 / HarmonyOS 热路径上减少 GC 抖动与 shape 污染。 + +> **范围:面向 HarmonyOS 及同类嵌入环境的 ArkTS(类 TypeScript)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 相关 JS 说明见 [jsperf](../../../jsperf/docs/zh-CN/js-performance-guide.md)。 + +能在真机或等价运行时上先画像更好(`DevEco` profiler、简单计时)。不要默认 Node / Chrome 的 V8 层级建议成立。 + +## 目录 + +- [1. 分配](#1-分配) +- [2. Shape 与 delete](#2-shape-与-delete) +- [3. Bridge / console 噪声](#3-bridge--console-噪声) +- [4. 循环中的查找与 JSON](#4-循环中的查找与-json) +- [Lint 对照](#lint-对照) + diff --git a/packages/arktsperf/docs/guide-src/zh-CN/01_allocation.md b/packages/arktsperf/docs/guide-src/zh-CN/01_allocation.md new file mode 100644 index 0000000..147470b --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/01_allocation.md @@ -0,0 +1,5 @@ +## 1. 分配 + +紧密的 UI 或动画循环里的对象 / 数组字面量与 `new` 会加重 GC。画像显示有压力时,复用缓冲区。 + +Lint: `at-alloc-hot`(默认 off) diff --git a/packages/arktsperf/docs/guide-src/zh-CN/02_shapes.md b/packages/arktsperf/docs/guide-src/zh-CN/02_shapes.md new file mode 100644 index 0000000..65e430a --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/02_shapes.md @@ -0,0 +1,5 @@ +## 2. Shape 与 delete + +`delete` 与混杂的属性 shape 会走多态路径;更稳妥的是稳定对象布局(与 V8 hidden class 同一思路——请在 ArkTS 上**重测**)。 + +对 `delete` 与 `Map.delete` 的 Node 代理约 1×,因此没有对应 lint。在 ArkTS VM 上重测后再考虑新增。 diff --git a/packages/arktsperf/docs/guide-src/zh-CN/03_bridge.md b/packages/arktsperf/docs/guide-src/zh-CN/03_bridge.md new file mode 100644 index 0000000..4b3b960 --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/03_bridge.md @@ -0,0 +1,5 @@ +## 3. Bridge / console 噪声 + +热路径上的 `console.log` 以及频繁的同步跨语言 / worker bridge 调用会拖帧。请门控调试 I/O。 + +Lint: `at-console-hot`(默认 off) diff --git a/packages/arktsperf/docs/guide-src/zh-CN/04_lookups.md b/packages/arktsperf/docs/guide-src/zh-CN/04_lookups.md new file mode 100644 index 0000000..10eb2ef --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/04_lookups.md @@ -0,0 +1,5 @@ +## 4. 循环中的查找与 JSON + +热循环里对列表做 `Array.includes` / `indexOf` 是每次迭代线性扫描;先建一次 `Set` 再用 `has`。短字符串场景下,稠密查找表往往优于 `Map`(短 haystack 上 `Map` 常更慢)。在循环里反复 parse/stringify 许多小 JSON 会重复分配与解析;应解析一整块数组,或复用缓冲区。 + +Lint: `at-includes-hot`, `at-json-hot`(both 默认 hint). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. diff --git a/packages/arktsperf/docs/guide-src/zh-CN/99_tail.md b/packages/arktsperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..cd0d744 --- /dev/null +++ b/packages/arktsperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,15 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| at-alloc-hot | 循环内分配类写法 | off | +| at-console-hot | 循环内 console / bridge 噪声 | off | +| at-includes-hot | 循环内 `includes` / `indexOf` | hint | +| at-json-hot | 循环内 `JSON.parse` / `stringify` | hint | + +仅在 `evidence/at-*.jsonl` 之上再叠加真机或 ArkTS-VM bench 时才升级(见 [evidence-policy.md](evidence-policy.md))。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [jsperf 指南](../../../jsperf/docs/zh-CN/js-performance-guide.md)(相关,但不相同) diff --git a/packages/arktsperf/docs/zh-CN/arkts-performance-guide.md b/packages/arktsperf/docs/zh-CN/arkts-performance-guide.md new file mode 100644 index 0000000..18dc841 --- /dev/null +++ b/packages/arktsperf/docs/zh-CN/arkts-performance-guide.md @@ -0,0 +1,51 @@ +# ArkTS 性能指南(v0) + +在嵌入式 / HarmonyOS 热路径上减少 GC 抖动与 shape 污染。 + +> **范围:面向 HarmonyOS 及同类嵌入环境的 ArkTS(类 TypeScript)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 相关 JS 说明见 [jsperf](../../../jsperf/docs/zh-CN/js-performance-guide.md)。 + +能在真机或等价运行时上先画像更好(`DevEco` profiler、简单计时)。不要默认 Node / Chrome 的 V8 层级建议成立。 + +## 目录 + +- [1. 分配](#1-分配) +- [2. Shape 与 delete](#2-shape-与-delete) +- [3. Bridge / console 噪声](#3-bridge--console-噪声) +- [4. 循环中的查找与 JSON](#4-循环中的查找与-json) +- [Lint 对照](#lint-对照) + +## 1. 分配 + +紧密的 UI 或动画循环里的对象 / 数组字面量与 `new` 会加重 GC。画像显示有压力时,复用缓冲区。 + +Lint: `at-alloc-hot`(默认 off) +## 2. Shape 与 delete + +`delete` 与混杂的属性 shape 会走多态路径;更稳妥的是稳定对象布局(与 V8 hidden class 同一思路——请在 ArkTS 上**重测**)。 + +对 `delete` 与 `Map.delete` 的 Node 代理约 1×,因此没有对应 lint。在 ArkTS VM 上重测后再考虑新增。 +## 3. Bridge / console 噪声 + +热路径上的 `console.log` 以及频繁的同步跨语言 / worker bridge 调用会拖帧。请门控调试 I/O。 + +Lint: `at-console-hot`(默认 off) +## 4. 循环中的查找与 JSON + +热循环里对列表做 `Array.includes` / `indexOf` 是每次迭代线性扫描;先建一次 `Set` 再用 `has`。短字符串场景下,稠密查找表往往优于 `Map`(短 haystack 上 `Map` 常更慢)。在循环里反复 parse/stringify 许多小 JSON 会重复分配与解析;应解析一整块数组,或复用缓冲区。 + +Lint: `at-includes-hot`, `at-json-hot`(both 默认 hint). Their JSONL evidence is a Node proxy, not an ArkTS VM, so it cannot raise severity on its own. +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| at-alloc-hot | 循环内分配类写法 | off | +| at-console-hot | 循环内 console / bridge 噪声 | off | +| at-includes-hot | 循环内 `includes` / `indexOf` | hint | +| at-json-hot | 循环内 `JSON.parse` / `stringify` | hint | + +仅在 `evidence/at-*.jsonl` 之上再叠加真机或 ArkTS-VM bench 时才升级(见 [evidence-policy.md](evidence-policy.md))。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [jsperf 指南](../../../jsperf/docs/zh-CN/js-performance-guide.md)(相关,但不相同) diff --git a/packages/arktsperf/docs/zh-CN/corpus.md b/packages/arktsperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..15ed209 --- /dev/null +++ b/packages/arktsperf/docs/zh-CN/corpus.md @@ -0,0 +1,17 @@ +# arktsperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +截至2026-10-22(亚洲/上海);真正的ArkTS树。 + +| 树 | 提交 / 来源 | Lint r2 → r3 重新设计 | 注释 | +|------|-----------------|----------------------|-------| +| **基本功能** | gitee `openharmony/applications_app_samples` `a826ab0e` `code/BasicFeature` | 29 → **22** | at-includes-hot, at-json-hot | +| **用户界面** | 同一代码库 `code/UI` | 13 → **8** | 包含热点 | +| **dimina-harmony** | 本地 Harmony/ArkTS 树(非供应商化;发布时固定 URL+提交) @ `cfd73f9914da1f417a4f1af62fa08b7642548f5d` | 6 → **26** | BASE64 at-includes-hot TP | + +**语料库准备好:是**(r3 重新设计)。默认值:`at-includes-hot` 和 `at-json-hot` **提示**;其他三条规则 **关闭**。没有 G4。 + +不要将语料库来源提交到 `packages/arktsperf`。 diff --git a/packages/arktsperf/docs/zh-CN/evidence-policy.md b/packages/arktsperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..b0094b1 --- /dev/null +++ b/packages/arktsperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,18 @@ +# arktsperf 证据政策 + +## 门槛 + +只有在有 `evidence/.md` + `.jsonl` **和** 首选 ArkTS/设备运行时基准的情况下,规则才可以被视为 **`error`**。仅使用 **Node 代理** 会保持 **`hint`** / 包默认(在 md 中说明)。 + +## 当前映射 + +截至2026年10月22日(B批次)。 + +| 规则 | 严重性 | 证据 | 问题 | +|------|----------|----------|-------| +| at-alloc-hot | 关闭 | 节点代理 JSONL (~33×) | 循环中的分配类似 | +| 控制台热 | 关闭 | 节点代理 JSONL (~208×) | 循环中的控制台 | +| at-includes-hot | 提示 | 节点代理 JSONL (~29× includes→Set) | 循环中的 indexOf/includes | +| at-json-hot | 提示 | 节点代理 JSONL (~2× JSON) | 循环中的 JSON.parse/stringify | + +单靠节点代理不会升级严重性。 diff --git a/packages/arktsperf/docs/zh-CN/rule-ids.md b/packages/arktsperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..e71e8ed --- /dev/null +++ b/packages/arktsperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,27 @@ +# arktsperf 规则 ID(别名) + +规范 ID 仅为 **slug**。旧地图中的数字是历史性的,**不**被接受。 + +| Slug | +|------| +| `at-alloc-hot` | +| `at-console-hot` | +| `at-includes-hot` | +| `at-json-hot` | + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或相应的工具包选项)。旧的 `*-in-loop` ID 会误导工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `在循环中分配` | `热分配` | +| `在循环中使用控制台` | `控制台热加载` | +| `在循环中包含` | `热门包含` | +| `at-json-in-loop` | `at-json-hot` | + +## 已移除 + +- `at-object-delete` — 节点代理约 1.01×;见 `evidence/_removed/` diff --git a/packages/arktsperf/evidence/zh-CN/README.md b/packages/arktsperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..a484c59 --- /dev/null +++ b/packages/arktsperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# arktsperf 证据 + +更新于 **2026-10-22** 亚洲/上海(B批次 G4 填充)。 + +所有五条规则的 JSONL 文件都是 **Node 代理**(不是 ArkTS VM)。默认设置未更改。 + +参见每条规则的 `*.md` + `*.jsonl`(`at-includes-hot.mem.jsonl` 保存内存探针)。政策:[`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md)。根矩阵:[`../../../docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/arktsperf/evidence/zh-CN/at-alloc-hot.md b/packages/arktsperf/evidence/zh-CN/at-alloc-hot.md new file mode 100644 index 0000000..e7ee9af --- /dev/null +++ b/packages/arktsperf/evidence/zh-CN/at-alloc-hot.md @@ -0,0 +1,36 @@ +# at-alloc-hot — 循环中的新建 / 分配 (Node 代理) + +**状态:** 已测量 (节点代理) + +## 主张 + +在 **Node/V8** 上,每次迭代新建 `new Uint8Array` 比重用一个缓冲区更慢(代理 ArkTS 循环中分配的异味)。 + +## 如何测量 + +- 测试工具:`examples/bench/evidence_at001_at003.js` +- **运行时:node-proxy** — 不是 ArkTS VM / 设备 SoftAP +- warm=3;取 9 的中位数;N=200000 + +- 原始行: [`at-alloc-hot.jsonl`](../at-alloc-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| 运行时 | **node-proxy** | +| node | **v20.19.2** | +| v8 | **11.3.244.8-node.38** | +| 中位数之前 (`new_in_loop`) | ~4.768 毫秒 | +| (`reuse_buffer`) 后的中位数 | ~0.1461 毫秒 | +| **前/后比例** | **~32.6×** | + +## 与 lint 的对应 + +`arktsperf-lint` at-alloc-hot:严重性保持软件包默认(参见证据政策)。JSONL 是 **节点代理**,而不是设备。 + +## 限制 + +- 仅代理;ArkTS/ArkRuntime 可能有所不同。 +- 默认保持 **关闭**(G3 可接受-习惯用法);不升级。 diff --git a/packages/arktsperf/evidence/zh-CN/at-console-hot.md b/packages/arktsperf/evidence/zh-CN/at-console-hot.md new file mode 100644 index 0000000..87a064d --- /dev/null +++ b/packages/arktsperf/evidence/zh-CN/at-console-hot.md @@ -0,0 +1,36 @@ +# at-console-hot — 循环中的控制台(Node 代理) + +**状态:** 已测量 (节点代理) + +## 主张 + +每次迭代的 `console.log`(标准输出被丢弃)比安静循环加一次日志在 Node/V8 上慢得多。 + +## 如何测量 + +- 测试工具:`examples/bench/evidence_at001_at003.js` +- **运行时:node-proxy** +- N_CONSOLE=20000 + +- 原始行:[`at-console-hot.jsonl`](../at-console-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| 运行时 | **node-proxy** | +| node | **v20.19.2** | +| v8 | **11.3.244.8-node.38** | +| 中值之前 (`console_in_loop`) | ~24.79 毫秒 | +| (`quiet_then_once`) 之后的中位数 | ~0.1194 毫秒 | +| **前/后比率** | **~208×** | + +## 与 lint 的对应 + +`arktsperf-lint` 在控制台热点:严重性保持包的默认值(见证据政策)。JSONL 是 **Node 代理**,不是设备。 + +## 限制 + +- I/O / 控制台环境依赖。 +- 默认保持 **关闭**。 diff --git a/packages/arktsperf/evidence/zh-CN/at-includes-hot.md b/packages/arktsperf/evidence/zh-CN/at-includes-hot.md new file mode 100644 index 0000000..20c55e1 --- /dev/null +++ b/packages/arktsperf/evidence/zh-CN/at-includes-hot.md @@ -0,0 +1,48 @@ +# at-includes-hot — 循环中的 includes/indexOf (Node 代理) + +**状态:** 已测量 (节点代理) + +## 主张 + +`Array.includes` 在热循环中对 256 条目的列表的速度远慢于 Node/V8 上的 `Set.has`(约 **29 倍**)。短字母表的 `String.indexOf` 与 `Map.get` 相比约为 **0.95 倍**(Map 常常落后);一个稠密的 **查找表(LUT)** 却能获胜(L=64 时约 3 倍)。Lint 消息(2026-10-22):Array→Set;String.indexOf→LUT — 对于短的查找内容 **不要** 使用 Map。 + +## 如何测量 + +- 测试平台:`examples/bench/evidence_at004_at005.js` +- 深入研究(LUT 分支):独立脚本,不随本仓库分发 +- **运行时:node-proxy** — 不是 ArkTS VM +- 配对:includes→Set (N=5e4); indexOf→Map (N=2e5, 字母长度 64); 深入研究中的 LUT 分支 + +- 原始行: [`at-includes-hot.jsonl`](../at-includes-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| 运行时 | **node-proxy** | +| node | **v20.19.2** | +| v8 | **11.3.244.8-node.38** | +| 中值之前 (`includes_loop`) | ~13.37 毫秒 | +| (`set_has`) 后的中位数 | ~0.4637 毫秒 | +| **前/后比例** | **~28.8×** | +| 中位数之前 (`indexOf_loop`) | ~1.954 毫秒 | +| (`map_lookup`) 之后的中位数 | ~2.056 毫秒 | +| **前/后比例** | **~0.951×** | + +## 与 lint 的对应 + +`arktsperf-lint` at-includes-hot:严重性保持为 **提示**。JSONL 是 **节点代理**,不是设备——单独无法升级。夹具 `arktsperf-lint/fixtures/good/at-includes-hot_string_lut.ts` 记录了字符串路径。 + +## 限制 + +- 主要结论包括→Set。indexOf→Map 在短字母表上没有优势;更推荐使用查找表(LUT)。 +- 默认保持 **hint**;单独使用 Node 代理不会升级为错误。 + +## 降落 2026-10-22 + +匹配器消息缩小;已添加良好固定装置的查找表。严重性保持不变。 + +## 内存 (2026-10-22,node-proxy) + +`out` 数组的 `heapUsed` 增量约为 1.25–1.43 MiB;仅 Set(256) 约为 **184 KiB**。问题出在 CPU(包括 → Set),而不是 RAM 减少。[`at-includes-hot.mem.jsonl`](../at-includes-hot.mem.jsonl)。 diff --git a/packages/arktsperf/evidence/zh-CN/at-json-hot.md b/packages/arktsperf/evidence/zh-CN/at-json-hot.md new file mode 100644 index 0000000..7e743b1 --- /dev/null +++ b/packages/arktsperf/evidence/zh-CN/at-json-hot.md @@ -0,0 +1,39 @@ +# at-json-hot — 循环中的 JSON.parse/stringify(Node 代理) + +**状态:** 已测量 (节点代理) + +## 主张 + +在循环中解析许多小的 JSON 字符串比对一个数组块执行一次 `JSON.parse` 要慢;在 Node/V8 上,每个项目的 `stringify` 比对一个数组执行一次 stringify 略慢。 + +## 如何测量 + +- 测试工具: `examples/bench/evidence_at004_at005.js` +- **运行环境: node-proxy** +- N=20000 + +- 原始行:[`at-json-hot.jsonl`](../at-json-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| 运行时 | **node-proxy** | +| node | **v20.19.2** | +| v8 | **11.3.244.8-node.38** | +| 中位数之前 (`parse_in_loop`) | ~8.702 毫秒 | +| (`parse_once_array`) 后的中位数 | ~3.895 毫秒 | +| **前/后比例** | **~2.23×** | +| 循环内字符串化前的中位数 (`stringify_in_loop`) | ~2.985 毫秒 | +| 中位数(`stringify_once` 之后) | ~1.781 毫秒 | +| **前后比例** | **~1.68×** | + +## 与 lint 的对应 + +`arktsperf-lint` 在 json-hot:严重性保持包默认(参见证据政策)。JSONL 是 **Node 代理**,而不是设备。 + +## 限制 + +- 适度的比率(约2倍解析,约1.7倍字符串化)。 +- 默认保持 **hint**。 diff --git a/packages/arktsperf/zh-CN/CONTRIBUTING.md b/packages/arktsperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/arktsperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/arktsperf/zh-CN/README.md b/packages/arktsperf/zh-CN/README.md new file mode 100644 index 0000000..98ab4c1 --- /dev/null +++ b/packages/arktsperf/zh-CN/README.md @@ -0,0 +1,37 @@ +# arktsperf + +HarmonyOS **ArkTS** 性能笔记、小示例,以及 `arktsperf-lint`(纯 Node,零 npm 依赖)。ArkTS 类似 TypeScript;检查器接受 `.ets`、`.ts` 和 `.js` 文件。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**(AGENT.md G3)。默认值:`at-includes-hot` 和 `at-json-hot` **启用**;`at-alloc-hot` 和 `at-console-hot` **关闭**。`at-object-delete` 在一个 Node 代理保持约 1× 后被移除。Guide v0 脚手架。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/arkts-performance-guide.md) — 来自 `docs/guide-src/` +- [`arktsperf-lint`](../arktsperf-lint/README.md) — 五条规则(见其表格获取默认值) +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +嵌入式/类似小程序的约束:热分配、多态对象形状,以及 UI 或 worker 热路径上的同步桥/控制台通信。**at-console-hot**(循环中的 `console.*`)在 G3 之后默认 **关闭**(采样噪声);可通过 `--severity at-console-hot=hint` 启用。没有设备或本地测试平台的声明仍为临时性质。**语料库就绪:是** — 参见 [docs/corpus.md](../docs/zh-CN/corpus.md)。 + +## 运行 lint + +```sh +cd packages/arktsperf +node arktsperf-lint/bin/arktsperf-lint.js path/to/sources +bash scripts/smoke-lint.sh +bash scripts/self-lint.sh +``` + +仅在 **错误** 严重性发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ per-rule Node-proxy evidence (*.md + *.jsonl) +examples/ tiny ArkTS/TS demos +arktsperf-lint/ pure-JS checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/beamperf/docs/beam-performance-guide.md b/packages/beamperf/docs/beam-performance-guide.md index 5431664..eec5b9b 100644 --- a/packages/beamperf/docs/beam-performance-guide.md +++ b/packages/beamperf/docs/beam-performance-guide.md @@ -2,17 +2,12 @@ Less quadratic list growth, fewer binary rebuilds, quieter hot loops. -> **Scope: Elixir 1.14+ and Erlang/OTP 25+ on BEAM.** One kit covers both languages. Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Elixir 1.14+ and Erlang/OTP 25+ on BEAM.** One kit covers both languages. Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`:timer.tc`, `fprof`, `eprof`, `Benchee`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Extensions scanned: `.ex`, `.exs`, `.erl`. +Profile first (`:timer.tc`, `fprof`, `eprof`, `Benchee`). Extensions scanned: `.ex`, `.exs`, `.erl`. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists](#1-lists) - [2. Binaries / strings and hot I/O](#2-binaries--strings-and-hot-io) - [Lint map](#lint-map) @@ -23,13 +18,13 @@ Less quadratic list growth, fewer binary rebuilds, quieter hot loops. `Map.merge/2` or `Keyword.merge/2` on every element of a reduce was measured slower than `Map.put/3` (see [evidence-policy.md](evidence-policy.md)). Use `Map.put/3`, or merge outside only when the other map stays the same. Chained `Enum.map/2` then `Enum.filter/2` builds an intermediate list. On the measured chain of about 200000 integers that was about 4.5×; a short list is a different cost. -Companion lint: **be-list-concat-reduce**, **be-map-merge-hot**, **be-enum-map-filter-chain** (all default **hint**). +Lint: `be-list-concat-reduce`, `be-map-merge-hot`, `be-enum-map-filter-chain` (all default hint). ## 2. Binaries / strings and hot I/O `a <> b` (Elixir) or repeated binary append in a loop rebuilds binaries. Prefer iolists / `IO.iodata_to_binary/1`. No lint rule covers this smell (its former rule, `BE002`, was retired). Keep `IO.puts` / `:io.format` / `io:format` out of tight loops. -Companion lint: **be-io-puts-hot** (default **off**). +Lint: `be-io-puts-hot` (default off). ## Lint map diff --git a/packages/beamperf/docs/guide-src/00_head.md b/packages/beamperf/docs/guide-src/00_head.md index 3ef9150..3d516f9 100644 --- a/packages/beamperf/docs/guide-src/00_head.md +++ b/packages/beamperf/docs/guide-src/00_head.md @@ -2,17 +2,12 @@ Less quadratic list growth, fewer binary rebuilds, quieter hot loops. -> **Scope: Elixir 1.14+ and Erlang/OTP 25+ on BEAM.** One kit covers both languages. Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Elixir 1.14+ and Erlang/OTP 25+ on BEAM.** One kit covers both languages. Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`:timer.tc`, `fprof`, `eprof`, `Benchee`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Extensions scanned: `.ex`, `.exs`, `.erl`. +Profile first (`:timer.tc`, `fprof`, `eprof`, `Benchee`). Extensions scanned: `.ex`, `.exs`, `.erl`. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists](#1-lists) - [2. Binaries / strings and hot I/O](#2-binaries--strings-and-hot-io) - [Lint map](#lint-map) diff --git a/packages/beamperf/docs/guide-src/01_lists.md b/packages/beamperf/docs/guide-src/01_lists.md index 7054a55..4fea1f6 100644 --- a/packages/beamperf/docs/guide-src/01_lists.md +++ b/packages/beamperf/docs/guide-src/01_lists.md @@ -4,5 +4,5 @@ `Map.merge/2` or `Keyword.merge/2` on every element of a reduce was measured slower than `Map.put/3` (see [evidence-policy.md](evidence-policy.md)). Use `Map.put/3`, or merge outside only when the other map stays the same. Chained `Enum.map/2` then `Enum.filter/2` builds an intermediate list. On the measured chain of about 200000 integers that was about 4.5×; a short list is a different cost. -Companion lint: **be-list-concat-reduce**, **be-map-merge-hot**, **be-enum-map-filter-chain** (all default **hint**). +Lint: `be-list-concat-reduce`, `be-map-merge-hot`, `be-enum-map-filter-chain` (all default hint). diff --git a/packages/beamperf/docs/guide-src/02_binaries.md b/packages/beamperf/docs/guide-src/02_binaries.md index d98cdf7..ee0d312 100644 --- a/packages/beamperf/docs/guide-src/02_binaries.md +++ b/packages/beamperf/docs/guide-src/02_binaries.md @@ -2,5 +2,5 @@ `a <> b` (Elixir) or repeated binary append in a loop rebuilds binaries. Prefer iolists / `IO.iodata_to_binary/1`. No lint rule covers this smell (its former rule, `BE002`, was retired). Keep `IO.puts` / `:io.format` / `io:format` out of tight loops. -Companion lint: **be-io-puts-hot** (default **off**). +Lint: `be-io-puts-hot` (default off). diff --git a/packages/beamperf/docs/guide-src/zh-CN/00_head.md b/packages/beamperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..0e5a123 --- /dev/null +++ b/packages/beamperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,14 @@ +# BEAM 性能指南(v0)— Elixir 与 Erlang + +少做平方级 list 增长,少重建 binary,让热循环更安静。 + +> **范围:Elixir 1.14+ 与 Erlang/OTP 25+(BEAM)。** 一个 kit 覆盖两种语言。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`:timer.tc`、`fprof`、`eprof`、`Benchee`)。扫描扩展名:`.ex`、`.exs`、`.erl`。 + +## 目录 + +- [1. List](#1-list) +- [2. Binary / 字符串与热 I/O](#2-binary--字符串与热-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/beamperf/docs/guide-src/zh-CN/01_lists.md b/packages/beamperf/docs/guide-src/zh-CN/01_lists.md new file mode 100644 index 0000000..83f0e5b --- /dev/null +++ b/packages/beamperf/docs/guide-src/zh-CN/01_lists.md @@ -0,0 +1,7 @@ +## 1. List + +循环里的 `list ++ [x]`(Elixir)或 `List ++ [X]`(Erlang)是平方级的。优先 `[x | acc]` 再 `Enum.reverse/1` / `lists:reverse/1`,或使用正确的累加器。 + +在 reduce 的每个元素上调用 `Map.merge/2` 或 `Keyword.merge/2`,测得比 `Map.put/3` 更慢(见 [evidence-policy.md](evidence-policy.md))。用 `Map.put/3`;仅当另一 map 不变时,才在循环外 merge。链式 `Enum.map/2` 再 `Enum.filter/2` 会建中间 list。约 200000 整数的测量链大约 4.5×;短 list 成本不同。 + +Lint: `be-list-concat-reduce`, `be-map-merge-hot`, `be-enum-map-filter-chain`(all 默认 hint) diff --git a/packages/beamperf/docs/guide-src/zh-CN/02_binaries.md b/packages/beamperf/docs/guide-src/zh-CN/02_binaries.md new file mode 100644 index 0000000..c6c6297 --- /dev/null +++ b/packages/beamperf/docs/guide-src/zh-CN/02_binaries.md @@ -0,0 +1,5 @@ +## 2. Binary / 字符串与热 I/O + +循环里的 `a <> b`(Elixir)或反复 binary 追加会重建 binary。优先 iolist / `IO.iodata_to_binary/1`。没有 lint 覆盖该 smell(旧规则 `BE002` 已退役)。不要把 `IO.puts` / `:io.format` / `io:format` 放进紧密循环。 + +Lint: `be-io-puts-hot`(默认 off) diff --git a/packages/beamperf/docs/guide-src/zh-CN/99_tail.md b/packages/beamperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..859d7c6 --- /dev/null +++ b/packages/beamperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,14 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| be-list-concat-reduce | 循环内 list `++` | hint | +| be-map-merge-hot | 循环内 `Map`/`Keyword.merge` | hint | +| be-enum-map-filter-chain | 链式 `Enum.map`/`filter` | hint | +| be-io-puts-hot | 循环内 `IO.puts` / `io:format` | off | + +仅在有 `evidence/be-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/beamperf/docs/zh-CN/beam-performance-guide.md b/packages/beamperf/docs/zh-CN/beam-performance-guide.md new file mode 100644 index 0000000..b479a04 --- /dev/null +++ b/packages/beamperf/docs/zh-CN/beam-performance-guide.md @@ -0,0 +1,40 @@ +# BEAM 性能指南(v0)— Elixir 与 Erlang + +少做平方级 list 增长,少重建 binary,让热循环更安静。 + +> **范围:Elixir 1.14+ 与 Erlang/OTP 25+(BEAM)。** 一个 kit 覆盖两种语言。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`:timer.tc`、`fprof`、`eprof`、`Benchee`)。扫描扩展名:`.ex`、`.exs`、`.erl`。 + +## 目录 + +- [1. List](#1-list) +- [2. Binary / 字符串与热 I/O](#2-binary--字符串与热-io) +- [Lint 对照](#lint-对照) + +## 1. List + +循环里的 `list ++ [x]`(Elixir)或 `List ++ [X]`(Erlang)是平方级的。优先 `[x | acc]` 再 `Enum.reverse/1` / `lists:reverse/1`,或使用正确的累加器。 + +在 reduce 的每个元素上调用 `Map.merge/2` 或 `Keyword.merge/2`,测得比 `Map.put/3` 更慢(见 [evidence-policy.md](evidence-policy.md))。用 `Map.put/3`;仅当另一 map 不变时,才在循环外 merge。链式 `Enum.map/2` 再 `Enum.filter/2` 会建中间 list。约 200000 整数的测量链大约 4.5×;短 list 成本不同。 + +Lint: `be-list-concat-reduce`, `be-map-merge-hot`, `be-enum-map-filter-chain`(all 默认 hint) +## 2. Binary / 字符串与热 I/O + +循环里的 `a <> b`(Elixir)或反复 binary 追加会重建 binary。优先 iolist / `IO.iodata_to_binary/1`。没有 lint 覆盖该 smell(旧规则 `BE002` 已退役)。不要把 `IO.puts` / `:io.format` / `io:format` 放进紧密循环。 + +Lint: `be-io-puts-hot`(默认 off) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| be-list-concat-reduce | 循环内 list `++` | hint | +| be-map-merge-hot | 循环内 `Map`/`Keyword.merge` | hint | +| be-enum-map-filter-chain | 链式 `Enum.map`/`filter` | hint | +| be-io-puts-hot | 循环内 `IO.puts` / `io:format` | off | + +仅在有 `evidence/be-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/beamperf/docs/zh-CN/corpus.md b/packages/beamperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..7fb974d --- /dev/null +++ b/packages/beamperf/docs/zh-CN/corpus.md @@ -0,0 +1,37 @@ +# beamperf 语料库目标 + +大型树木**不会**被供应,语料库也不会随仓库一起分发:将它克隆到仓库之外的任何本地目录,并将该路径传递给代码检查工具。 +使用以下命令进行代码检查: + +```sh +elixir packages/beamperf/beamperf-lint/beamperf_lint.exs \ + / --format json --no-fail +``` + +检查器是 **Elixir/Erlang** (`beamperf-lint`)。规则:全部四条(见 [rule-ids.md](rule-ids.md))。扫描 `.ex` / `.exs` / `.erl` 文件。 + +## 固定目标 + +克隆于 2026-10-21(亚洲/上海)。 + +| 树 | 提交 | 大小 | Lint (r2 → r3 重新设计) | +|------|--------|------|-------------------------| +| **elixir** | `aad5397` 浅 `https://github.com/elixir-lang/elixir.git` | 22M | **1 → 4** (be-map-merge-hot:3 be-enum-map-filter-chain:1) | +| **凤凰** | `e5880ef` 浅 `https://github.com/phoenixframework/phoenix.git` | 7.6M | **0 → 0** | +| **牛仔** | `6475600` 浅 `https://github.com/ninenines/cowboy.git` | 11M | **0 → 0** | + +路径跳过: `test(s)/`、`example(s)/`、`docs/`、`benchmark(s)/`、`_build/`、`deps/`、`fixtures/`、`tmp/`、`scripts/`、`iex/`、`integration_test/`。 + +## G3 TP/FP 分诊 (2026-10-22 亚洲/上海) + +| | r1 n=70 | r2 n=1 | **r3 n=4** | +|--|---------|--------|------------| +| TP | 4.3% | 100% | **75%** | +| FP+DOC+SKIP | **64.3%** | 0% | **0%** | +| OK-idiom | 31.4% | 0% | 25% | + +**语料库准备好了吗?`yes`**(r3 重新设计)。默认值:`be-list-concat-reduce`、`be-map-merge-hot` 和 `be-enum-map-filter-chain` **提示**;`be-io-puts-hot` **关闭**。无 G4 / 无默认 `error`。 + +### 重新设计(`beamperf-lint`) +- be-io-puts-hot → **关闭**(BE002 已弃用);be-map-merge-hot merge-in-loop;be-enum-map-filter-chain 枚举链;Erlang `Acc ++ [` 无循环头 +- Elixir `%{}` 不再被视为 Erlang `%` 注释 diff --git a/packages/beamperf/docs/zh-CN/evidence-policy.md b/packages/beamperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..09344da --- /dev/null +++ b/packages/beamperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,18 @@ +# beamperf 证据政策 + +## 门槛 + +一条规则只有在有 `evidence/.md` + `.jsonl` **以及**明确的政策决定的情况下才可以被视为 **`error`**。仅测量的 JSONL 并不会自动升级。批次 B(2026-10-22)保持了包的默认设置。 + +## 当前映射 + +截至2026-10-22(B批次 G4)。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| be-list-concat-reduce | 提示 | **是** ~453× | 列表 ++ | +| be-io-puts-hot | 关闭 | **是** ~3810× | 循环中的 IO | +| be-map-merge-hot | 提示 | **是** ~6.9× 合并对比放入 | Map/Keyword.merge | +| 枚举-映射-过滤-链 | 提示 | **是** ~4.5× 先映射再过滤 | 枚举链 | + +已移除(墓碑):`BE002`(二进制 `<>`),见 `evidence/_removed/`。 diff --git a/packages/beamperf/docs/zh-CN/rule-ids.md b/packages/beamperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..420d0c7 --- /dev/null +++ b/packages/beamperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,25 @@ +# beamperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `be-list-concat-reduce` | +| `be-io-puts-hot` | +| `be-map-merge-hot` | +| `be-enum-map-filter-chain` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `be-io-puts-in-loop` | `be-io-puts-hot` | +| `be-map-merge-in-loop` | `be-map-merge-hot` | + +## Removed (no slug) + +- `BE002` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/beamperf/evidence/zh-CN/README.md b/packages/beamperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..9116b70 --- /dev/null +++ b/packages/beamperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# beamperf 证据 + +更新于 **2026-10-22** 亚洲/上海。 + +所有四条规则均在 OTP 27 / Elixir 1.18 上测量。默认设置未更改(未升级为 `error`)。 + +请参阅每条规则 `*.md` + `*.jsonl`。政策: [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md)。根矩阵: [`../../../docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/beamperf/evidence/zh-CN/be-enum-map-filter-chain.md b/packages/beamperf/evidence/zh-CN/be-enum-map-filter-chain.md new file mode 100644 index 0000000..bee90d6 --- /dev/null +++ b/packages/beamperf/evidence/zh-CN/be-enum-map-filter-chain.md @@ -0,0 +1,39 @@ +# be-enum-map-filter-chain — Enum.map |> Enum.filter 链 + +**状态:** 已测量 + +## 主张 + +将 `Enum.map/2` 然后 `Enum.filter/2`(或 `Enum.reject/2`)链接起来会分配一个中间列表,并且比单个 `Enum.reduce` 在一次遍历中同时进行映射和过滤要慢(在 OTP 29 / Elixir 1.20.4 上对于 N≈2e5 的整数约慢 ~6.93× 和 ~6.18×)。 + +## 如何测量 + +- 测试工具:`packages/beamperf/examples/bench/evidence_be004_be005.exs` +- 时间测量:与 be-map-merge-hot 相同;N=200000 个整数,包含负数和正数 +- 对 A:`map(*2) |> filter(>0)` 与 单次 reduce + 反转(匹配优秀的 `one_pass`) +- 对 B:`map(+1) |> reject(==0)` 与 单次 reduce + 反转 +- 原始行数据:[`be-enum-map-filter-chain.jsonl`](../be-enum-map-filter-chain.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| Elixir | **1.18.3** | +| OTP | **27** | +| 中值之前 (`map_filter_chain`) | 4.546 毫秒 | +| (`one_pass_reduce`) 后的中位数 | 0.656 毫秒 | +| **前/后比例** | **~6.93×** | +| `map_reject_chain` 之前的中位数 | 5.498 毫秒 | +| `one_pass_reject` 后的中位数 | 0.890 毫秒 | +| **前/后比例** | **~6.18×** | + +## 与 lint 的对应 + +`beamperf-lint` 会在 `Enum.map` 被管道送入 `Enum.filter` / `Enum.reject` 时触发 `be-enum-map-filter-chain`。严重性保持 **提示** (~4.5×;不升级)。 + +## 限制 + +- 纯整数变换的微基准测试;实际管道可能更倾向于清晰或使用 Stream。 +- `Enum` 是急切的 — Stream 会改变情况。 +- 一次遍历的 reduce 先构建反转的列表然后再反转;其他一次遍历的结构(推导式)可能略有不同。 diff --git a/packages/beamperf/evidence/zh-CN/be-io-puts-hot.md b/packages/beamperf/evidence/zh-CN/be-io-puts-hot.md new file mode 100644 index 0000000..7745472 --- /dev/null +++ b/packages/beamperf/evidence/zh-CN/be-io-puts-hot.md @@ -0,0 +1,34 @@ +# be-io-puts-hot — 循环中的 IO.puts + +**状态:** 已测量 + +## 主张 + +每次迭代的 `IO.puts`(空组长)比在 OTP 29 / Elixir 1.20.4 上先求和然后一次性 `IO.puts` 慢好几个数量级。之后的样本是 0.000834 毫秒,所以比例接近计时器的下限。 + +## 如何测量 + +- 测试工具:`examples/bench/evidence_be001_be003.exs` +- N=5000;IO 重定向到空进程 + +- 原始行: [`be-io-puts-hot.jsonl`](../be-io-puts-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| otp | **27** | +| elixir | **1.18.3** | +| 循环内 `io_puts_in_loop` 之前的中位值 | 3.627 毫秒 | +| `io_once_after` 之后的中位数 | 0.000834 毫秒 | +| **前/后比例** | **~4349×** | + +## 与 lint 的对应 + +`beamperf-lint` be-io-puts-hot:保持软件包默认严重性;证据不会自动升级。 + +## 限制 + +- 输入/输出 / 消息传递成本;依赖环境。 +- 默认保持 **关闭**。 diff --git a/packages/beamperf/evidence/zh-CN/be-list-concat-reduce.md b/packages/beamperf/evidence/zh-CN/be-list-concat-reduce.md new file mode 100644 index 0000000..050307a --- /dev/null +++ b/packages/beamperf/evidence/zh-CN/be-list-concat-reduce.md @@ -0,0 +1,34 @@ +# be-list-concat-reduce — 在 reduce 中使用 list ++ + +**状态:** 已测量 + +## 主张 + +`acc ++ [i]` 在 reduce 中比 cons + `Enum.reverse/1` 慢得多(约 776 倍),在 OTP 29 / Elixir 1.20.4 上测试。 + +## 如何测量 + +- 测试工具:`examples/bench/evidence_be001_be003.exs` +- N=8000;预热=3;9次的中位数;`:erlang.monotonic_time(:nanosecond)` + +- 原始行: [`be-list-concat-reduce.jsonl`](../be-list-concat-reduce.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| otp | **27** | +| elixir | **1.18.3** | +| 中位数之前 (`list_plusplus`) | 95.715 毫秒 | +| (`cons_reverse`) 之后的中位数 | 0.123 毫秒 | +| **前/后比率** | **~776×** | + +## 与 lint 的对应 + +`beamperf-lint` be-list-concat-reduce:保持软件包默认严重性;证据不会自动升级。 + +## 限制 + +- 二次列表增长;实际代码应优先使用 cons 或 `:lists.reverse`。 +- 尽管比例很大,默认仍保留 **提示**(此轮不会升级严重性)。 diff --git a/packages/beamperf/evidence/zh-CN/be-map-merge-hot.md b/packages/beamperf/evidence/zh-CN/be-map-merge-hot.md new file mode 100644 index 0000000..429f7f3 --- /dev/null +++ b/packages/beamperf/evidence/zh-CN/be-map-merge-hot.md @@ -0,0 +1,44 @@ +# be-map-merge-hot — Map.merge / Keyword.merge 在循环中 + +**状态:** 已测量 + +## 主张 + +`Enum.reduce` 对每个元素调用 `Map.merge/2` 比使用 `Map.put/3` 更新要慢(在 OTP 29 / Elixir 1.20.4 上,多键折叠大约慢 2.30 倍,单键大约慢 1.77 倍)。在 reduce 中使用 `Keyword.merge/2` 比 `Keyword.put/3` 大约慢 **2.00 倍**。 + +## 如何测量 + +- 测试环境:`packages/beamperf/examples/bench/evidence_be004_be005.exs` +- 测量时间:`:erlang.monotonic_time(:nanosecond)`;预热=3;取9次的中位数;每次采样前进行垃圾回收 +- 对照 A:N=5000 个映射 `%{i => i, :tag => …}` 使用 `Map.merge` 与 N=5000 个 `%{k:, v:}` 使用 `Map.put`(匹配良好的 `put_in_loop`) +- 对照 B:N=5000 个单键映射 —— 在相同形状下使用 `Map.merge` 与 `Map.put` +- 对照 C:N=2000 个关键字列表 —— `Keyword.merge` 与 `Keyword.put` +- 原始数据文件行:[`be-map-merge-hot.jsonl`](../be-map-merge-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| Elixir | **1.18.3** | +| OTP | **27** | +| 中位数之前(`map_merge_reduce`) | 2.099 毫秒 | +| (`map_put_kv`) 之后的中位数 | 0.912 毫秒 | +| **前后比例** | **~2.30×** | +| `map_merge_single` 之前的中位数 | 1.726 毫秒 | +| (`map_put_single`) 后的中位数 | 0.973 毫秒 | +| **前/后比例** | **~1.77×** | +| 中值之前 (`keyword_merge_reduce`) | 2.304 毫秒 | +| (`keyword_put`) 之后的中位数 | 1.153 毫秒 | +| **前/后比例** | **~2.00×** | + +## 与 lint 的对应 + +`beamperf-lint` 在 reduce/loop 内部的 `Map.merge` / `Keyword.merge` 上触发 be-map-merge-hot。严重性保持 **提示** — 最强的一对约 7 倍是真实的,但此次未升级。 + +## 限制 + +- A 组比较多键合并映射与 key-value 结构的放置(夹具对齐;工作内容不完全相同)。 +- 单键合并与放置的差异仅约 1.3 倍——当映射保持很小时,增长成本很小。 +- 关键字列表是线性的;合并成本随关键字长度增加而增加。 +- 未测量 `:maps.merge/2` NIF 或大型持久结构。 diff --git a/packages/beamperf/zh-CN/CONTRIBUTING.md b/packages/beamperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/beamperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/beamperf/zh-CN/README.md b/packages/beamperf/zh-CN/README.md new file mode 100644 index 0000000..2a1e3f9 --- /dev/null +++ b/packages/beamperf/zh-CN/README.md @@ -0,0 +1,38 @@ +# 光束性能 + +> **范围说明:** **beamperf** 在一个 BEAM 工具包下涵盖 **Elixir 和 Erlang**。Lint 扫描 `.ex` / `.exs` / `.erl` 文件。优先使用 Elixir CLI;Erlang 模式在同一遍扫描中被识别。不同于 JVM 工具包。 + +Elixir + Erlang (BEAM) 性能笔记、小示例,以及 `beamperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**。`be-list-concat-reduce`、`be-map-merge-hot` 和 `be-enum-map-filter-chain` **提示**;`be-io-puts-hot` **关闭**。删除了一条规则(墓碑,从未得到过 slug)。没有 G4。指南 v0。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/beam-performance-guide.md) — 来自 `docs/guide-src/` +- [`beamperf-lint`](../beamperf-lint/README.md) — 四条规则(参见其表格获取默认值) +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +避免在循环中使用 `list ++` 和字符串/`<>` 的增长(优先使用 cons + reverse 或 iolist),在频繁执行的循环中不要使用 `IO.puts` / `io:format`。 + +## 运行 lint + +```sh +elixir packages/beamperf/beamperf-lint/beamperf_lint.exs path/to/sources +bash packages/beamperf/scripts/smoke-lint.sh +bash packages/beamperf/scripts/self-lint.sh +``` + +仅在 **错误** 严重性发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ per-rule measured evidence (*.md + *.jsonl) and _removed/ tombstones +examples/ tiny demos +beamperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/cperf/docs/c-performance-guide.md b/packages/cperf/docs/c-performance-guide.md index b0948ea..8e92b12 100644 --- a/packages/cperf/docs/c-performance-guide.md +++ b/packages/cperf/docs/c-performance-guide.md @@ -2,15 +2,11 @@ Less allocation chatter, fewer surprise I/O stalls. -> **Scope: C11 / C17.** C++ remarks are incidental where the same pattern applies (e.g. `new` in a loop is still an allocator hit). Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks — they tell you where the checker fires, not how much wall time you would save. +> **Scope: C11 / C17.** C++ remarks are incidental where the same pattern applies (e.g. `new` in a loop is still an allocator hit). Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. > -> Companion: `cperf-lint/` — line-window heuristics for twelve rules (see § Lint map). Dogfooded on quickjs, lua, gcc (sparse), and sqlite `src/` (2026-10-21 Asia/Shanghai). That run does not cover the later hint rules; see [corpus.md](corpus.md). Corpus trees are not vendored. +> Companion: `cperf-lint/` — twelve line-window rules (see Lint map). Run against quickjs, lua, gcc (sparse), and sqlite `src/` (2026-10-21 Asia/Shanghai). That run does not cover the later hint rules; see [corpus.md](corpus.md). Corpus trees are not vendored. -## How to read this - -1. **Measure first** on a real workload (`perf`, cachegrind, ASan for bugs — not for speed). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. When a lint fires on engine / compiler code, check whether the call is debug, codegen, or intentional CLI I/O before “fixing” it. +Profile first on a real workload (`perf`, cachegrind; ASan for bugs, not speed). When a lint fires on engine or compiler code, check whether the call is debug, codegen, or intentional CLI I/O before changing it. ## Contents @@ -29,16 +25,15 @@ Less allocation chatter, fewer surprise I/O stalls. Order of attack (same as the shared XPerf playbook): -1. **Design / batching** — amortize network, disk, memory, CPU across batches. -2. **Allocation shape** — arenas, pools, capacity doubling (§1). -3. **Hot I/O** — keep `printf` / `fprintf` out of steady-state loops (§2). -4. **Layout / compiler** — SoA, `restrict`, LTO: measure; wrong `restrict` is UB. -5. **Verify** — counters and sanitizers (§3). +1. Design / batching — amortize network, disk, memory, and CPU across batches. +2. Allocation shape — arenas, pools, capacity doubling (§1). +3. Hot I/O — keep `printf` / `fprintf` out of steady-state loops (§2). +4. Layout / compiler — SoA, `restrict`, LTO: measure; wrong `restrict` is UB. +5. Verify — counters and sanitizers (§3). -Machine claims should record compiler, `-O` level, and CPU briefly. `cp-alloc-hot`, `cp-realloc-grow-one` and `cp-printf-hot` ship JSONL microbenches under `evidence/` (see [evidence-policy.md](evidence-policy.md)); other examples under `examples/` remain pattern demos (`cc -std=c17 -O2`). +Machine claims should record compiler, `-O` level, and CPU briefly. `cp-alloc-hot`, `cp-realloc-grow-one`, and `cp-printf-hot` ship JSONL microbenches under `evidence/` (see [evidence-policy.md](evidence-policy.md)); other examples under `examples/` remain pattern demos (`cc -std=c17 -O2`). Sources: [XPerf playbook](../../../docs/playbook.md) · [corpus.md](corpus.md) - ## 1. Allocation ### 1.1 Hot-path `malloc` / `calloc` @@ -47,28 +42,28 @@ Sources: [XPerf playbook](../../../docs/playbook.md) · [corpus.md](corpus.md) | Pattern | When | |---------|------| -| **Arena** (bump pointer; free / reset all at once) | Same lifetime: request, frame, parse, batch | -| **Size-class pool** | Many equal-sized objects; checkout / release | -| **Stack / VLA / fixed buffer** | Bound known and small; lifetime nested in the caller | -| **Hoist out of the loop** | One buffer reused across iterations | +| Arena (bump pointer; free / reset all at once) | Same lifetime: request, frame, parse, batch | +| Size-class pool | Many equal-sized objects; checkout / release | +| Stack / VLA / fixed buffer | Bound known and small; lifetime nested in the caller | +| Hoist out of the loop | One buffer reused across iterations | See `examples/m01_arena.c`. -Lint: **cp-alloc-hot** — `malloc` / `calloc` / `realloc` inside a `for` / `while` / `do` body (brace-depth heuristic). +Lint: `cp-alloc-hot` (`malloc` / `calloc` / `realloc` inside a `for` / `while` / `do` body; brace-depth heuristic). -**Measured evidence (error-tier):** [`evidence/cp-alloc-hot.md`](../evidence/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../evidence/cp-alloc-hot.jsonl) — `cc -std=c11 -O2` (Debian gcc 14.2.0), malloc+free per iter vs arena bump ~**10.8×** (N=2e5, median of 9). Policy: [evidence-policy.md](evidence-policy.md). Harness: `examples/bench/cp001_malloc_vs_arena.c`. +Measured evidence (error-tier): [`evidence/cp-alloc-hot.md`](../evidence/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../evidence/cp-alloc-hot.jsonl) — `cc -std=c11 -O2` (Debian gcc 14.2.0), malloc+free per iter vs arena bump ~**10.8×** (N=2e5, median of 9). Policy: [evidence-policy.md](evidence-policy.md). Harness: `examples/bench/cp001_malloc_vs_arena.c`. -**Evidence (dogfood, not bench):** On mature VMs, true hot-path `malloc`-in-loop is rare after skips. After r2 wrapper / brace fixes: quickjs cp-alloc-hot **0**; lua cp-alloc-hot **0**. Remaining cp-alloc-hot pressure is mostly compact trees and glue: sqlite `src/` r3 **14** cp-alloc-hot (e.g. `tclsqlite.c` realloc in a line loop); gcc sparse r3 **28** cp-alloc-hot. Thin `return realloc(...)` wrappers were false positives in r1 (cleared in r2). Treat cp-alloc-hot hits as review prompts — init loops that allocate once per known N are often fine. +Corpus hits (not a bench): On mature VMs, true hot-path `malloc`-in-loop is rare after skips. After r2 wrapper / brace fixes: quickjs cp-alloc-hot **0**; lua cp-alloc-hot **0**. Remaining cp-alloc-hot pressure is mostly compact trees and glue: sqlite `src/` r3 **14** cp-alloc-hot (e.g. `tclsqlite.c` realloc in a line loop); gcc sparse r3 **28** cp-alloc-hot. Thin `return realloc(...)` wrappers were false positives in r1 (cleared in r2). Treat cp-alloc-hot hits as review prompts — init loops that allocate once per known N are often fine. ### 1.2 Growth without capacity (cp-realloc-grow-one) Repeated `realloc(p, n + 1)` (or `old + 1`) is quadratic and moves buffers. Track `len` and `cap` separately; grow by doubling (or another fixed factor). -Lint: **cp-realloc-grow-one** — best-effort regex for `realloc` grow-by-one. +Lint: `cp-realloc-grow-one` (best-effort regex for `realloc` grow-by-one; default error). -**Measured evidence (error-tier):** [`evidence/cp-realloc-grow-one.md`](../evidence/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../evidence/cp-realloc-grow-one.jsonl) — realloc(+1) vs capacity doubling ~**12.6×** (N=1e5); companion STATIC-VS-O2 case4 ~**3×**. Severity **error**. +Measured evidence (error-tier): [`evidence/cp-realloc-grow-one.md`](../evidence/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../evidence/cp-realloc-grow-one.jsonl) — realloc(+1) vs capacity doubling ~**12.6×** (N=1e5); companion STATIC-VS-O2 case4 ~**3×**. Severity **error**. -**Evidence (dogfood):** Almost never fired on engine corpora — quickjs / lua **0**; gcc sparse **1**; sqlite **1** (r3). Pattern is uncommon in mature code; the measured gate still makes greenfield growth fail CI under `--preset app`. +Corpus hits: Almost never fired on engine corpora — quickjs / lua **0**; gcc sparse **1**; sqlite **1** (r3). Pattern is uncommon in mature code; the measured gate still makes greenfield growth fail CI under `--preset app`. ### 1.3 What corpora taught about noise @@ -86,11 +81,11 @@ RSS spikes from unbounded freelists / caches need a working-set cap — not meas ### 1.5 `strlen` in loop condition (cp-strlen-in-cond) -`for (i = 0; i < strlen(s); i++)` recomputes length every iteration (often O(n²)). Hoist `size_t n = strlen(s)` (or use a known length). Lint: **cp-strlen-in-cond** — default **hint**. About 125× when the compiler must recompute the length; about 1× when `-O2` hoists it. +`for (i = 0; i < strlen(s); i++)` recomputes length every iteration (often O(n²)). Hoist `size_t n = strlen(s)` (or use a known length). Lint: `cp-strlen-in-cond` (default hint). About 125× when the compiler must recompute the length; about 1× when `-O2` hoists it. ### 1.6 `strcat` in a loop (cp-strcat-hot) -Repeated `strcat`/`strncat` walks the destination from the start each time. Prefer an end pointer + `memcpy`, or `stpcpy` where available. Lint: **cp-strcat-hot** — default **off** after G3. A local run of fixed chunks was about 221× an end pointer. Enable with `--severity cp-strcat-hot=hint`. +Repeated `strcat`/`strncat` walks the destination from the start each time. Prefer an end pointer + `memcpy`, or `stpcpy` where available. Lint: `cp-strcat-hot` (default off after G3). A local run of fixed chunks was about 221× an end pointer. Enable with `--severity cp-strcat-hot=hint`. ### 1.7 `alloca` in a loop (cp-alloca-hot) @@ -116,13 +111,13 @@ Formatting and stdio locking in a tight loop dwarf the work you meant to measure - Gate debug printers behind `#ifdef DUMP` / `#ifdef DEBUG` (or a runtime `if (dump_file)`). - Use binary / length-prefixed logs when volume is high. -Lint: **cp-printf-hot** — `printf` / `fprintf` / `puts` (and friends) inside a loop-body window. +Lint: `cp-printf-hot` (`printf` / `fprintf` / `puts` and friends inside a loop-body window). -**Measured evidence (partial agreement; default off):** [`evidence/cp-printf-hot.md`](../evidence/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../evidence/cp-printf-hot.jsonl) — hot unbuffered vs buffered fprintf ~**4.8×** (N=8e4); silent ceiling ~**561×**. STATIC-VS-O2 case3 `#if 0` / dead dump ≈ **1.0×** with static fire → keep below **error** (default **off**). See [evidence-policy.md](evidence-policy.md). +Measured evidence (partial agreement; default off): [`evidence/cp-printf-hot.md`](../evidence/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../evidence/cp-printf-hot.jsonl) — hot unbuffered vs buffered fprintf ~**4.8×** (N=8e4); silent ceiling ~**561×**. STATIC-VS-O2 case3 `#if 0` / dead dump ≈ **1.0×** with static fire → keep below error (default off). See [evidence-policy.md](evidence-policy.md). ### 2.2 When dump / debug is intentional -Engine and compiler trees are full of **correct** looped printers: RTL dumps, strlen dumps, atom leakage traces, bytecode dumps. Heuristically cp-printf-hot is right (“this is I/O in a loop”); operationally it is noise for CI on those trees. +Engine and compiler trees are full of correct looped printers: RTL dumps, strlen dumps, atom leakage traces, bytecode dumps. The lint is right that this is I/O in a loop; for CI on those trees it is usually noise. **cperf-lint suppressions (r2–r3):** @@ -135,9 +130,9 @@ Engine and compiler trees are full of **correct** looped printers: RTL dumps, st Fixtures: `fixtures/good/dump_printf_loop.c`, `ifdef_dump_printf.c`, `dump_file_fprintf.c`. -### 2.3 Corpus residual (heuristic dogfood) +### 2.3 Corpus residual (lint hit counts) -Counts after path skips + suppressions. **Not** “printf cost in ms”. +Counts after path skips + suppressions. These are not “printf cost in ms”. | Corpus | r1 → r2 → r3 (cp-printf-hot-heavy totals) | Notes | |--------|-------------------------------------|-------| @@ -146,7 +141,7 @@ Counts after path skips + suppressions. **Not** “printf cost in ms”. | gcc sparse | — → 4392 → **357** (cp-printf-hot:328) | Under-500 target met; top residual: `collect2.cc` ctor-list writers (~49), `tree-ssa-strlen.cc` dump (~31), `mkdeps.cc` (~27), `reload.cc` (~20), `makeucnid.cc` table emitter (~11) | | sqlite `src/` | — → — → **93** (cp-printf-hot:78) | `treeview.c` debug tree (~25), `vdbe.c` / `vdbeaux.c` plan/bytecode dump, `bitvec.c` test dump | -**Reading residual paths:** `collect2` / `mkdeps` / `makeucnid` / `dtoa` table gens are **codegen writers** — looped `fprintf` is the product. Optional future quiet mode: path allowlist or “codegen writer” suppress. Do not “optimize away” intentional emitters because the lint fired. +Reading residual paths: `collect2` / `mkdeps` / `makeucnid` / `dtoa` table gens are codegen writers — looped `fprintf` is the product. An optional quiet mode could path-allowlist or suppress codegen writers. Do not “optimize away” intentional emitters just because the lint fired. ### 2.4 CLI tools @@ -173,7 +168,7 @@ Record compiler (`cc -v`), flags (`-O2` / `-O3` / `-flto`), OS, and CPU model. C ASan / TSan / UBSan find bugs. They inflate time and RSS. Use them on the same change set that touches allocators or races; quote numbers from **unsanitized** Release builds only. -### 3.4 Measured gates vs dogfood +### 3.4 Measured gates vs corpus hits In-tree JSONL under `evidence/` (see [evidence-policy.md](evidence-policy.md)): @@ -192,15 +187,15 @@ In-tree JSONL under `evidence/` (see [evidence-policy.md](evidence-policy.md)): | cp-alloca-hot | **hint** | stack 1280000 vs 64 bytes; wall clock about 1×; not corpus-triaged | | cp-struct-by-value | **hint** | JSONL about 12.3× cross-TU; same TU about 1×; not corpus-triaged | -Corpus residual counts (§2.3) remain lint dogfood only. Pattern demos (`examples/m01_arena.c`) still illustrate APIs without replacing the JSONL gate. +Corpus residual counts (§2.3) remain lint hit counts only. Pattern demos (`examples/m01_arena.c`) still illustrate APIs without replacing the JSONL gate. ## 4. Hot calls -A hot loop should not format a new string, open a file, sample the clock, or compile a regular expression on every pass. Format into a buffer you already have, open the stream once, read the clock outside the steady state, and compile a pattern once. These lints are **hint**. The 2026-10-22 corpus verdict does not cover them. +A hot loop should not format a new string, open a file, sample the clock, or compile a regular expression on every pass. Format into a buffer you already have, open the stream once, read the clock outside the steady state, and compile a pattern once. These lints are hint. The 2026-10-22 corpus verdict does not cover them. -- **cp-sprintf-hot** — `sprintf` or `snprintf` in a loop body. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-fopen-hot** — `fopen` in a loop body. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-gettime-hot** — `gettimeofday` or `clock_gettime` in a loop body. A bare `time` call is not matched; that name is too broad. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-regex-hot** — `regcomp` in a loop body. `regexec` is not matched. A microbench was about 9.7× versus compiling once. The rule stays **hint** and is not corpus-triaged. +- `cp-sprintf-hot` — `sprintf` or `snprintf` in a loop body. JSONL is present. Default hint; not corpus-triaged. +- `cp-fopen-hot` — `fopen` in a loop body. JSONL is present. Default hint; not corpus-triaged. +- `cp-gettime-hot` — `gettimeofday` or `clock_gettime` in a loop body. A bare `time` call is not matched; that name is too broad. JSONL is present. Default hint; not corpus-triaged. +- `cp-regex-hot` — `regcomp` in a loop body. `regexec` is not matched. A microbench was about 9.7× versus compiling once. Default hint; not corpus-triaged. A fixture hit is not a measured speedup. See [evidence-policy.md](evidence-policy.md). ## 5. Flag layout @@ -211,7 +206,7 @@ Several on/off properties of one record, such as which power sources a vehicle h Turning alignment off, or a packing pragma, is a target choice. Some CPUs fault or slow down on unaligned loads. This hint does not ask for that. -**cp-wide-bool-flags** fires when one struct has four or more full-width `bool` or `_Bool` members, or four or more `int` / `unsigned` members named `is_`, `has_`, or `can_`, and the struct has no bit-field. It is a **hint**. JSONL is present. The rule stays hint, and the 2026-10-22 corpus verdict does not cover it. A handful of records on a desktop can ignore it. A similar object-header rule on the JVM was removed after measurement showed no footprint win; this C hint is not that measurement. +Lint: `cp-wide-bool-flags` (default hint). Fires when one struct has four or more full-width `bool` or `_Bool` members, or four or more `int` / `unsigned` members named `is_`, `has_`, or `can_`, and the struct has no bit-field. JSONL is present. The 2026-10-22 corpus verdict does not cover it. A handful of records on a desktop can ignore it. A similar object-header rule on the JVM was removed after measurement showed no footprint win; this C hint is not that measurement. A fixture hit is not a measured saving. See [evidence-policy.md](evidence-policy.md). ## Lint map diff --git a/packages/cperf/docs/guide-src/00_head.md b/packages/cperf/docs/guide-src/00_head.md index 8efe33a..1530d02 100644 --- a/packages/cperf/docs/guide-src/00_head.md +++ b/packages/cperf/docs/guide-src/00_head.md @@ -2,15 +2,11 @@ Less allocation chatter, fewer surprise I/O stalls. -> **Scope: C11 / C17.** C++ remarks are incidental where the same pattern applies (e.g. `new` in a loop is still an allocator hit). Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks — they tell you where the checker fires, not how much wall time you would save. +> **Scope: C11 / C17.** C++ remarks are incidental where the same pattern applies (e.g. `new` in a loop is still an allocator hit). Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. > -> Companion: `cperf-lint/` — line-window heuristics for twelve rules (see § Lint map). Dogfooded on quickjs, lua, gcc (sparse), and sqlite `src/` (2026-10-21 Asia/Shanghai). That run does not cover the later hint rules; see [corpus.md](corpus.md). Corpus trees are not vendored. +> Companion: `cperf-lint/` — twelve line-window rules (see Lint map). Run against quickjs, lua, gcc (sparse), and sqlite `src/` (2026-10-21 Asia/Shanghai). That run does not cover the later hint rules; see [corpus.md](corpus.md). Corpus trees are not vendored. -## How to read this - -1. **Measure first** on a real workload (`perf`, cachegrind, ASan for bugs — not for speed). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. When a lint fires on engine / compiler code, check whether the call is debug, codegen, or intentional CLI I/O before “fixing” it. +Profile first on a real workload (`perf`, cachegrind; ASan for bugs, not speed). When a lint fires on engine or compiler code, check whether the call is debug, codegen, or intentional CLI I/O before changing it. ## Contents @@ -29,13 +25,12 @@ Less allocation chatter, fewer surprise I/O stalls. Order of attack (same as the shared XPerf playbook): -1. **Design / batching** — amortize network, disk, memory, CPU across batches. -2. **Allocation shape** — arenas, pools, capacity doubling (§1). -3. **Hot I/O** — keep `printf` / `fprintf` out of steady-state loops (§2). -4. **Layout / compiler** — SoA, `restrict`, LTO: measure; wrong `restrict` is UB. -5. **Verify** — counters and sanitizers (§3). +1. Design / batching — amortize network, disk, memory, and CPU across batches. +2. Allocation shape — arenas, pools, capacity doubling (§1). +3. Hot I/O — keep `printf` / `fprintf` out of steady-state loops (§2). +4. Layout / compiler — SoA, `restrict`, LTO: measure; wrong `restrict` is UB. +5. Verify — counters and sanitizers (§3). -Machine claims should record compiler, `-O` level, and CPU briefly. `cp-alloc-hot`, `cp-realloc-grow-one` and `cp-printf-hot` ship JSONL microbenches under `evidence/` (see [evidence-policy.md](evidence-policy.md)); other examples under `examples/` remain pattern demos (`cc -std=c17 -O2`). +Machine claims should record compiler, `-O` level, and CPU briefly. `cp-alloc-hot`, `cp-realloc-grow-one`, and `cp-printf-hot` ship JSONL microbenches under `evidence/` (see [evidence-policy.md](evidence-policy.md)); other examples under `examples/` remain pattern demos (`cc -std=c17 -O2`). Sources: [XPerf playbook](../../../docs/playbook.md) · [corpus.md](corpus.md) - diff --git a/packages/cperf/docs/guide-src/01_allocation.md b/packages/cperf/docs/guide-src/01_allocation.md index 06bbb90..2b04832 100644 --- a/packages/cperf/docs/guide-src/01_allocation.md +++ b/packages/cperf/docs/guide-src/01_allocation.md @@ -6,28 +6,28 @@ | Pattern | When | |---------|------| -| **Arena** (bump pointer; free / reset all at once) | Same lifetime: request, frame, parse, batch | -| **Size-class pool** | Many equal-sized objects; checkout / release | -| **Stack / VLA / fixed buffer** | Bound known and small; lifetime nested in the caller | -| **Hoist out of the loop** | One buffer reused across iterations | +| Arena (bump pointer; free / reset all at once) | Same lifetime: request, frame, parse, batch | +| Size-class pool | Many equal-sized objects; checkout / release | +| Stack / VLA / fixed buffer | Bound known and small; lifetime nested in the caller | +| Hoist out of the loop | One buffer reused across iterations | See `examples/m01_arena.c`. -Lint: **cp-alloc-hot** — `malloc` / `calloc` / `realloc` inside a `for` / `while` / `do` body (brace-depth heuristic). +Lint: `cp-alloc-hot` (`malloc` / `calloc` / `realloc` inside a `for` / `while` / `do` body; brace-depth heuristic). -**Measured evidence (error-tier):** [`evidence/cp-alloc-hot.md`](../evidence/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../evidence/cp-alloc-hot.jsonl) — `cc -std=c11 -O2` (Debian gcc 14.2.0), malloc+free per iter vs arena bump ~**10.8×** (N=2e5, median of 9). Policy: [evidence-policy.md](evidence-policy.md). Harness: `examples/bench/cp001_malloc_vs_arena.c`. +Measured evidence (error-tier): [`evidence/cp-alloc-hot.md`](../evidence/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../evidence/cp-alloc-hot.jsonl) — `cc -std=c11 -O2` (Debian gcc 14.2.0), malloc+free per iter vs arena bump ~**10.8×** (N=2e5, median of 9). Policy: [evidence-policy.md](evidence-policy.md). Harness: `examples/bench/cp001_malloc_vs_arena.c`. -**Evidence (dogfood, not bench):** On mature VMs, true hot-path `malloc`-in-loop is rare after skips. After r2 wrapper / brace fixes: quickjs cp-alloc-hot **0**; lua cp-alloc-hot **0**. Remaining cp-alloc-hot pressure is mostly compact trees and glue: sqlite `src/` r3 **14** cp-alloc-hot (e.g. `tclsqlite.c` realloc in a line loop); gcc sparse r3 **28** cp-alloc-hot. Thin `return realloc(...)` wrappers were false positives in r1 (cleared in r2). Treat cp-alloc-hot hits as review prompts — init loops that allocate once per known N are often fine. +Corpus hits (not a bench): On mature VMs, true hot-path `malloc`-in-loop is rare after skips. After r2 wrapper / brace fixes: quickjs cp-alloc-hot **0**; lua cp-alloc-hot **0**. Remaining cp-alloc-hot pressure is mostly compact trees and glue: sqlite `src/` r3 **14** cp-alloc-hot (e.g. `tclsqlite.c` realloc in a line loop); gcc sparse r3 **28** cp-alloc-hot. Thin `return realloc(...)` wrappers were false positives in r1 (cleared in r2). Treat cp-alloc-hot hits as review prompts — init loops that allocate once per known N are often fine. ### 1.2 Growth without capacity (cp-realloc-grow-one) Repeated `realloc(p, n + 1)` (or `old + 1`) is quadratic and moves buffers. Track `len` and `cap` separately; grow by doubling (or another fixed factor). -Lint: **cp-realloc-grow-one** — best-effort regex for `realloc` grow-by-one. +Lint: `cp-realloc-grow-one` (best-effort regex for `realloc` grow-by-one; default error). -**Measured evidence (error-tier):** [`evidence/cp-realloc-grow-one.md`](../evidence/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../evidence/cp-realloc-grow-one.jsonl) — realloc(+1) vs capacity doubling ~**12.6×** (N=1e5); companion STATIC-VS-O2 case4 ~**3×**. Severity **error**. +Measured evidence (error-tier): [`evidence/cp-realloc-grow-one.md`](../evidence/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../evidence/cp-realloc-grow-one.jsonl) — realloc(+1) vs capacity doubling ~**12.6×** (N=1e5); companion STATIC-VS-O2 case4 ~**3×**. Severity **error**. -**Evidence (dogfood):** Almost never fired on engine corpora — quickjs / lua **0**; gcc sparse **1**; sqlite **1** (r3). Pattern is uncommon in mature code; the measured gate still makes greenfield growth fail CI under `--preset app`. +Corpus hits: Almost never fired on engine corpora — quickjs / lua **0**; gcc sparse **1**; sqlite **1** (r3). Pattern is uncommon in mature code; the measured gate still makes greenfield growth fail CI under `--preset app`. ### 1.3 What corpora taught about noise @@ -45,11 +45,11 @@ RSS spikes from unbounded freelists / caches need a working-set cap — not meas ### 1.5 `strlen` in loop condition (cp-strlen-in-cond) -`for (i = 0; i < strlen(s); i++)` recomputes length every iteration (often O(n²)). Hoist `size_t n = strlen(s)` (or use a known length). Lint: **cp-strlen-in-cond** — default **hint**. About 125× when the compiler must recompute the length; about 1× when `-O2` hoists it. +`for (i = 0; i < strlen(s); i++)` recomputes length every iteration (often O(n²)). Hoist `size_t n = strlen(s)` (or use a known length). Lint: `cp-strlen-in-cond` (default hint). About 125× when the compiler must recompute the length; about 1× when `-O2` hoists it. ### 1.6 `strcat` in a loop (cp-strcat-hot) -Repeated `strcat`/`strncat` walks the destination from the start each time. Prefer an end pointer + `memcpy`, or `stpcpy` where available. Lint: **cp-strcat-hot** — default **off** after G3. A local run of fixed chunks was about 221× an end pointer. Enable with `--severity cp-strcat-hot=hint`. +Repeated `strcat`/`strncat` walks the destination from the start each time. Prefer an end pointer + `memcpy`, or `stpcpy` where available. Lint: `cp-strcat-hot` (default off after G3). A local run of fixed chunks was about 221× an end pointer. Enable with `--severity cp-strcat-hot=hint`. ### 1.7 `alloca` in a loop (cp-alloca-hot) diff --git a/packages/cperf/docs/guide-src/02_hot_io.md b/packages/cperf/docs/guide-src/02_hot_io.md index 31b9ef1..a0c4fa2 100644 --- a/packages/cperf/docs/guide-src/02_hot_io.md +++ b/packages/cperf/docs/guide-src/02_hot_io.md @@ -8,13 +8,13 @@ Formatting and stdio locking in a tight loop dwarf the work you meant to measure - Gate debug printers behind `#ifdef DUMP` / `#ifdef DEBUG` (or a runtime `if (dump_file)`). - Use binary / length-prefixed logs when volume is high. -Lint: **cp-printf-hot** — `printf` / `fprintf` / `puts` (and friends) inside a loop-body window. +Lint: `cp-printf-hot` (`printf` / `fprintf` / `puts` and friends inside a loop-body window). -**Measured evidence (partial agreement; default off):** [`evidence/cp-printf-hot.md`](../evidence/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../evidence/cp-printf-hot.jsonl) — hot unbuffered vs buffered fprintf ~**4.8×** (N=8e4); silent ceiling ~**561×**. STATIC-VS-O2 case3 `#if 0` / dead dump ≈ **1.0×** with static fire → keep below **error** (default **off**). See [evidence-policy.md](evidence-policy.md). +Measured evidence (partial agreement; default off): [`evidence/cp-printf-hot.md`](../evidence/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../evidence/cp-printf-hot.jsonl) — hot unbuffered vs buffered fprintf ~**4.8×** (N=8e4); silent ceiling ~**561×**. STATIC-VS-O2 case3 `#if 0` / dead dump ≈ **1.0×** with static fire → keep below error (default off). See [evidence-policy.md](evidence-policy.md). ### 2.2 When dump / debug is intentional -Engine and compiler trees are full of **correct** looped printers: RTL dumps, strlen dumps, atom leakage traces, bytecode dumps. Heuristically cp-printf-hot is right (“this is I/O in a loop”); operationally it is noise for CI on those trees. +Engine and compiler trees are full of correct looped printers: RTL dumps, strlen dumps, atom leakage traces, bytecode dumps. The lint is right that this is I/O in a loop; for CI on those trees it is usually noise. **cperf-lint suppressions (r2–r3):** @@ -27,9 +27,9 @@ Engine and compiler trees are full of **correct** looped printers: RTL dumps, st Fixtures: `fixtures/good/dump_printf_loop.c`, `ifdef_dump_printf.c`, `dump_file_fprintf.c`. -### 2.3 Corpus residual (heuristic dogfood) +### 2.3 Corpus residual (lint hit counts) -Counts after path skips + suppressions. **Not** “printf cost in ms”. +Counts after path skips + suppressions. These are not “printf cost in ms”. | Corpus | r1 → r2 → r3 (cp-printf-hot-heavy totals) | Notes | |--------|-------------------------------------|-------| @@ -38,7 +38,7 @@ Counts after path skips + suppressions. **Not** “printf cost in ms”. | gcc sparse | — → 4392 → **357** (cp-printf-hot:328) | Under-500 target met; top residual: `collect2.cc` ctor-list writers (~49), `tree-ssa-strlen.cc` dump (~31), `mkdeps.cc` (~27), `reload.cc` (~20), `makeucnid.cc` table emitter (~11) | | sqlite `src/` | — → — → **93** (cp-printf-hot:78) | `treeview.c` debug tree (~25), `vdbe.c` / `vdbeaux.c` plan/bytecode dump, `bitvec.c` test dump | -**Reading residual paths:** `collect2` / `mkdeps` / `makeucnid` / `dtoa` table gens are **codegen writers** — looped `fprintf` is the product. Optional future quiet mode: path allowlist or “codegen writer” suppress. Do not “optimize away” intentional emitters because the lint fired. +Reading residual paths: `collect2` / `mkdeps` / `makeucnid` / `dtoa` table gens are codegen writers — looped `fprintf` is the product. An optional quiet mode could path-allowlist or suppress codegen writers. Do not “optimize away” intentional emitters just because the lint fired. ### 2.4 CLI tools diff --git a/packages/cperf/docs/guide-src/03_measurement.md b/packages/cperf/docs/guide-src/03_measurement.md index 3462d09..bfb5a9e 100644 --- a/packages/cperf/docs/guide-src/03_measurement.md +++ b/packages/cperf/docs/guide-src/03_measurement.md @@ -19,7 +19,7 @@ Record compiler (`cc -v`), flags (`-O2` / `-O3` / `-flto`), OS, and CPU model. C ASan / TSan / UBSan find bugs. They inflate time and RSS. Use them on the same change set that touches allocators or races; quote numbers from **unsanitized** Release builds only. -### 3.4 Measured gates vs dogfood +### 3.4 Measured gates vs corpus hits In-tree JSONL under `evidence/` (see [evidence-policy.md](evidence-policy.md)): @@ -38,4 +38,4 @@ In-tree JSONL under `evidence/` (see [evidence-policy.md](evidence-policy.md)): | cp-alloca-hot | **hint** | stack 1280000 vs 64 bytes; wall clock about 1×; not corpus-triaged | | cp-struct-by-value | **hint** | JSONL about 12.3× cross-TU; same TU about 1×; not corpus-triaged | -Corpus residual counts (§2.3) remain lint dogfood only. Pattern demos (`examples/m01_arena.c`) still illustrate APIs without replacing the JSONL gate. +Corpus residual counts (§2.3) remain lint hit counts only. Pattern demos (`examples/m01_arena.c`) still illustrate APIs without replacing the JSONL gate. diff --git a/packages/cperf/docs/guide-src/04_hot_calls.md b/packages/cperf/docs/guide-src/04_hot_calls.md index f733cb5..35dd3e7 100644 --- a/packages/cperf/docs/guide-src/04_hot_calls.md +++ b/packages/cperf/docs/guide-src/04_hot_calls.md @@ -1,10 +1,10 @@ ## 4. Hot calls -A hot loop should not format a new string, open a file, sample the clock, or compile a regular expression on every pass. Format into a buffer you already have, open the stream once, read the clock outside the steady state, and compile a pattern once. These lints are **hint**. The 2026-10-22 corpus verdict does not cover them. +A hot loop should not format a new string, open a file, sample the clock, or compile a regular expression on every pass. Format into a buffer you already have, open the stream once, read the clock outside the steady state, and compile a pattern once. These lints are hint. The 2026-10-22 corpus verdict does not cover them. -- **cp-sprintf-hot** — `sprintf` or `snprintf` in a loop body. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-fopen-hot** — `fopen` in a loop body. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-gettime-hot** — `gettimeofday` or `clock_gettime` in a loop body. A bare `time` call is not matched; that name is too broad. JSONL is present. The rule stays hint and is not corpus-triaged. -- **cp-regex-hot** — `regcomp` in a loop body. `regexec` is not matched. A microbench was about 9.7× versus compiling once. The rule stays **hint** and is not corpus-triaged. +- `cp-sprintf-hot` — `sprintf` or `snprintf` in a loop body. JSONL is present. Default hint; not corpus-triaged. +- `cp-fopen-hot` — `fopen` in a loop body. JSONL is present. Default hint; not corpus-triaged. +- `cp-gettime-hot` — `gettimeofday` or `clock_gettime` in a loop body. A bare `time` call is not matched; that name is too broad. JSONL is present. Default hint; not corpus-triaged. +- `cp-regex-hot` — `regcomp` in a loop body. `regexec` is not matched. A microbench was about 9.7× versus compiling once. Default hint; not corpus-triaged. A fixture hit is not a measured speedup. See [evidence-policy.md](evidence-policy.md). diff --git a/packages/cperf/docs/guide-src/05_flag_layout.md b/packages/cperf/docs/guide-src/05_flag_layout.md index 5debcb8..193cb74 100644 --- a/packages/cperf/docs/guide-src/05_flag_layout.md +++ b/packages/cperf/docs/guide-src/05_flag_layout.md @@ -6,6 +6,6 @@ Several on/off properties of one record, such as which power sources a vehicle h Turning alignment off, or a packing pragma, is a target choice. Some CPUs fault or slow down on unaligned loads. This hint does not ask for that. -**cp-wide-bool-flags** fires when one struct has four or more full-width `bool` or `_Bool` members, or four or more `int` / `unsigned` members named `is_`, `has_`, or `can_`, and the struct has no bit-field. It is a **hint**. JSONL is present. The rule stays hint, and the 2026-10-22 corpus verdict does not cover it. A handful of records on a desktop can ignore it. A similar object-header rule on the JVM was removed after measurement showed no footprint win; this C hint is not that measurement. +Lint: `cp-wide-bool-flags` (default hint). Fires when one struct has four or more full-width `bool` or `_Bool` members, or four or more `int` / `unsigned` members named `is_`, `has_`, or `can_`, and the struct has no bit-field. JSONL is present. The 2026-10-22 corpus verdict does not cover it. A handful of records on a desktop can ignore it. A similar object-header rule on the JVM was removed after measurement showed no footprint win; this C hint is not that measurement. A fixture hit is not a measured saving. See [evidence-policy.md](evidence-policy.md). diff --git a/packages/cperf/docs/guide-src/zh-CN/00_head.md b/packages/cperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..19e2798 --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,36 @@ +# C 性能指南(v0) + +少一点无谓分配,少一点意外 I/O 卡顿。 + +> **范围:C11 / C17。** 文中顺带提到的 C++ 备注仅在同一模式适用时成立(例如循环里的 `new` 仍是分配器开销)。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 +> +> 配套:`cperf-lint/` —— 十二条行窗规则(见 Lint 对照表)。已在 quickjs、lua、gcc(稀疏)以及 sqlite `src/` 上跑过(2026-10-21 Asia/Shanghai)。该次运行未覆盖较晚的 hint 规则;见 [corpus.md](corpus.md)。语料树未 vendored。 + +先在真实负载上测量(`perf`、cachegrind;ASan 找 bug,不是测速)。当 lint 打在引擎或编译器代码上时,先确认调用是调试、代码生成,还是有意的 CLI I/O,再改。 + +## 目录 + +- [0. 范围与方法](#0-范围与方法) +- [1. 分配](#1-分配) +- [2. 热路径 I/O](#2-热路径-io) +- [3. 测量](#3-测量) +- [4. 热路径调用](#4-热路径调用) +- [5. 标志布局](#5-标志布局) +- [Lint 对照表](#lint-对照表) +- [参考](#参考) + +--- + +## 0. 范围与方法 + +进攻顺序(与共享 XPerf playbook 相同): + +1. 设计 / 批处理 —— 把网络、磁盘、内存、CPU 摊到批次上。 +2. 分配形态 —— arena、池、容量倍增(§1)。 +3. 热路径 I/O —— 稳态循环里不要放 `printf` / `fprintf`(§2)。 +4. 布局 / 编译器 —— SoA、`restrict`、LTO:先测;错误的 `restrict` 是 UB。 +5. 验证 —— 计数器与 sanitizer(§3)。 + +机器侧论断应简要记录编译器、`-O` 级别和 CPU。`cp-alloc-hot`、`cp-realloc-grow-one` 与 `cp-printf-hot` 在 `evidence/` 下提供 JSONL 微基准(见 [evidence-policy.md](evidence-policy.md));`examples/` 下其余示例仍是模式演示(`cc -std=c17 -O2`)。 + +来源:[XPerf playbook](../../../../docs/zh-CN/playbook.md) · [corpus.md](corpus.md) diff --git a/packages/cperf/docs/guide-src/zh-CN/01_allocation.md b/packages/cperf/docs/guide-src/zh-CN/01_allocation.md new file mode 100644 index 0000000..5bd68d7 --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/01_allocation.md @@ -0,0 +1,66 @@ +## 1. 分配 + +### 1.1 热路径 `malloc` / `calloc` + +紧循环里的 `malloc` / `free` 会付分配器开销、可能碎片化,并给每次迭代加上失败路径。优先: + +| 模式 | 何时 | +|------|------| +| **Arena**(bump 指针;一次性释放 / 重置) | 同一生命周期:请求、帧、解析、批次 | +| **按尺寸分级的池** | 大量等长对象;检出 / 归还 | +| **栈 / VLA / 固定缓冲** | 上界已知且小;生命周期嵌在调用方内 | +| **提到循环外** | 跨迭代复用同一缓冲 | + +见 `examples/m01_arena.c`。 + +Lint:**cp-alloc-hot** —— `for` / `while` / `do` 体内的 `malloc` / `calloc` / `realloc`(花括号深度启发式)。 + +**实测证据(error 档):** [`evidence/cp-alloc-hot.md`](../../evidence/zh-CN/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../../evidence/cp-alloc-hot.jsonl) —— `cc -std=c11 -O2`(Debian gcc 14.2.0),每轮 malloc+free 对比 arena bump 约 **10.8×**(N=2e5,9 次中位数)。策略:[evidence-policy.md](evidence-policy.md)。Harness:`examples/bench/cp001_malloc_vs_arena.c`。 + +证据(语料命中,非墙钟): 在成熟 VM 上,跳过后真正的热路径「循环 malloc」很少。r2 包装 / 花括号修复后:quickjs cp-alloc-hot **0**;lua cp-alloc-hot **0**。剩余压力主要在紧凑树与胶水:sqlite `src/` r3 **14**(如 `tclsqlite.c` 行循环里 realloc);gcc 稀疏 r3 **28**。单行 `return realloc(...)` 包装在 r1 是误报(r2 已清)。把 cp-alloc-hot 命中当作评审提示——按已知 N 只分配一次的初始化循环往往没问题。 + +### 1.2 无容量的增长(cp-realloc-grow-one) + +反复 `realloc(p, n + 1)`(或 `old + 1`)是平方级并会搬缓冲。分开跟踪 `len` 与 `cap`;按倍增(或其他固定因子)增长。 + +Lint:**cp-realloc-grow-one** —— 对 grow-by-one 的尽力正则。 + +**实测证据(error 档):** [`evidence/cp-realloc-grow-one.md`](../../evidence/zh-CN/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../../evidence/cp-realloc-grow-one.jsonl) —— realloc(+1) 对比容量倍增约 **12.6×**(N=1e5);配套 STATIC-VS-O2 case4 约 **3×**。严重级别 error。 + +证据(语料命中): 在引擎语料上几乎从不开火——quickjs / lua **0**;gcc 稀疏 **1**;sqlite **1**(r3)。成熟代码里该模式少见;实测门禁仍会让绿地增长在 `--preset app` 下失败。 + +### 1.3 语料教给我们的噪声 + +| 噪声类 | 示例 | cperf-lint 中的应对 | +|--------|------|---------------------| +| 生成 / harness | `unicode_gen.c`、`fuzz/`、`run-test262.c`、`ltests.c` | 路径跳过 | +| 薄分配器包装 | `return realloc(p, n);` | 跳过单语句包装(r2) | +| 无花括号 `for` 粘上下一句 | 单行循环后接包装 | 清除 `pending_loop`(r2) | + +路径跳过还覆盖 gcc `testsuite/`、`gen*.c/cc`、`*generate*`、`generated/`、`*dump*` 源。完整清单:[corpus.md](corpus.md)。 + +### 1.4 暂定 + +无界 freelist / 缓存引起的 RSS 尖峰需要工作集上限——树内尚未测。SoA vs AoS:`examples/p01_soa.c`(布局演示,无计时声明)。 + +### 1.5 循环条件里的 `strlen`(cp-strlen-in-cond) + +`for (i = 0; i < strlen(s); i++)` 每次迭代重算长度(常为 O(n²))。提升 `size_t n = strlen(s)`(或用已知长度)。Lint:**cp-strlen-in-cond** —— 默认 hint。编译器必须重算时约 125×;`-O2` 能提升时约 1×。 + +### 1.6 循环里的 `strcat`(cp-strcat-hot) + +反复 `strcat`/`strncat` 每次从起点扫目标。优先用尾指针 + `memcpy`,或可用时用 `stpcpy`。Lint:**cp-strcat-hot** —— G3 后默认 off。本地固定块测试相对尾指针约 221×。用 `--severity cp-strcat-hot=hint` 开启。 + +### 1.7 循环里的 `alloca`(cp-alloca-hot) + +`alloca` 与 `__builtin_alloca` 活到函数返回。循环内调用每轮再叠一块。若不必全部保持存活,复用一块缓冲。若必须存活,栈仍会涨;带已知上界的堆缓冲是常见替代。 + +循环体内声明的 VLA 是另一模式,其存储随块结束。本 hint 不匹配 VLA。 + +hint。2026-10-22 语料裁决未覆盖本 slug。本地一次运行让每块地址保持存活:20000 次 × 64 字节覆盖 1280000 字节栈,对比单缓冲 64 字节。每块只碰两字节时约 1×,故不是加速声明。见 [evidence](../../evidence/zh-CN/cp-alloca-hot.md)。 + +### 1.8 大结构体按值传递(cp-struct-by-value) + +按值参数在调用处复制记录。当同文件定义了该 struct、可理解成员下界至少 512 字节且至少四个字段、参数为无 `*` 的 `struct Name x` 时开火。下界按 LP64 宽度,跳过不理解的成员。数组参数不当作整记录拷贝。 + +hint。不是通用加速。2026-10-22 语料裁决未覆盖。512 字节记录(八个 `int` 加 `unsigned char[480]`)跨翻译单元约 12.3× 对比指针;同翻译单元静态函数约 1×(编译器消拷贝)。更小记录未达阈值。见 [evidence](../../evidence/zh-CN/cp-struct-by-value.md)。 diff --git a/packages/cperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/cperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..5589afc --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,45 @@ +## 2. 热路径 I/O + +### 2.1 循环里的 `printf` / `fprintf`(cp-printf-hot) + +紧循环里的格式化与 stdio 锁往往盖过你本想测量的工作。优先: + +- 先攒到缓冲;批次结束再写一次。 +- 把调试打印关在 `#ifdef DUMP` / `#ifdef DEBUG`(或运行时 `if (dump_file)`)后面。 +- 量大时用二进制 / 长度前缀日志。 + +Lint:**cp-printf-hot** —— 循环体窗口内的 `printf` / `fprintf` / `puts`(及同类)。 + +**实测证据(部分同意;默认 off):** [`evidence/cp-printf-hot.md`](../../evidence/zh-CN/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../../evidence/cp-printf-hot.jsonl) —— 热路径无缓冲对比缓冲 fprintf 约 **4.8×**(N=8e4);静默天花板约 **561×**。STATIC-VS-O2 case3 `#if 0` / 死 dump ≈ **1.0×** 却静态开火 → 保持低于 error(默认 off)。见 [evidence-policy.md](evidence-policy.md)。 + +### 2.2 有意的 dump / 调试 + +引擎与编译器树里充满**正确**的循环打印:RTL dump、strlen dump、atom 泄漏追踪、字节码 dump。启发式上 cp-printf-hot 说得对(「这是循环里的 I/O」);对那些树做 CI 时,操作上却是噪声。 + +**cperf-lint 抑制(r2–r3):** + +- 函数名匹配 `dump|debug|trace|log_`,或以 `print` 开头/结尾(不含 `sprintf`)。 +- 匹配 `DUMP|DEBUG|ENABLE_*DUMP|GATHER_STATISTICS|ENABLE_CHECKING` 的 `#if` / `#ifdef` / `#elif` 帧。 +- 调用行上的 dump 流:`*dump_file`、`*_dump`、`dump_*`、`sched_dump`、`asm_out_file`、`stderr`。 +- 上一行 `if (dump_*)` 或 `#if…DUMP|DEBUG`。 +- 多行 `#define` 体(行以 `\` 结束)——头文件里的 asm 发射器。 +- 路径跳过:`gen*.c/cc`、`*generate*`、`*dump*` 源、`bitintpow10*`、… + +夹具:`fixtures/good/dump_printf_loop.c`、`ifdef_dump_printf.c`、`dump_file_fprintf.c`。 + +### 2.3 语料残留(lint 命中计数) + +路径跳过 + 抑制后的计数。**不是**「printf 多少毫秒」。 + +| 语料 | r1 → r2 → r3(cp-printf-hot-heavy 合计) | 备注 | +|------|------------------------------------------|------| +| quickjs | 258 → 76 → **62**(r3 全为 cp-printf-hot) | 残留:`quickjs.c` atom/调试 printf(~29)、`dtoa.c` 表发射(~15)、`qjsc.c` 进度、`libregexp.c` / `libunicode.c` 统计 | +| lua | 3 → 2 → **1** | 跳过后很安静 | +| gcc 稀疏 | — → 4392 → **357**(cp-printf-hot:328) | 已达低于 500 目标;主要残留:`collect2.cc` ctor 列表写(~49)、`tree-ssa-strlen.cc` dump(~31)、`mkdeps.cc`(~27)、`reload.cc`(~20)、`makeucnid.cc` 表发射(~11) | +| sqlite `src/` | — → — → **93**(cp-printf-hot:78) | `treeview.c` 调试树(~25)、`vdbe.c` / `vdbeaux.c` plan/字节码 dump、`bitvec.c` 测试 dump | + +**怎么读残留路径:** `collect2` / `mkdeps` / `makeucnid` / `dtoa` 表生成是**代码生成写入器**——循环 `fprintf` 就是产物。可选的未来安静模式:路径白名单或「codegen writer」抑制。不要因为 lint 开火就把有意的发射器「优化掉」。 + +### 2.4 CLI 工具 + +`qjsc.c` 一类 CLI 里的 `fprintf(stderr, …)` 进度往往可接受。优先限速或 `--verbose` 门控,而不是删掉用户可见进度。 diff --git a/packages/cperf/docs/guide-src/zh-CN/03_measurement.md b/packages/cperf/docs/guide-src/zh-CN/03_measurement.md new file mode 100644 index 0000000..2dda541 --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/03_measurement.md @@ -0,0 +1,41 @@ +## 3. 测量 + +说短一点、诚实一点:本 kit 不替代 `perf`。用下面工具判断某次 cperf-lint 命中是否要紧。 + +### 3.1 墙钟与计数器 + +```sh +perf stat -e cycles,instructions,cache-misses,branch-misses,task-clock -- ./app +perf record -g -- ./app && perf report # 尽量保留 frame pointers +``` + +记录编译器(`cc -v`)、标志(`-O2` / `-O3` / `-flto`)、OS 与 CPU 型号。比较分布(N 次最优或中位数),不要只看一个样本。 + +### 3.2 Cachegrind / callgrind + +`valgrind --tool=cachegrind ./app` 模拟缓存行为;没有 PMC 时有用。慢;不宜作为大树的 CI 门禁。 + +### 3.3 Sanitizer 不是测速工具 + +ASan / TSan / UBSan 找 bug。它们会放大时间与 RSS。在改动分配器或竞态的同一变更集上用它们;数字只引用**未开 sanitizer** 的 Release 构建。 + +### 3.4 实测门禁与语料命中 + +树内 `evidence/` 下的 JSONL(见 [evidence-policy.md](evidence-policy.md)): + +| 规则 | 严重级别 | 墙钟(本地运行) | +|------|----------|------------------| +| cp-alloc-hot | error | 循环 malloc 对比 arena 约 10.8× | +| cp-realloc-grow-one | error | realloc(+1) 对比倍增约 12.6× | +| cp-printf-hot | off(默认) | 热路径无缓冲/缓冲约 4.8×;`#if 0` 约 1.0× → 部分 | +| cp-strlen-in-cond | hint | 字符串可变时约 125×;能提升时约 1× | +| cp-strcat-hot | off(默认) | 约 221×;保持 off | +| cp-sprintf-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-fopen-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-gettime-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-wide-bool-flags | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-regex-hot | hint | JSONL 约 9.7×;未做语料 triage | +| cp-alloca-hot | hint | 栈 1280000 vs 64 字节;墙钟约 1×;未做语料 triage | +| cp-struct-by-value | hint | JSONL 跨 TU 约 12.3×;同 TU 约 1×;未做语料 triage | + +语料残留计数(§2.3)仍只是 lint 命中计数。模式演示(`examples/m01_arena.c`)仍说明 API,不替代 JSONL 门禁。 diff --git a/packages/cperf/docs/guide-src/zh-CN/04_hot_calls.md b/packages/cperf/docs/guide-src/zh-CN/04_hot_calls.md new file mode 100644 index 0000000..9e0582a --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/04_hot_calls.md @@ -0,0 +1,10 @@ +## 4. 热路径调用 + +热循环不应每轮格式化新字符串、打开文件、采样时钟或编译正则。写到已有缓冲、流只开一次、时钟读在稳态外、模式只编译一次。这些 lint 是 hint。2026-10-22 语料裁决未覆盖它们。 + +- **cp-sprintf-hot** —— 循环体内的 `sprintf` 或 `snprintf`。有 JSONL。保持 hint,未做语料 triage。 +- **cp-fopen-hot** —— 循环体内的 `fopen`。有 JSONL。保持 hint,未做语料 triage。 +- **cp-gettime-hot** —— 循环体内的 `gettimeofday` 或 `clock_gettime`。裸 `time` 不匹配(名字太宽)。有 JSONL。保持 hint,未做语料 triage。 +- **cp-regex-hot** —— 循环体内的 `regcomp`。不匹配 `regexec`。微基准相对「编译一次」约 9.7×。保持 hint,未做语料 triage。 + +夹具命中不等于实测加速。见 [evidence-policy.md](evidence-policy.md)。 diff --git a/packages/cperf/docs/guide-src/zh-CN/05_flag_layout.md b/packages/cperf/docs/guide-src/zh-CN/05_flag_layout.md new file mode 100644 index 0000000..5f2b04a --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/05_flag_layout.md @@ -0,0 +1,11 @@ +## 5. 标志布局 + +一条记录上的若干开关属性(例如某车有哪些动力源),在大量副本共享紧张 RAM 预算时,不必每个都占满一个整数。混合(电动与汽油并存)是位组合,不是互斥枚举值。 + +要紧的是 `sizeof`,它包含对齐填充。四个 `int` 标志占四个 `int`:`int` 16 位时 8 字节,32 位时 16。位域宽度不是那个尺寸。`unsigned` 位域常存进一个 `unsigned int`,所以四个 1 位字段仍可能占 4 字节。`unsigned char` 位域可共享一字节。单个 `unsigned` 掩码是另一种紧凑形式。在你交付的 ABI 上检查 `sizeof`。 + +关掉对齐或 packing pragma 是目标选择。有的 CPU 对未对齐加载会故障或变慢。本 hint 不要求那样做。 + +**cp-wide-bool-flags** 在一个 struct 有四个及以上全宽 `bool` / `_Bool` 成员,或四个及以上名为 `is_` / `has_` / `can_` 的 `int` / `unsigned` 成员、且没有位域时开火。这是 hint。有 JSONL。保持 hint;2026-10-22 语料裁决未覆盖。桌面上少量记录可以忽略。JVM 上类似的对象头规则在测量显示无占用收益后已移除;本 C hint 不是那次测量。 + +夹具命中不等于实测节省。见 [evidence-policy.md](evidence-policy.md)。 diff --git a/packages/cperf/docs/guide-src/zh-CN/99_tail.md b/packages/cperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..415cf28 --- /dev/null +++ b/packages/cperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,22 @@ +## Lint 对照表 + +| ID | 主题 | 严重级别 / 备注 | +|----|------|-----------------| +| cp-alloc-hot | 循环内 malloc/calloc/realloc | error — 有证据;VM 上少见;sqlite/gcc 十余到数十 | +| cp-realloc-grow-one | 无容量的 realloc 增长 | error — 有证据;成熟树几乎从不开火 | +| cp-printf-hot | 循环内 printf/fprintf/puts | 默认 off — 热路径 YES / 死代码 NEGATIVE;需要 dump 抑制 | +| cp-strlen-in-cond | 循环条件里的 strlen | hint — 重算约 125×,能提升约 1× | +| cp-strcat-hot | 循环内 strcat/strncat | 默认 off — 约 221×;冷 CLI/路径可接受 | +| cp-sprintf-hot | 循环内格式化写缓冲 | hint — 有 JSONL;未做语料 triage | +| cp-fopen-hot | 循环内打开文件 | hint — 有 JSONL;未做语料 triage | +| cp-gettime-hot | 循环内采样时钟(不含裸 `time`) | hint — 有 JSONL;未做语料 triage | +| cp-wide-bool-flags | 同一 struct 四个及以上全宽布尔标志 | hint — 有 JSONL;未做语料 triage | +| cp-regex-hot | 循环内 `regcomp`(不匹配 `regexec`) | hint — JSONL 约 9.7×;未做语料 triage | +| cp-alloca-hot | 循环内 `alloca` / `__builtin_alloca`(不匹配 VLA) | hint — 测过栈覆盖;墙钟约 1×;未做语料 triage | +| cp-struct-by-value | 可见下界 ≥512 字节且 ≥4 字段的 struct 按值参数 | hint — JSONL 跨 TU 约 12.3×;未做语料 triage | + +## 参考 + +- [corpus.md](corpus.md) — clone pin 与 r2→r3 表 +- 共享方法:[XPerf playbook](../../../../docs/zh-CN/playbook.md) +- 布局演示:`examples/p01_soa.c`、`examples/p02_restrict_note.c` diff --git a/packages/cperf/docs/zh-CN/c-performance-guide.md b/packages/cperf/docs/zh-CN/c-performance-guide.md new file mode 100644 index 0000000..ec4dae8 --- /dev/null +++ b/packages/cperf/docs/zh-CN/c-performance-guide.md @@ -0,0 +1,231 @@ +# C 性能指南(v0) + +少一点无谓分配,少一点意外 I/O 卡顿。 + +> **范围:C11 / C17。** 文中顺带提到的 C++ 备注仅在同一模式适用时成立(例如循环里的 `new` 仍是分配器开销)。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 +> +> 配套:`cperf-lint/` —— 十二条行窗规则(见 Lint 对照表)。已在 quickjs、lua、gcc(稀疏)以及 sqlite `src/` 上跑过(2026-10-21 Asia/Shanghai)。该次运行未覆盖较晚的 hint 规则;见 [corpus.md](corpus.md)。语料树未 vendored。 + +先在真实负载上测量(`perf`、cachegrind;ASan 找 bug,不是测速)。当 lint 打在引擎或编译器代码上时,先确认调用是调试、代码生成,还是有意的 CLI I/O,再改。 + +## 目录 + +- [0. 范围与方法](#0-范围与方法) +- [1. 分配](#1-分配) +- [2. 热路径 I/O](#2-热路径-io) +- [3. 测量](#3-测量) +- [4. 热路径调用](#4-热路径调用) +- [5. 标志布局](#5-标志布局) +- [Lint 对照表](#lint-对照表) +- [参考](#参考) + +--- + +## 0. 范围与方法 + +进攻顺序(与共享 XPerf playbook 相同): + +1. 设计 / 批处理 —— 把网络、磁盘、内存、CPU 摊到批次上。 +2. 分配形态 —— arena、池、容量倍增(§1)。 +3. 热路径 I/O —— 稳态循环里不要放 `printf` / `fprintf`(§2)。 +4. 布局 / 编译器 —— SoA、`restrict`、LTO:先测;错误的 `restrict` 是 UB。 +5. 验证 —— 计数器与 sanitizer(§3)。 + +机器侧论断应简要记录编译器、`-O` 级别和 CPU。`cp-alloc-hot`、`cp-realloc-grow-one` 与 `cp-printf-hot` 在 `evidence/` 下提供 JSONL 微基准(见 [evidence-policy.md](evidence-policy.md));`examples/` 下其余示例仍是模式演示(`cc -std=c17 -O2`)。 + +来源:[XPerf playbook](../../../../docs/zh-CN/playbook.md) · [corpus.md](corpus.md) +## 1. 分配 + +### 1.1 热路径 `malloc` / `calloc` + +紧循环里的 `malloc` / `free` 会付分配器开销、可能碎片化,并给每次迭代加上失败路径。优先: + +| 模式 | 何时 | +|------|------| +| **Arena**(bump 指针;一次性释放 / 重置) | 同一生命周期:请求、帧、解析、批次 | +| **按尺寸分级的池** | 大量等长对象;检出 / 归还 | +| **栈 / VLA / 固定缓冲** | 上界已知且小;生命周期嵌在调用方内 | +| **提到循环外** | 跨迭代复用同一缓冲 | + +见 `examples/m01_arena.c`。 + +Lint:**cp-alloc-hot** —— `for` / `while` / `do` 体内的 `malloc` / `calloc` / `realloc`(花括号深度启发式)。 + +**实测证据(error 档):** [`evidence/cp-alloc-hot.md`](../../evidence/zh-CN/cp-alloc-hot.md) · [`cp-alloc-hot.jsonl`](../../evidence/cp-alloc-hot.jsonl) —— `cc -std=c11 -O2`(Debian gcc 14.2.0),每轮 malloc+free 对比 arena bump 约 **10.8×**(N=2e5,9 次中位数)。策略:[evidence-policy.md](evidence-policy.md)。Harness:`examples/bench/cp001_malloc_vs_arena.c`。 + +证据(语料命中,非墙钟): 在成熟 VM 上,跳过后真正的热路径「循环 malloc」很少。r2 包装 / 花括号修复后:quickjs cp-alloc-hot **0**;lua cp-alloc-hot **0**。剩余压力主要在紧凑树与胶水:sqlite `src/` r3 **14**(如 `tclsqlite.c` 行循环里 realloc);gcc 稀疏 r3 **28**。单行 `return realloc(...)` 包装在 r1 是误报(r2 已清)。把 cp-alloc-hot 命中当作评审提示——按已知 N 只分配一次的初始化循环往往没问题。 + +### 1.2 无容量的增长(cp-realloc-grow-one) + +反复 `realloc(p, n + 1)`(或 `old + 1`)是平方级并会搬缓冲。分开跟踪 `len` 与 `cap`;按倍增(或其他固定因子)增长。 + +Lint:**cp-realloc-grow-one** —— 对 grow-by-one 的尽力正则。 + +**实测证据(error 档):** [`evidence/cp-realloc-grow-one.md`](../../evidence/zh-CN/cp-realloc-grow-one.md) · [`cp-realloc-grow-one.jsonl`](../../evidence/cp-realloc-grow-one.jsonl) —— realloc(+1) 对比容量倍增约 **12.6×**(N=1e5);配套 STATIC-VS-O2 case4 约 **3×**。严重级别 error。 + +证据(语料命中): 在引擎语料上几乎从不开火——quickjs / lua **0**;gcc 稀疏 **1**;sqlite **1**(r3)。成熟代码里该模式少见;实测门禁仍会让绿地增长在 `--preset app` 下失败。 + +### 1.3 语料教给我们的噪声 + +| 噪声类 | 示例 | cperf-lint 中的应对 | +|--------|------|---------------------| +| 生成 / harness | `unicode_gen.c`、`fuzz/`、`run-test262.c`、`ltests.c` | 路径跳过 | +| 薄分配器包装 | `return realloc(p, n);` | 跳过单语句包装(r2) | +| 无花括号 `for` 粘上下一句 | 单行循环后接包装 | 清除 `pending_loop`(r2) | + +路径跳过还覆盖 gcc `testsuite/`、`gen*.c/cc`、`*generate*`、`generated/`、`*dump*` 源。完整清单:[corpus.md](corpus.md)。 + +### 1.4 暂定 + +无界 freelist / 缓存引起的 RSS 尖峰需要工作集上限——树内尚未测。SoA vs AoS:`examples/p01_soa.c`(布局演示,无计时声明)。 + +### 1.5 循环条件里的 `strlen`(cp-strlen-in-cond) + +`for (i = 0; i < strlen(s); i++)` 每次迭代重算长度(常为 O(n²))。提升 `size_t n = strlen(s)`(或用已知长度)。Lint:**cp-strlen-in-cond** —— 默认 hint。编译器必须重算时约 125×;`-O2` 能提升时约 1×。 + +### 1.6 循环里的 `strcat`(cp-strcat-hot) + +反复 `strcat`/`strncat` 每次从起点扫目标。优先用尾指针 + `memcpy`,或可用时用 `stpcpy`。Lint:**cp-strcat-hot** —— G3 后默认 off。本地固定块测试相对尾指针约 221×。用 `--severity cp-strcat-hot=hint` 开启。 + +### 1.7 循环里的 `alloca`(cp-alloca-hot) + +`alloca` 与 `__builtin_alloca` 活到函数返回。循环内调用每轮再叠一块。若不必全部保持存活,复用一块缓冲。若必须存活,栈仍会涨;带已知上界的堆缓冲是常见替代。 + +循环体内声明的 VLA 是另一模式,其存储随块结束。本 hint 不匹配 VLA。 + +hint。2026-10-22 语料裁决未覆盖本 slug。本地一次运行让每块地址保持存活:20000 次 × 64 字节覆盖 1280000 字节栈,对比单缓冲 64 字节。每块只碰两字节时约 1×,故不是加速声明。见 [evidence](../../evidence/zh-CN/cp-alloca-hot.md)。 + +### 1.8 大结构体按值传递(cp-struct-by-value) + +按值参数在调用处复制记录。当同文件定义了该 struct、可理解成员下界至少 512 字节且至少四个字段、参数为无 `*` 的 `struct Name x` 时开火。下界按 LP64 宽度,跳过不理解的成员。数组参数不当作整记录拷贝。 + +hint。不是通用加速。2026-10-22 语料裁决未覆盖。512 字节记录(八个 `int` 加 `unsigned char[480]`)跨翻译单元约 12.3× 对比指针;同翻译单元静态函数约 1×(编译器消拷贝)。更小记录未达阈值。见 [evidence](../../evidence/zh-CN/cp-struct-by-value.md)。 +## 2. 热路径 I/O + +### 2.1 循环里的 `printf` / `fprintf`(cp-printf-hot) + +紧循环里的格式化与 stdio 锁往往盖过你本想测量的工作。优先: + +- 先攒到缓冲;批次结束再写一次。 +- 把调试打印关在 `#ifdef DUMP` / `#ifdef DEBUG`(或运行时 `if (dump_file)`)后面。 +- 量大时用二进制 / 长度前缀日志。 + +Lint:**cp-printf-hot** —— 循环体窗口内的 `printf` / `fprintf` / `puts`(及同类)。 + +**实测证据(部分同意;默认 off):** [`evidence/cp-printf-hot.md`](../../evidence/zh-CN/cp-printf-hot.md) · [`cp-printf-hot.jsonl`](../../evidence/cp-printf-hot.jsonl) —— 热路径无缓冲对比缓冲 fprintf 约 **4.8×**(N=8e4);静默天花板约 **561×**。STATIC-VS-O2 case3 `#if 0` / 死 dump ≈ **1.0×** 却静态开火 → 保持低于 error(默认 off)。见 [evidence-policy.md](evidence-policy.md)。 + +### 2.2 有意的 dump / 调试 + +引擎与编译器树里充满**正确**的循环打印:RTL dump、strlen dump、atom 泄漏追踪、字节码 dump。启发式上 cp-printf-hot 说得对(「这是循环里的 I/O」);对那些树做 CI 时,操作上却是噪声。 + +**cperf-lint 抑制(r2–r3):** + +- 函数名匹配 `dump|debug|trace|log_`,或以 `print` 开头/结尾(不含 `sprintf`)。 +- 匹配 `DUMP|DEBUG|ENABLE_*DUMP|GATHER_STATISTICS|ENABLE_CHECKING` 的 `#if` / `#ifdef` / `#elif` 帧。 +- 调用行上的 dump 流:`*dump_file`、`*_dump`、`dump_*`、`sched_dump`、`asm_out_file`、`stderr`。 +- 上一行 `if (dump_*)` 或 `#if…DUMP|DEBUG`。 +- 多行 `#define` 体(行以 `\` 结束)——头文件里的 asm 发射器。 +- 路径跳过:`gen*.c/cc`、`*generate*`、`*dump*` 源、`bitintpow10*`、… + +夹具:`fixtures/good/dump_printf_loop.c`、`ifdef_dump_printf.c`、`dump_file_fprintf.c`。 + +### 2.3 语料残留(lint 命中计数) + +路径跳过 + 抑制后的计数。**不是**「printf 多少毫秒」。 + +| 语料 | r1 → r2 → r3(cp-printf-hot-heavy 合计) | 备注 | +|------|------------------------------------------|------| +| quickjs | 258 → 76 → **62**(r3 全为 cp-printf-hot) | 残留:`quickjs.c` atom/调试 printf(~29)、`dtoa.c` 表发射(~15)、`qjsc.c` 进度、`libregexp.c` / `libunicode.c` 统计 | +| lua | 3 → 2 → **1** | 跳过后很安静 | +| gcc 稀疏 | — → 4392 → **357**(cp-printf-hot:328) | 已达低于 500 目标;主要残留:`collect2.cc` ctor 列表写(~49)、`tree-ssa-strlen.cc` dump(~31)、`mkdeps.cc`(~27)、`reload.cc`(~20)、`makeucnid.cc` 表发射(~11) | +| sqlite `src/` | — → — → **93**(cp-printf-hot:78) | `treeview.c` 调试树(~25)、`vdbe.c` / `vdbeaux.c` plan/字节码 dump、`bitvec.c` 测试 dump | + +**怎么读残留路径:** `collect2` / `mkdeps` / `makeucnid` / `dtoa` 表生成是**代码生成写入器**——循环 `fprintf` 就是产物。可选的未来安静模式:路径白名单或「codegen writer」抑制。不要因为 lint 开火就把有意的发射器「优化掉」。 + +### 2.4 CLI 工具 + +`qjsc.c` 一类 CLI 里的 `fprintf(stderr, …)` 进度往往可接受。优先限速或 `--verbose` 门控,而不是删掉用户可见进度。 +## 3. 测量 + +说短一点、诚实一点:本 kit 不替代 `perf`。用下面工具判断某次 cperf-lint 命中是否要紧。 + +### 3.1 墙钟与计数器 + +```sh +perf stat -e cycles,instructions,cache-misses,branch-misses,task-clock -- ./app +perf record -g -- ./app && perf report # 尽量保留 frame pointers +``` + +记录编译器(`cc -v`)、标志(`-O2` / `-O3` / `-flto`)、OS 与 CPU 型号。比较分布(N 次最优或中位数),不要只看一个样本。 + +### 3.2 Cachegrind / callgrind + +`valgrind --tool=cachegrind ./app` 模拟缓存行为;没有 PMC 时有用。慢;不宜作为大树的 CI 门禁。 + +### 3.3 Sanitizer 不是测速工具 + +ASan / TSan / UBSan 找 bug。它们会放大时间与 RSS。在改动分配器或竞态的同一变更集上用它们;数字只引用**未开 sanitizer** 的 Release 构建。 + +### 3.4 实测门禁与语料命中 + +树内 `evidence/` 下的 JSONL(见 [evidence-policy.md](evidence-policy.md)): + +| 规则 | 严重级别 | 墙钟(本地运行) | +|------|----------|------------------| +| cp-alloc-hot | error | 循环 malloc 对比 arena 约 10.8× | +| cp-realloc-grow-one | error | realloc(+1) 对比倍增约 12.6× | +| cp-printf-hot | off(默认) | 热路径无缓冲/缓冲约 4.8×;`#if 0` 约 1.0× → 部分 | +| cp-strlen-in-cond | hint | 字符串可变时约 125×;能提升时约 1× | +| cp-strcat-hot | off(默认) | 约 221×;保持 off | +| cp-sprintf-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-fopen-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-gettime-hot | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-wide-bool-flags | hint | 有 JSONL;保持 hint;未做语料 triage | +| cp-regex-hot | hint | JSONL 约 9.7×;未做语料 triage | +| cp-alloca-hot | hint | 栈 1280000 vs 64 字节;墙钟约 1×;未做语料 triage | +| cp-struct-by-value | hint | JSONL 跨 TU 约 12.3×;同 TU 约 1×;未做语料 triage | + +语料残留计数(§2.3)仍只是 lint 命中计数。模式演示(`examples/m01_arena.c`)仍说明 API,不替代 JSONL 门禁。 +## 4. 热路径调用 + +热循环不应每轮格式化新字符串、打开文件、采样时钟或编译正则。写到已有缓冲、流只开一次、时钟读在稳态外、模式只编译一次。这些 lint 是 hint。2026-10-22 语料裁决未覆盖它们。 + +- **cp-sprintf-hot** —— 循环体内的 `sprintf` 或 `snprintf`。有 JSONL。保持 hint,未做语料 triage。 +- **cp-fopen-hot** —— 循环体内的 `fopen`。有 JSONL。保持 hint,未做语料 triage。 +- **cp-gettime-hot** —— 循环体内的 `gettimeofday` 或 `clock_gettime`。裸 `time` 不匹配(名字太宽)。有 JSONL。保持 hint,未做语料 triage。 +- **cp-regex-hot** —— 循环体内的 `regcomp`。不匹配 `regexec`。微基准相对「编译一次」约 9.7×。保持 hint,未做语料 triage。 + +夹具命中不等于实测加速。见 [evidence-policy.md](evidence-policy.md)。 +## 5. 标志布局 + +一条记录上的若干开关属性(例如某车有哪些动力源),在大量副本共享紧张 RAM 预算时,不必每个都占满一个整数。混合(电动与汽油并存)是位组合,不是互斥枚举值。 + +要紧的是 `sizeof`,它包含对齐填充。四个 `int` 标志占四个 `int`:`int` 16 位时 8 字节,32 位时 16。位域宽度不是那个尺寸。`unsigned` 位域常存进一个 `unsigned int`,所以四个 1 位字段仍可能占 4 字节。`unsigned char` 位域可共享一字节。单个 `unsigned` 掩码是另一种紧凑形式。在你交付的 ABI 上检查 `sizeof`。 + +关掉对齐或 packing pragma 是目标选择。有的 CPU 对未对齐加载会故障或变慢。本 hint 不要求那样做。 + +**cp-wide-bool-flags** 在一个 struct 有四个及以上全宽 `bool` / `_Bool` 成员,或四个及以上名为 `is_` / `has_` / `can_` 的 `int` / `unsigned` 成员、且没有位域时开火。这是 hint。有 JSONL。保持 hint;2026-10-22 语料裁决未覆盖。桌面上少量记录可以忽略。JVM 上类似的对象头规则在测量显示无占用收益后已移除;本 C hint 不是那次测量。 + +夹具命中不等于实测节省。见 [evidence-policy.md](evidence-policy.md)。 +## Lint 对照表 + +| ID | 主题 | 严重级别 / 备注 | +|----|------|-----------------| +| cp-alloc-hot | 循环内 malloc/calloc/realloc | error — 有证据;VM 上少见;sqlite/gcc 十余到数十 | +| cp-realloc-grow-one | 无容量的 realloc 增长 | error — 有证据;成熟树几乎从不开火 | +| cp-printf-hot | 循环内 printf/fprintf/puts | 默认 off — 热路径 YES / 死代码 NEGATIVE;需要 dump 抑制 | +| cp-strlen-in-cond | 循环条件里的 strlen | hint — 重算约 125×,能提升约 1× | +| cp-strcat-hot | 循环内 strcat/strncat | 默认 off — 约 221×;冷 CLI/路径可接受 | +| cp-sprintf-hot | 循环内格式化写缓冲 | hint — 有 JSONL;未做语料 triage | +| cp-fopen-hot | 循环内打开文件 | hint — 有 JSONL;未做语料 triage | +| cp-gettime-hot | 循环内采样时钟(不含裸 `time`) | hint — 有 JSONL;未做语料 triage | +| cp-wide-bool-flags | 同一 struct 四个及以上全宽布尔标志 | hint — 有 JSONL;未做语料 triage | +| cp-regex-hot | 循环内 `regcomp`(不匹配 `regexec`) | hint — JSONL 约 9.7×;未做语料 triage | +| cp-alloca-hot | 循环内 `alloca` / `__builtin_alloca`(不匹配 VLA) | hint — 测过栈覆盖;墙钟约 1×;未做语料 triage | +| cp-struct-by-value | 可见下界 ≥512 字节且 ≥4 字段的 struct 按值参数 | hint — JSONL 跨 TU 约 12.3×;未做语料 triage | + +## 参考 + +- [corpus.md](corpus.md) — clone pin 与 r2→r3 表 +- 共享方法:[XPerf playbook](../../../../docs/zh-CN/playbook.md) +- 布局演示:`examples/p01_soa.c`、`examples/p02_restrict_note.c` diff --git a/packages/cperf/docs/zh-CN/corpus.md b/packages/cperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..1534c74 --- /dev/null +++ b/packages/cperf/docs/zh-CN/corpus.md @@ -0,0 +1,62 @@ +# cperf 语料库目标 + +大型树木**不会**被供应,语料库也不会随仓库一起分发:请将其克隆到仓库之外的任意本地目录,并将该路径传递给代码检查器。使用以下命令进行检查: + +```sh +cd packages/cperf/cperf-lint && make +./cperf-lint / --format json --no-fail +``` + +检查器是 **C11**(`cperf-lint`)。之前的 Python 原型已被移除。 + +## 固定目标 + +为2026-10-22的重新设计克隆(g3f)。 + +| 树 | 提交 | 大小 | Lint g3c → g3f(默认) | +|------|--------|------|--------------------------| +| **quickjs** | `7d1c13c` 浅 | 7.8M | **0 → 2** cp-strlen-in-cond | +| **lua** | `8cb93ba` shallow | 2.7M | **0 → 0** | +| **gcc** (sparse) | `152e0cc` | 176M | **5 → 7** (cp-strlen-in-cond:2 cp-alloc-hot:5) | +| **sqlite** | `0498915` sparse | 14M | **0 → 0** | + +路径跳过未更改的部分 (r3 + G3)。新:cp-strlen-in-cond strlen-in-cond; cp-strcat-hot strcat-in-loop (关闭)。 + +## 是否 corpus-ready? + +**是** — 重新设计后的 G3 2026-10-22(亚洲/上海)。默认残差 **9** +(cp-strlen-in-cond×4 TP + cp-alloc-hot×5,1 TP / 4 OK-习语)。FP+DOC+SKIP **0%**。来自 **cp-strlen-in-cond** 的真实 TP(循环条件中的 `strlen`)。 + +cp-alloc-hot/cp-realloc-grow-one 保持 **错误** (G4 JSONL)。 cp-printf-hot/cp-strcat-hot 默认 **关闭**。 cp-strlen-in-cond 默认 **提示**。 + +## 主要 + +| Tree | Status | +|------|--------| +| quickjs | **完成** (2× cp-strlen-in-cond TP) | +| gcc sparse | **完成** (cp-strlen-in-cond TP + cp-alloc-hot 混合) | +| sqlite | **完成**(默认情况下清除) | +| V8 | 延迟的 (C++ / clang AST) | + +## 次要 + +| Tree | Status | +|------|--------| +| lua | **完成**(清理)| +| redis / libuv / musl | 后期 | + +## 库存 + +```text +/ + quickjs/ + lua/ + gcc/ # sparse + sqlite/ # sparse src/ +``` + +不要将语料来源提交到 `packages/cperf`。 + +## 2026-10-23 — 新提示规则,未分类 + +`cp-sprintf-hot`、`cp-fopen-hot`、`cp-gettime-hot` 和 `cp-wide-bool-flags` 是**提示**。每个都有 JSONL 微基准,并且尚未经过 G2/G3。`cp-regex-hot` 是**提示**,并且有 JSONL(约 9.7×)。上述这些 slug 都不属于 2026-10-22 的语料库准备判定。 diff --git a/packages/cperf/docs/zh-CN/evidence-policy.md b/packages/cperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..63ccb20 --- /dev/null +++ b/packages/cperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,49 @@ +# cperf 证据政策 + +## 门槛 + +只有当 `packages/cperf/evidence/` 下同时存在以下两项时,cperf 规则才可以被记录/处理为 **`error`**: + +1. `.md` — 说明、如何测量、`cc` 版本/标志、限制 +2. `.jsonl` — ≥1 行前/后数据,字段包括 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts`,以及 `cc`(编译器和标志;`node` 和 `v8` 保持为空)。可选字段 `role`:`before` = 问题,`after` = 修复。 + +否则规则最多是 **`hint`**。 + +拥有 JSONL 是必要的但不充分的:如果测量到的契合度只有部分(在常见形状上进行静态执行而不使用实时时钟成本),保留 **`hint`** 并在 md 中记录负面情况。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重程度 | 证据 | 裁决 | +|------|----------|----------|---------| +| cp-alloc-hot | **错误** | [cp-alloc-hot.md](../../evidence/zh-CN/cp-alloc-hot.md) · [cp-alloc-hot.jsonl](../../evidence/cp-alloc-hot.jsonl) | 同意 ~**10.8×** malloc-in-loop 对比 arena | +| cp-realloc-grow-one | **错误** | [cp-realloc-grow-one.md](../../evidence/zh-CN/cp-realloc-grow-one.md) · [cp-realloc-grow-one.jsonl](../../evidence/cp-realloc-grow-one.jsonl) | 同意 ~**12.6×**(伴随 case4 ~3×);方向明确 | +| cp-printf-hot | **关闭**(默认) | [cp-printf-hot.md](../../evidence/zh-CN/cp-printf-hot.md) · [cp-printf-hot.jsonl](../../evidence/cp-printf-hot.jsonl) | 热 unbuf/buf ~**4.8×** 是;G3 语料库几乎全部 OK-习语/`#if 0` → **关闭**;使用 `--preset all` 或 `--severity cp-printf-hot=hint` | +| cp-strlen-in-cond | **提示** | [cp-strlen-in-cond.md](../../evidence/zh-CN/cp-strlen-in-cond.md) | 当字符串可能变化时约 125×;当 `-O2` 提升时约 0.93×;保留提示 | +| cp-strcat-hot | **关闭**(默认) | [cp-strcat-hot.md](../../evidence/zh-CN/cp-strcat-hot.md) | 比结束指针快约 221 倍;保持关闭(G3) | +| cp-sprintf-hot | **提示** | [cp-sprintf-hot.md](../../evidence/zh-CN/cp-sprintf-hot.md) · [cp-sprintf-hot.jsonl](../../evidence/cp-sprintf-hot.jsonl) | 待定;尚未语料库分类 | +| cp-fopen-hot | **提示** | [cp-fopen-hot.md](../../evidence/zh-CN/cp-fopen-hot.md) · [cp-fopen-hot.jsonl](../../evidence/cp-fopen-hot.jsonl) | 待定;未进行语料库分类 | +| cp-gettime-hot | **提示** | [cp-gettime-hot.md](../../evidence/zh-CN/cp-gettime-hot.md) · [cp-gettime-hot.jsonl](../../evidence/cp-gettime-hot.jsonl) | 待处理;未进行语料库分级;未匹配裸 `time` | +| cp-wide-bool-flags | **提示** | [cp-wide-bool-flags.md](../../evidence/zh-CN/cp-wide-bool-flags.md) · [cp-wide-bool-flags.jsonl](../../evidence/cp-wide-bool-flags.jsonl) | 存在 JSONL;保持提示;未经过语料筛选;四个全角标志,没有位字段 | +| cp-regex-hot | **提示** | [cp-regex-hot.md](../../evidence/zh-CN/cp-regex-hot.md) · [cp-regex-hot.jsonl](../../evidence/cp-regex-hot.jsonl) | 约 9.7×;保持提示;未进行语料库分类;`regexec` 未匹配 | +| cp-alloca-hot | **提示** | [cp-alloca-hot.md](../../evidence/zh-CN/cp-alloca-hot.md) · [cp-alloca-hot.jsonl](../../evidence/cp-alloca-hot.jsonl) | 栈覆盖 1280000 对比 64 字节;墙时钟约 1×;保持提示;未进行语料库分流;VLA 未匹配 | +| cp-struct-by-value | **提示** | [cp-struct-by-value.md](../../evidence/zh-CN/cp-struct-by-value.md) · [cp-struct-by-value.jsonl](../../evidence/cp-struct-by-value.jsonl) | 在 512 字节时跨 TU 约 12.3×;同 TU 约 1×;保持提示;未经过语料库筛选 | + +## 再生 + +```sh +bash packages/cperf/scripts/regen-evidence-cp001.sh +bash packages/cperf/scripts/regen-evidence-cp002.sh +bash packages/cperf/scripts/regen-evidence-cp003.sh +# optional correlation suite → examples/bench/build/static-vs-o2/ +bash packages/cperf/scripts/run-static-vs-o2.sh +``` + +如果比例有明显变化,请更新匹配的 `.md` 中值。 + +## CLI 预设 + +默认 `--preset app`(和 `engine`):**cp-alloc-hot=error**、**cp-realloc-grow-one=error**、**cp-strlen-in-cond=hint**、**cp-sprintf-hot=hint**、**cp-fopen-hot=hint**、**cp-gettime-hot=hint**、**cp-wide-bool-flags=hint**、**cp-regex-hot=hint**、**cp-alloca-hot=hint**、**cp-struct-by-value=hint**、**cp-printf-hot=off** 和 **cp-strcat-hot=off**。关闭的规则是 G3 2026-10-22 降级。提示未经过语料库分级。`cp-regex-hot`、`cp-alloca-hot` 和 `cp-struct-by-value` 有 JSONL 并保持提示状态。未知的 slug 保持 **hint**。 + +`--preset all` 会将每条规则提升为 dogfood 的错误级别。可覆盖:`--severity RULE=error|hint|off`。 diff --git a/packages/cperf/docs/zh-CN/rule-ids.md b/packages/cperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..51008ea --- /dev/null +++ b/packages/cperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,35 @@ +# cperf 规则 ID(简写) + +规范 ID 仅为 **slug**。旧地图中的数字是历史性的,**不**被接受。 + +| Slug | +|------| +| `cp-alloc-hot` | +| `cp-realloc-grow-one` | +| `cp-printf-hot` | +| `cp-strlen-in-cond` | +| `cp-strcat-hot` | +| `cp-sprintf-hot` | +| `cp-fopen-hot` | +| `cp-gettime-hot` | +| `cp-wide-bool-flags` | +| `cp-regex-hot` | +| `cp-alloca-hot` | +| `cp-struct-by-value` | + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或相应的工具包选项)。旧的 `*-in-loop` ID 会误导工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `cp-alloc-in-loop` | `cp-alloc-hot` | +| `cp-alloca-in-loop` | `cp-alloca-hot` | +| `cp-fopen-in-loop` | `cp-fopen-hot` | +| `cp-gettime-in-loop` | `cp-gettime-hot` | +| `cp-printf-in-loop` | `cp-printf-hot` | +| `cp-regex-in-loop` | `cp-regex-hot` | +| `cp-sprintf-in-loop` | `cp-sprintf-hot` | +| `cp-strcat-in-loop` | `cp-strcat-hot` | diff --git a/packages/cperf/evidence/zh-CN/cp-alloc-hot.md b/packages/cperf/evidence/zh-CN/cp-alloc-hot.md new file mode 100644 index 0000000..39a5ac1 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-alloc-hot.md @@ -0,0 +1,37 @@ +# cp-alloc-hot — 循环体中的分配 + +## 主张 + +在这台 Linux 机器上使用 `cc -O2` 时,在紧密循环的每次迭代中调用 `malloc`/`free` 要比从预先分配好的内存池中为相同数量的 N 个小对象分配内存慢得多。 + +## 如何测量 + +- 来源:`packages/cperf/examples/bench/cp001_malloc_vs_arena.c` +- 构建:`cc -std=c11 -O2 -o cp001_malloc_vs_arena cp001_malloc_vs_arena.c` +- 计时:`clock_gettime(CLOCK_MONOTONIC)`;预热=3;取9轮的中位数;N=200000 +- **之前:** 每次迭代 `malloc(sizeof(int))` + 写入 + `free` +- **之后:** 为 N 个整数分配一次 arena;每次迭代使用 bump 分配;只释放一次 arena +- 原始行:[`cp-alloc-hot.jsonl`](../cp-alloc-hot.jsonl) +- 配套:`scripts/run-static-vs-o2.sh` case1(相同模式;比率随测试环境配置变化) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| 编译器 | **cc (Debian 14.2.0-19) 14.2.0** · 参数 `cc -std=c11 -O2` | +| `malloc_free_in_loop` 之前的中位数 | 约 1.34 毫秒 | +| (`arena_bump`) 后的中位数 | ~0.12 毫秒 | +| **前后比例** | **~10.8×** | + +重新测量 2026-10-21 19:19 亚洲/上海:前一个闸值约为 ~10.0×(1.44 → 0.14 毫秒)。漂移不显著;声明保持不变。 + +## 与 lint 的对应 + +`cperf-lint` 的 cp-alloc-hot 会在 `for`/`while`/`do` 循环体内对 `malloc`/`calloc`/`realloc` 触发(根据花括号深度启发式判断;薄 `return realloc(...)` 包装将被跳过)。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md),严重性为 **错误**,因为该 JSONL 文件存在且一致。 + +## 限制 + +- 微型基准测试用于微小固定大小的分配;真实的热点路径可能需要使用内存池、栈缓冲区或提升复用——arena 是一种有效的解决方案,但不是唯一的。 +- 对于已知 N 的初始化循环,每次分配一次且生命周期较长,通常是可以的;当循环不是热点时,把 lint 视为代码审核提示。 +- 不衡量碎片化或多线程分配器的情况。 diff --git a/packages/cperf/evidence/zh-CN/cp-alloca-hot.md b/packages/cperf/evidence/zh-CN/cp-alloca-hot.md new file mode 100644 index 0000000..4296f33 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-alloca-hot.md @@ -0,0 +1,29 @@ +# cp-alloca-hot — 测量的堆栈增长,仍然提示 + +## 状态 + +**提示**。实时时钟 JSONL:[cp-alloca-hot.jsonl](../cp-alloca-hot.jsonl)。该文件是计时伴随文件。通过的测量如下堆栈覆盖。未进行语料库分拣。2026-10-22 的语料库裁定不涵盖此部分。在循环体中的 VLA 未匹配。 + +## 测量 + +clang 21.0.0,`-std=c11 -O2`。双方每次迭代写入两个字节,并保持一个易变累加器。前一方在每次循环中调用 `alloca(64)` 并保留第一个和最后一个块的地址,因此这些块会一直存在直到函数返回。Cover 是从最低字节到最高字节之后一个字节的距离。 + +| 角色 | 案例 | 堆栈字节 | 操作 | +|------|------|------------:|----:| +| 之后 | 一个缓冲区 | 64 | 20000 | +| 之前 | 每次迭代 `alloca` | 1280000 | 20000 | + +相同触摸的挂钟,那 20000 次迭代循环的 80 次调用。之后的时间不是零。比例约为 1。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 之后 | `one_buffer` | 0.295 | 1600000 | 1 | +| 之前 | `alloca_each_iter` | 0.293 | 1600000 | 0.993 | + +这不是一般的加速。提示存在的原因是每次迭代都会增加另一个64字节的块。在栈已经驻留后,重用一个缓冲区并没有使两个字节的访问更快。 + +在一个 2000 次迭代的循环中,一个 64 字节的 VLA 占用了 64 字节。该槽被重复使用,因此该模式没有被匹配。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。如果每个块必须在函数返回之前保持活跃,一个缓冲区的重写是错误的,栈仍然会增长。在进行任何默认更改之前,仍然需要对语料库进行分类。 diff --git a/packages/cperf/evidence/zh-CN/cp-fopen-hot.md b/packages/cperf/evidence/zh-CN/cp-fopen-hot.md new file mode 100644 index 0000000..a687ed7 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-fopen-hot.md @@ -0,0 +1,20 @@ +# cp-fopen-hot — 已测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[cp-fopen-hot.jsonl](../cp-fopen-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +2000 次打开一个一字节文件。打开一次后进行重绕/revc。 这是系统调用限制,而不是解析基准测试。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `fopen_once` | 0.068000 | 2000 | 1 | +| 之前 | `fopen_each_iter` | 28.792000 | 2000 | 423.413 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/cperf/evidence/zh-CN/cp-gettime-hot.md b/packages/cperf/evidence/zh-CN/cp-gettime-hot.md new file mode 100644 index 0000000..77c62e3 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-gettime-hot.md @@ -0,0 +1,20 @@ +# cp-gettime-hot — 已测量,仍提示 + +## 状态 + +**提示**。挂钟 JSONL:[cp-gettime-hot.jsonl](../cp-gettime-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +100000 次迭代。双方每次迭代都存储一个值,所以循环不能消除。之前调用 gettimeofday;之后使用在循环外采集的一个样本。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `clock_once` | 0.036000 | 100000 | 1 | +| 之前 | `gettimeofday_in_loop` | 1.194000 | 100000 | 33.167 | + +微基准显示了这个形状的成本。默认保持 **hint**,直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/cperf/evidence/zh-CN/cp-printf-hot.md b/packages/cperf/evidence/zh-CN/cp-printf-hot.md new file mode 100644 index 0000000..55483cf --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-printf-hot.md @@ -0,0 +1,43 @@ +# cp-printf-hot — 循环中的 printf / fprintf + +## 权利要求(狭义) + +当 `fprintf` 在强制无缓冲(`_IONBF`)的流上运行的**真正热的**路径时,墙钟时间比在该 Linux 机器上用大 `_IOFBF` 缓冲区在 `cc -O2` 下的**相同调用位置**慢好几倍。完全移除 I/O 则又快一个数量级。 + +## 我们*不*做的声明 + +仅仅出现 cp-printf-hot lint 并**不**意味着进程很慢:死掉的辅助程序、`#if 0` 代码块以及 dump/codegen 打印器会以约 **1.0×** 热路径时间触发扫描器(参见 STATIC-VS-O2 case3)。 + +## 如何测量 + +- 来源:`packages/cperf/examples/bench/cp003_printf_hot.c` +- 构建:`cc -std=c11 -O2 -o cp003_printf_hot cp003_printf_hot.c` +- 计时:`clock_gettime(CLOCK_MONOTONIC)`;预热=2;9轮的中位数;N=80000;输出 `/dev/null` +- **之前:** `setvbuf(_IONBF)` + 每次迭代 `fprintf` +- **之后:** `setvbuf` 1 MiB `_IOFBF` + 每次迭代相同 `fprintf` (+ `fflush`) +- **参考:** 仅计算的静默循环(上限) +- 原始数据行:[`cp-printf-hot.jsonl`](../cp-printf-hot.jsonl) +- 相关负值案例:`case3_dead_printf.c` / lint 探针 `case3_if0.c`(静态触发,无时间增量) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| 编译器 | **cc (Debian 14.2.0-19) 14.2.0** · 参数 `cc -std=c11 -O2` | +| 调用前中位数 (`unbuffered_fprintf`) | ~14.05 毫秒 | +| (`buffered_fprintf`) 后的中位数 | ~2.94 毫秒 | +| **未缓冲/缓冲 前后比** | **~4.8×** | +| 静默参考 | ~0.025 毫秒 · unbuf/静默 ≈ **561×** | + +Companion STATIC-VS-O2 case2 ≈ **3.8×** 未缓冲/缓冲;case3 死亡/`#if 0` ≈ **1.0×**。 + +## 与 lint 的对应 + +`cperf-lint` cp-printf-hot 会在循环体内检测 `printf`/`fprintf`/`puts`(及类似函数),同时考虑 dump/debug/`print*` / `#if DUMP|DEBUG|…` / 路径抑制。默认严重性为 **关闭**(G3 降级),根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md):对于 *高频* I/O 存在已测量的一致性,但“发现 ⇒ 成本”的相关性 **部分**(预处理器忽略 `#if 0`、dump 噪声)。仅当路径已知为热点时,可使用 `--severity cp-printf-hot=hint`(或 `error`)按树启用。 + +## 限制 + +- 扫描器不是预处理器;`#if 0` 仍然可能产生检测结果。 +- 缓冲与非缓冲是一种修复方法;通常正确的修复方法是在性能关键路径上根本不打印。 +- `/dev/null` 微基准低估了真实 tty/文件的成本;但仍足以表明在关键路径上这个问题很重要。 diff --git a/packages/cperf/evidence/zh-CN/cp-realloc-grow-one.md b/packages/cperf/evidence/zh-CN/cp-realloc-grow-one.md new file mode 100644 index 0000000..3d50e12 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-realloc-grow-one.md @@ -0,0 +1,37 @@ +# cp-realloc-grow-one — 按一增长的 realloc + +## 主张 + +在这台 Linux 机器上,当增长发生在被测路径上时,每次推入时使用 `realloc(p, (len+1) * sizeof T)` 增长缓冲区,比在 `cc -O2` 下使用容量加倍要慢得多。 + +## 如何测量 + +- 来源:`packages/cperf/examples/bench/cp002_realloc_growth.c` +- 构建:`cc -std=c11 -O2 -o cp002_realloc_growth cp002_realloc_growth.c` +- 时间测量:`clock_gettime(CLOCK_MONOTONIC)`;预热=3;取9轮中位数;N=100000 +- **之前:** 每次推入时使用 `realloc` 到 `len+1` +- **之后:** 当 `len+1 > cap` 时将容量翻倍 +- 原始数据行:[`cp-realloc-grow-one.jsonl`](../cp-realloc-grow-one.jsonl) +- 配套文件:通过 `scripts/run-static-vs-o2.sh` 的 `examples/bench/case4_realloc_growth.c` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| 编译器 | **cc (Debian 14.2.0-19) 14.2.0** · 参数 `cc -std=c11 -O2` | +| `realloc_plus_one` 之前的中位数 | ~0.63 毫秒 | +| `capacity_doubling` 后的中位数 | ~0.05 毫秒 | +| **前/后比例** | **~12.6×** | + +伴随 STATIC-VS-O2 case4(同一天,中位数为 7):嗅觉约 0.81 毫秒 → 清洁约 0.25 毫秒 ≈ **3.2×**。绝对比率因线束(时间窗口 / 预热)而异,但 **方向和与嗅觉的一致性是清楚的**。 + +## 与 lint 的对应 + +`cperf-lint` 会在尽力使用的 `realloc(..., …+1)` / `old+1` 样式第二个参数时触发。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md),严重性为 **错误** —— 当该模式热时,JSONL 存在并且实际时间一致。 + +## 限制 + +- 在成熟的树上很少见(语料本身几乎从未出现)。 +- 对于已知最终大小的合法一次性 `realloc(p, n+1)` 并不是问题;lint 是根据第二个参数的形式进行启发式检查的。 +- 不模拟自定义分配器或使用不同因子的摊销增长。 diff --git a/packages/cperf/evidence/zh-CN/cp-regex-hot.md b/packages/cperf/evidence/zh-CN/cp-regex-hot.md new file mode 100644 index 0000000..ae70dff --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-regex-hot.md @@ -0,0 +1,17 @@ +# cp-regex-hot — 测量过的,仍然提示 + +## 状态 +**提示**。挂钟 JSONL:[cp-regex-hot.jsonl](../cp-regex-hot.jsonl)。尚未进行语料库分级。2026-10-22 的语料库裁定不包括该标识符。`regexec` 未匹配。 + +## 测量 +双方在 `id-2026` 上调用 `regexec`。在每次迭代之前调用 `regcomp` 和 `regfree`。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 编译一次 | 0.834 | 2000 | 1 | +| 在每次迭代之前 | `regcomp` | 8.131 | 2000 | 9.749 | + +这是编译一次正则表达式规则的 C 版本兄弟。默认保持提示。 + +## 解除阻塞 +不要仅仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。在进行任何默认更改之前,仍然需要进行语料整理。 diff --git a/packages/cperf/evidence/zh-CN/cp-sprintf-hot.md b/packages/cperf/evidence/zh-CN/cp-sprintf-hot.md new file mode 100644 index 0000000..79f41e6 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-sprintf-hot.md @@ -0,0 +1,20 @@ +# cp-sprintf-hot — 已测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[cp-sprintf-hot.jsonl](../cp-sprintf-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识符。 + +## 测量 + +在 clang -O2 下进行 100000 次迭代。之前调用 sprintf 写入栈缓冲区;之后将十进制数字写入相同的缓冲区。输入是循环索引。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `digits_into_buffer` | 0.543000 | 100000 | 1 | +| 之前 | `sprintf_in_loop` | 7.553000 | 100000 | 13.910 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/cperf/evidence/zh-CN/cp-strcat-hot.md b/packages/cperf/evidence/zh-CN/cp-strcat-hot.md new file mode 100644 index 0000000..b4d3fd3 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-strcat-hot.md @@ -0,0 +1,16 @@ +# cp-strcat-hot — 已测量,保持关闭 + +**状态:** 已测量。默认保持 **关闭**(G3 冷 CLI 和路径生成器)。未重新升级。 + +## 主张 + +`strcat` 对 500 块每块 32 字节的数据重复 30 次,约为 **221×** 一个结束指针加 `memcpy`(3.097 毫秒 对比 0.014 毫秒)。长度匹配。 + +## 如何测量 + +- `clang -std=c11 -O2` +- 原始: [`cp-strcat-hot.jsonl`](../cp-strcat-hot.jsonl) + +## 限制 + +这个比例是将这个固定块重复放到一个空缓冲区上。一个已经到达末尾的目标,或者单个 `strcat`,是不同的形式。 diff --git a/packages/cperf/evidence/zh-CN/cp-strlen-in-cond.md b/packages/cperf/evidence/zh-CN/cp-strlen-in-cond.md new file mode 100644 index 0000000..719fd00 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-strlen-in-cond.md @@ -0,0 +1,17 @@ +# cp-strlen-in-cond — 已测量,仍然提示 + +**状态:** 已测量。默认保持 **提示**。未经过语料库筛选。 + +## 主张 + +当编译器必须假设字符串可能会改变(循环中读取一个 volatile 标志,一个 1500 字节字符串扫描 40 次)时,循环条件中的 `strlen` 大约是提升后的长度的 **125×**。在未改变的字符串上相同的写法大约是 **0.93×**:`-O2` 已经提升了 `strlen`。匹配器无法区分这两种情况。 + +## 如何测量 + +- `clang -std=c11 -O2` +- 双方对相同的字节求和。volatile 标志保持为 0,所以字符串没有改变。 +- 原始文件: [`cp-strlen-in-cond.jsonl`](../cp-strlen-in-cond.jsonl) + +## 限制 + +不要将 125× 理解为每个条件中 `strlen` 的成本。未改变的本地字符串已经被提升。 diff --git a/packages/cperf/evidence/zh-CN/cp-struct-by-value.md b/packages/cperf/evidence/zh-CN/cp-struct-by-value.md new file mode 100644 index 0000000..8b9a541 --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-struct-by-value.md @@ -0,0 +1,22 @@ +# cp-struct-by-value — 已测量,仍然提示 + +## 状态 + +**提示**。挂钟 JSONL:[cp-struct-by-value.jsonl](../cp-struct-by-value.jsonl)。未进行语料分类。2026-10-22 的语料裁决不涵盖此标识符。 + +## 测量 + +clang 21.0.0,`-std=c11 -O2`。被调用者在另一个翻译单元中。该记录有八个 `int` 字段和 `unsigned char pad[480]`(`sizeof` 512)。双方都调用该函数,并将两个字段加上第一个和最后一个填充字节加入一个易变的累加器中。11 次的中值。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 之后 | `pass_pointer` | 0.152 | 128000 | 1 | +| 之前 | `pass_by_value` | 1.864 | 128000 | 12.263 | + +在一个翻译单元中,一个静态的 `noinline` 对,使用相同的记录和相同的 128000 次调用,耗时为 0.153 毫秒,而不是 0.158 毫秒(比率 0.968)。编译器移除了拷贝。这不是普遍的加速。 + +当参数是 `struct Name x` 且没有 `*` 时,扫描器会触发,该结构体在同一文件中较早定义,它理解的成员下限至少为 512 字节,并且至少有四个这样的字段。该下限使用 LP64 宽度(`int` 4,`long` 和指针 8)并跳过嵌套类型、位字段以及其他它不理解的成员,因此可能会遗漏较大的记录。在重复测试中,一个 320 字节的记录为 5.848×,一个 448 字节的记录为 6.781×,因此这些大小不匹配。 + +## 解阻条件 + +不要仅凭这个文件将默认值设置为 `error`。同一文件的静态函数、冷调用或低估的记录大小启发式都可能抹去差异。在进行任何默认值更改之前,仍然需要进行语料筛选。 diff --git a/packages/cperf/evidence/zh-CN/cp-wide-bool-flags.md b/packages/cperf/evidence/zh-CN/cp-wide-bool-flags.md new file mode 100644 index 0000000..6e5064f --- /dev/null +++ b/packages/cperf/evidence/zh-CN/cp-wide-bool-flags.md @@ -0,0 +1,20 @@ +# cp-wide-bool-flags — 已测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[cp-wide-bool-flags.jsonl](../cp-wide-bool-flags.jsonl)。未进行语料库筛选。2026-10-22 的语料库裁定不涵盖此标识符。 + +## 测量 + +1000000 条记录的副本。宽结构是四个整数(在此构建中为 16 字节)。打包结构是四个无符号字符位字段(在此构建中为 1 字节)。这是对这两种布局的带宽微基准测试,而不是关于每个 ABI 的声明。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 后 | `copy_uchar_bitfields` | 0.067000 | 1000000 | 1 | +| 之前 | `copy_four_ints` | 1.277000 | 1000000 | 19.060 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。输入的变化、冷路径或其他 ABI 都可能抹去差异。在进行任何默认值更改之前,仍然需要进行语料库分流。 diff --git a/packages/cperf/zh-CN/CONTRIBUTING.md b/packages/cperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/cperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/cperf/zh-CN/README.md b/packages/cperf/zh-CN/README.md new file mode 100644 index 0000000..cc91a5f --- /dev/null +++ b/packages/cperf/zh-CN/README.md @@ -0,0 +1,49 @@ +# cperf + +C11/C17 性能笔记、小示例,以及用 **C11** 编写的 `cperf-lint`。在相同模式适用的地方,会出现简要的 C++ 备注。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。严重性预设。指南 v0。墙钟证据门 `cp-alloc-hot`、`cp-realloc-grow-one` 和 `cp-printf-hot`;`cp-strlen-in-cond`(提示)和 `cp-strcat-hot`(关闭)尚无。`cp-sprintf-hot`、`cp-fopen-hot`、`cp-gettime-hot` 和 `cp-wide-bool-flags` 是带 JSONL 的提示,并且未进行语料库分流。`cp-regex-hot` 是带 JSONL 的提示(约 9.7×),并且未进行语料库分流。语料库 r1–r3 仍为 lint dogfood。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/c-performance-guide.md) — 来自 `docs/guide-src/` +- [`cperf-lint`](../cperf-lint/README.md) — **C11** 检查器 +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +竞技场/池,容量翻倍,strlen 条件内(cp-strlen-in-cond),热点路径 stdio(cp-printf-hot),循环中的 strcat(cp-strcat-hot),格式化写入,文件打开,循环中的时钟采样,以及真实的测量记录。没有本地基准或引用的说法仍是暂定;语料计数是 lint 内部测试用的。 + +## 构建示例 + +```sh +cc -std=c17 -O2 -o /tmp/m01 examples/m01_arena.c && /tmp/m01 +cc -std=c17 -O2 -o /tmp/p01 examples/p01_soa.c && /tmp/p01 +``` + +## 运行 lint + +```sh +cd packages/cperf/cperf-lint +make +./cperf-lint path/to/sources --preset app # alloc/realloc=error; strlen, later hints, and cp-regex-hot=hint; printf/strcat=off +./cperf-lint path/to/sources --preset all # every rule error +./cperf-lint path/to/sources --severity cp-printf-hot=off +make self-lint # dogfood: lint src/ + include/, must be clean +make test # fixtures + severity smoke + self-lint +``` + +从包根目录:`bash scripts/smoke-lint.sh`。 + +当存在**错误**级别的发现时,退出代码为 `1`(提示会打印,但除非使用 `--strict-hints`,否则退出 `0`)。使用/IO 错误时,退出代码为 `2`。 + +## 布局 + +```text +docs/guide-src/ chapter sources (00_head … 04_hot_calls, 99_tail) +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ wall-clock gates: cp-alloc-hot, cp-realloc-grow-one, cp-printf-hot (*.md + *.jsonl) +examples/ tiny C demos + bench/ harnesses +cperf-lint/ C11 checker (src/, include/, Makefile) + fixtures +scripts/ regen-evidence-*.sh, run-static-vs-o2.sh +``` diff --git a/packages/cppperf/docs/cpp-performance-guide.md b/packages/cppperf/docs/cpp-performance-guide.md index 53bb494..563bbe3 100644 --- a/packages/cppperf/docs/cpp-performance-guide.md +++ b/packages/cppperf/docs/cpp-performance-guide.md @@ -2,34 +2,30 @@ Fewer reallocating containers, quieter hot paths. -> **Scope: C++17.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: C++17.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. > > **cppperf vs cperf:** this kit is dedicated C++. Sibling **cperf** covers C11/C17 (+ light C++). Prefer this package for `std::vector` / `std::string` / iostream smells. -## How to read this - -1. **Measure first** (`perf`, Tracy, `std::chrono`). -2. Treat each chapter as a checklist. -3. libstdc++ and libc++ differ — re-bench. +Profile first (`perf`, Tracy, `std::chrono`). libstdc++ and libc++ differ — re-bench. ## Contents -- [How to read this](#how-to-read-this) - [1. Containers and strings](#1-containers-and-strings) - [2. Hot I/O](#2-hot-io) +- [3. Regex, flush, and the clock](#3-regex-flush-and-the-clock) - [Lint map](#lint-map) ## 1. Containers and strings Repeated `push_back` without `reserve` reallocates when growth copies dominate (large N / many builders). Prefer sizing capacity once when the final size is known. -`std::string` `+=` / `append` of **multi-byte chunks** or a known final size benefits from `reserve`. Single-character `+=` under `-O2` is often already amortized (~1×) — do not treat every `+=` as a speed bug. +`std::string` `+=` / `append` of multi-byte chunks or a known final size benefits from `reserve`. Single-character `+=` under `-O2` is often already amortized (~1×) — do not treat every `+=` as a speed bug. A `const std::string` or `const std::vector` parameter, and those types copied in a range-for, copies on every call or element. Out of line, a 64-byte string parameter was about 15× a const reference, and one `int` in a vector was about 17×. A range-for of 128-byte strings was about 21×, and a range-for of 64-int vectors was about 21×. A 4-character string stayed about 1.01×, an empty vector about 1.2×, and summing every byte of a 4096-byte string about 1.3×. The lint leaves non-const by-value parameters alone, because that is also how a function takes ownership. Walking the packed bool specialization (`vector`) was about 12× a `vector` over 2,000,000 elements. Indexed reads were about 4.2× and reserved `push_back` about 6.9×, under the bar. Eight million elements occupied 1,000,000 bytes in the packed container and 8,000,000 bytes in the byte container. The hint is the walk. It is not a reason to pack other bool fields, and keeping the packed layout is reasonable when the smaller size is the point. -Companion lint: **cx-const-by-value**, **cx-vector-bool** (default **hint**; measure before escalating). `push_back` of 500000 ints was about 2.0× without `reserve` on two runs, and a one-character `string::push_back` was slower with `reserve` (about 0.82× and 0.79×). CX001 stays retired: one matcher cannot tell those apart. The 2026-10-22 corpus verdict does not cover `cx-const-by-value` or `cx-vector-bool`. +Lint: `cx-const-by-value`, `cx-vector-bool` (default hint; measure before escalating). `push_back` of 500000 ints was about 2.0× without `reserve` on two runs, and a one-character `string::push_back` was slower with `reserve` (about 0.82× and 0.79×). CX001 stays retired: one matcher cannot tell those apart. The 2026-10-22 corpus verdict does not cover `cx-const-by-value` or `cx-vector-bool`. ## 2. Hot I/O @@ -39,22 +35,23 @@ No lint rule covers this smell (CX003 was retired). ## 3. Regex, flush, and the clock -Constructing `std::regex` inside a loop compiles every iteration. On a short haystack that was about 7.7× versus constructing it once. `std::endl` on an external stream flushes every insertion (about 23× versus a newline and one flush). The same manipulator on a string stream was about 0.87×, so that shape is not the finding. Sampling `steady_clock::now` every pass was about 147× versus sampling once; a loop that needs a fresh timestamp has nothing to hoist. Building a string stream every iteration was about 1.2× at `-O2` and that rule was removed. These lints stay **hint**. They are not part of the 2026-10-22 corpus verdict. +Constructing `std::regex` inside a loop compiles every iteration. On a short haystack that was about 7.7× versus constructing it once. `std::endl` on an external stream flushes every insertion (about 23× versus a newline and one flush). The same manipulator on a string stream was about 0.87×, so that shape is not the finding. Sampling `steady_clock::now` every pass was about 147× versus sampling once; a loop that needs a fresh timestamp has nothing to hoist. Building a string stream every iteration was about 1.2× at `-O2` and that rule was removed. These lints stay hint. They are not part of the 2026-10-22 corpus verdict. `ifstream` of one path was about 2025× versus opening once. A path that changes every iteration cannot be hoisted. This does not revive the removed CX003 id. -Companion lint: **cx-regex-ctor-hot**, **cx-endl-hot**, **cx-clock-hot**, **cx-ifstream-hot**. +Lint: `cx-regex-ctor-hot`, `cx-endl-hot`, `cx-clock-hot`, `cx-ifstream-hot` (default hint). + ## Lint map | ID | Smell | Default | |----|-------|---------| | CX001 | removed (jitter) | — | -| cx-regex-ctor-hot | `std::regex` constructed in loop | **hint** (JSONL present; not corpus-triaged) | -| cx-endl-hot | `endl` flush in loop | **hint** (JSONL present; not corpus-triaged) | -| cx-clock-hot | clock sample in a loop | **hint** (JSONL; about 147× vs one sample; not corpus-triaged) | -| cx-ifstream-hot | file stream opened in a loop | **hint** (JSONL; about 2025× on one one-byte file; not corpus-triaged) | -| cx-const-by-value | `const` string or vector copied by value | **hint** (JSONL; about 15–21× when the copy allocates; not corpus-triaged) | -| cx-vector-bool | packed bool element container | **hint** (JSONL; walk about 12× vs a byte vector; not corpus-triaged) | +| cx-regex-ctor-hot | `std::regex` constructed in loop | hint (JSONL present; not corpus-triaged) | +| cx-endl-hot | `endl` flush in loop | hint (JSONL present; not corpus-triaged) | +| cx-clock-hot | clock sample in a loop | hint (JSONL; about 147× vs one sample; not corpus-triaged) | +| cx-ifstream-hot | file stream opened in a loop | hint (JSONL; about 2025× on one one-byte file; not corpus-triaged) | +| cx-const-by-value | `const` string or vector copied by value | hint (JSONL; about 15–21× when the copy allocates; not corpus-triaged) | +| cx-vector-bool | packed bool element container | hint (JSONL; walk about 12× vs a byte vector; not corpus-triaged) | | CX003 | removed (jitter) | — | | CX004 | removed (jitter) | — | diff --git a/packages/cppperf/docs/guide-src/00_head.md b/packages/cppperf/docs/guide-src/00_head.md index 1dce0c5..66b5efb 100644 --- a/packages/cppperf/docs/guide-src/00_head.md +++ b/packages/cppperf/docs/guide-src/00_head.md @@ -2,20 +2,16 @@ Fewer reallocating containers, quieter hot paths. -> **Scope: C++17.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: C++17.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. > > **cppperf vs cperf:** this kit is dedicated C++. Sibling **cperf** covers C11/C17 (+ light C++). Prefer this package for `std::vector` / `std::string` / iostream smells. -## How to read this - -1. **Measure first** (`perf`, Tracy, `std::chrono`). -2. Treat each chapter as a checklist. -3. libstdc++ and libc++ differ — re-bench. +Profile first (`perf`, Tracy, `std::chrono`). libstdc++ and libc++ differ — re-bench. ## Contents -- [How to read this](#how-to-read-this) - [1. Containers and strings](#1-containers-and-strings) - [2. Hot I/O](#2-hot-io) +- [3. Regex, flush, and the clock](#3-regex-flush-and-the-clock) - [Lint map](#lint-map) diff --git a/packages/cppperf/docs/guide-src/01_containers.md b/packages/cppperf/docs/guide-src/01_containers.md index 72c52dc..71071cd 100644 --- a/packages/cppperf/docs/guide-src/01_containers.md +++ b/packages/cppperf/docs/guide-src/01_containers.md @@ -2,11 +2,11 @@ Repeated `push_back` without `reserve` reallocates when growth copies dominate (large N / many builders). Prefer sizing capacity once when the final size is known. -`std::string` `+=` / `append` of **multi-byte chunks** or a known final size benefits from `reserve`. Single-character `+=` under `-O2` is often already amortized (~1×) — do not treat every `+=` as a speed bug. +`std::string` `+=` / `append` of multi-byte chunks or a known final size benefits from `reserve`. Single-character `+=` under `-O2` is often already amortized (~1×) — do not treat every `+=` as a speed bug. A `const std::string` or `const std::vector` parameter, and those types copied in a range-for, copies on every call or element. Out of line, a 64-byte string parameter was about 15× a const reference, and one `int` in a vector was about 17×. A range-for of 128-byte strings was about 21×, and a range-for of 64-int vectors was about 21×. A 4-character string stayed about 1.01×, an empty vector about 1.2×, and summing every byte of a 4096-byte string about 1.3×. The lint leaves non-const by-value parameters alone, because that is also how a function takes ownership. Walking the packed bool specialization (`vector`) was about 12× a `vector` over 2,000,000 elements. Indexed reads were about 4.2× and reserved `push_back` about 6.9×, under the bar. Eight million elements occupied 1,000,000 bytes in the packed container and 8,000,000 bytes in the byte container. The hint is the walk. It is not a reason to pack other bool fields, and keeping the packed layout is reasonable when the smaller size is the point. -Companion lint: **cx-const-by-value**, **cx-vector-bool** (default **hint**; measure before escalating). `push_back` of 500000 ints was about 2.0× without `reserve` on two runs, and a one-character `string::push_back` was slower with `reserve` (about 0.82× and 0.79×). CX001 stays retired: one matcher cannot tell those apart. The 2026-10-22 corpus verdict does not cover `cx-const-by-value` or `cx-vector-bool`. +Lint: `cx-const-by-value`, `cx-vector-bool` (default hint; measure before escalating). `push_back` of 500000 ints was about 2.0× without `reserve` on two runs, and a one-character `string::push_back` was slower with `reserve` (about 0.82× and 0.79×). CX001 stays retired: one matcher cannot tell those apart. The 2026-10-22 corpus verdict does not cover `cx-const-by-value` or `cx-vector-bool`. diff --git a/packages/cppperf/docs/guide-src/03_hot_calls.md b/packages/cppperf/docs/guide-src/03_hot_calls.md index 08efa0f..6d4fef5 100644 --- a/packages/cppperf/docs/guide-src/03_hot_calls.md +++ b/packages/cppperf/docs/guide-src/03_hot_calls.md @@ -1,7 +1,8 @@ ## 3. Regex, flush, and the clock -Constructing `std::regex` inside a loop compiles every iteration. On a short haystack that was about 7.7× versus constructing it once. `std::endl` on an external stream flushes every insertion (about 23× versus a newline and one flush). The same manipulator on a string stream was about 0.87×, so that shape is not the finding. Sampling `steady_clock::now` every pass was about 147× versus sampling once; a loop that needs a fresh timestamp has nothing to hoist. Building a string stream every iteration was about 1.2× at `-O2` and that rule was removed. These lints stay **hint**. They are not part of the 2026-10-22 corpus verdict. +Constructing `std::regex` inside a loop compiles every iteration. On a short haystack that was about 7.7× versus constructing it once. `std::endl` on an external stream flushes every insertion (about 23× versus a newline and one flush). The same manipulator on a string stream was about 0.87×, so that shape is not the finding. Sampling `steady_clock::now` every pass was about 147× versus sampling once; a loop that needs a fresh timestamp has nothing to hoist. Building a string stream every iteration was about 1.2× at `-O2` and that rule was removed. These lints stay hint. They are not part of the 2026-10-22 corpus verdict. `ifstream` of one path was about 2025× versus opening once. A path that changes every iteration cannot be hoisted. This does not revive the removed CX003 id. -Companion lint: **cx-regex-ctor-hot**, **cx-endl-hot**, **cx-clock-hot**, **cx-ifstream-hot**. +Lint: `cx-regex-ctor-hot`, `cx-endl-hot`, `cx-clock-hot`, `cx-ifstream-hot` (default hint). + diff --git a/packages/cppperf/docs/guide-src/99_tail.md b/packages/cppperf/docs/guide-src/99_tail.md index 624ed43..881e7fa 100644 --- a/packages/cppperf/docs/guide-src/99_tail.md +++ b/packages/cppperf/docs/guide-src/99_tail.md @@ -3,12 +3,12 @@ | ID | Smell | Default | |----|-------|---------| | CX001 | removed (jitter) | — | -| cx-regex-ctor-hot | `std::regex` constructed in loop | **hint** (JSONL present; not corpus-triaged) | -| cx-endl-hot | `endl` flush in loop | **hint** (JSONL present; not corpus-triaged) | -| cx-clock-hot | clock sample in a loop | **hint** (JSONL; about 147× vs one sample; not corpus-triaged) | -| cx-ifstream-hot | file stream opened in a loop | **hint** (JSONL; about 2025× on one one-byte file; not corpus-triaged) | -| cx-const-by-value | `const` string or vector copied by value | **hint** (JSONL; about 15–21× when the copy allocates; not corpus-triaged) | -| cx-vector-bool | packed bool element container | **hint** (JSONL; walk about 12× vs a byte vector; not corpus-triaged) | +| cx-regex-ctor-hot | `std::regex` constructed in loop | hint (JSONL present; not corpus-triaged) | +| cx-endl-hot | `endl` flush in loop | hint (JSONL present; not corpus-triaged) | +| cx-clock-hot | clock sample in a loop | hint (JSONL; about 147× vs one sample; not corpus-triaged) | +| cx-ifstream-hot | file stream opened in a loop | hint (JSONL; about 2025× on one one-byte file; not corpus-triaged) | +| cx-const-by-value | `const` string or vector copied by value | hint (JSONL; about 15–21× when the copy allocates; not corpus-triaged) | +| cx-vector-bool | packed bool element container | hint (JSONL; walk about 12× vs a byte vector; not corpus-triaged) | | CX003 | removed (jitter) | — | | CX004 | removed (jitter) | — | diff --git a/packages/cppperf/docs/guide-src/zh-CN/00_head.md b/packages/cppperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..0a95aa8 --- /dev/null +++ b/packages/cppperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,17 @@ +# C++ 性能指南(v0) + +少做会重新分配的容器,让热路径更安静。 + +> **范围:C++17。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 +> +> **cppperf 与 cperf:** 本 kit 专攻 C++。同级 **cperf** 覆盖 C11/C17(+ 轻度 C++)。`std::vector` / `std::string` / iostream 类 smell 优先用本 package。 + +先画像(`perf`、Tracy、`std::chrono`)。libstdc++ 与 libc++ 不同——请重测。 + +## 目录 + +- [1. 容器与字符串](#1-容器与字符串) +- [2. 热 I/O](#2-热-io) +- [3. Regex、flush 与时钟](#3-regexflush-与时钟) +- [Lint 对照](#lint-对照) + diff --git a/packages/cppperf/docs/guide-src/zh-CN/01_containers.md b/packages/cppperf/docs/guide-src/zh-CN/01_containers.md new file mode 100644 index 0000000..92ae0cc --- /dev/null +++ b/packages/cppperf/docs/guide-src/zh-CN/01_containers.md @@ -0,0 +1,11 @@ +## 1. 容器与字符串 + +不 `reserve` 就反复 `push_back`,在扩容拷贝占主导时(大 N / 许多 builder)会重新分配。已知最终大小时,优先一次定容。 + +`std::string` 对多字节块或已知最终大小做 `+=` / `append` 时,`reserve` 有益。`-O2` 下单字符 `+=` 往往已摊销(约 1×)——不要把每次 `+=` 都当成速度 bug。 + +`const std::string` 或 `const std::vector` 形参,以及 range-for 里按值拷贝这些类型,会在每次调用或每个元素上拷贝。跨翻译单元时,64 字节字符串形参相对 const 引用约 15×,vector 里一个 `int` 约 17×。128 字节字符串的 range-for 约 21×,64-int vector 的 range-for 约 21×。4 字符字符串约 1.01×,空 vector 约 1.2×,对 4096 字节字符串按字节求和约 1.3×。lint 不碰非 const 按值形参,因为那也是所有权转移的写法。 + +遍历打包的 bool 特化(`vector`)相对 `vector`,在 2,000,000 元素上约 12×。下标读约 4.2×,带 reserve 的 `push_back` 约 6.9×,未过线。八百万元素在打包容器里占 1,000,000 字节,在字节容器里占 8,000,000 字节。hint 针对遍历。它不是打包其他 bool 字段的理由;当更小体积才是目标时,保留打包布局是合理的。 + +Lint: `cx-const-by-value`, `cx-vector-bool`(默认 hint;升级前请测量). 50 万次 int `push_back` 在两次运行上不 `reserve` 约 2.0×;单字符 `string::push_back` 带 `reserve` 更慢(约 0.82× 与 0.79×)。CX001 保持退役:一个匹配器分不清这些情况。2026-10-22 语料裁决不覆盖 `cx-const-by-value` 或 `cx-vector-bool`。 diff --git a/packages/cppperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/cppperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..463ddb3 --- /dev/null +++ b/packages/cppperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,5 @@ +## 2. 热 I/O + +紧密循环里的 `std::cout << ...` 会刷新并格式化。画像显示有压力时,优先缓冲写入,或把 `{fmt}` / `printf` 放到循环外。 + +没有 lint 覆盖该 smell(CX003 已退役)。 diff --git a/packages/cppperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/cppperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..3cbf993 --- /dev/null +++ b/packages/cppperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,7 @@ +## 3. Regex、flush 与时钟 + +循环里构造 `std::regex` 每轮都编译。短 haystack 上相对构造一次约 7.7×。外部流上的 `std::endl` 每次插入都 flush(相对换行 + 一次 flush 约 23×)。字符串流上的同一操纵符约 0.87×,因此那种形态不是 finding。每轮采 `steady_clock::now` 相对采一次约 147×;需要新鲜时间戳的循环没有可外提的东西。每轮建 string stream 在 `-O2` 下约 1.2×,该规则已移除。这些 lint 保持 hint。它们不属于 2026-10-22 corpus 裁决。 + +对同一路径 `ifstream` 约 2025×(相对打开一次)。每轮都变的路径不能外提。这不会复活已移除的 CX003 id。 + +Lint: `cx-regex-ctor-hot`, `cx-endl-hot`, `cx-clock-hot`, `cx-ifstream-hot`(默认 hint) diff --git a/packages/cppperf/docs/guide-src/zh-CN/99_tail.md b/packages/cppperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..7cf43ff --- /dev/null +++ b/packages/cppperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,19 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| CX001 | 已移除(抖动) | — | +| cx-regex-ctor-hot | 循环内构造 `std::regex` | hint(有 JSONL;未 corpus 分级) | +| cx-endl-hot | 循环内 `endl` flush | hint(有 JSONL;未 corpus 分级) | +| cx-clock-hot | 循环内采时钟 | hint(有 JSONL;相对一次采样约 147×;未 corpus 分级) | +| cx-ifstream-hot | 循环内打开文件流 | hint(有 JSONL;单字节文件约 2025×;未 corpus 分级) | +| cx-const-by-value | 按值拷贝 `const` string 或 vector | hint(有 JSONL;拷贝会分配时约 15–21×;未 corpus 分级) | +| cx-vector-bool | 打包 bool 元素容器 | hint(有 JSONL;遍历相对字节 vector 约 12×;未 corpus 分级) | +| CX003 | 已移除(抖动) | — | +| CX004 | 已移除(抖动) | — | + +仅在有 `evidence/cx-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。退役的 CX004 表明 `-O2` 可能抹掉效果;若声称速度,请在 `-O0` / sanitizer 下测量。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/cppperf/docs/zh-CN/corpus.md b/packages/cppperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..66b6c4e --- /dev/null +++ b/packages/cppperf/docs/zh-CN/corpus.md @@ -0,0 +1,50 @@ +# cppperf 语料库目标 + +大型树木**不会**被供应,语料库也不会随仓库一起分发:将它克隆到仓库之外的任何本地目录,并将该路径传递给代码检查工具。 +使用以下命令进行代码检查: + +```sh +cd packages/cppperf/cppperf-lint && make +./cppperf-lint / --format json --no-fail +``` + +检查器是 **C++17** (`cppperf-lint`)。活动规则:`cx-regex-ctor-hot`、`cx-endl-hot`、`cx-clock-hot`、`cx-ifstream-hot`、`cx-const-by-value`、`cx-vector-bool`。`cx-string-append-unreserved` 已被移除(约 1.09×)。2026-10-23 添加的规则是提示,并不属于下面的语料库判定。已移除(抖动):CX001/CX003/CX004。 + +## 固定目标 + +克隆于 2026-10-21(亚洲/上海);lint 计数 r1 → r2 → r3(重新设计)。 + +| 树 | 提交 | 大小 | Lint(基线 → r1 → r2 → **r3**) | +|------|--------|------|-------------------------------------| +| **json** | `ef3d350` 浅拷贝 `https://github.com/nlohmann/json.git` | 91M | **4 → 0 → 0 → 0** | +| **fmt** | `0fc3163` 浅克隆 `https://github.com/fmtlib/fmt.git` | 4.5M | **49 → 11 → 3 → 0** | +| **spdlog** | `76d4580` 浅拷贝 `https://github.com/gabime/spdlog.git` | 2.3M | **25 → 4 → 1 → 3** (CX004) | + +路径跳过:`test/`、`tests/`、`Tests/`、`third_party/`、`thirdparty/`、`benchmarks/`、`benchmark/`、`bench/`、`examples/`、`example/`、`docs/`、`gtest/`、`gmock/`、`doctest/`、`bundled/`,以及现有的 `.git` / `vendor` / `build`。 + +## G3 TP/FP 分诊 (2026-10-22 亚洲/上海) + +| 阶段 | 样本数量 | 真阳性 | 假阳性 | 正确成语 | 假阳性+文档+跳过 | 语料库准备好? | +|-------|----------|----|----|----------|-------------|----------------| +| r1 | 15 | 0% | 20% | 80% | 20% | **no** | +| r2 | 4 | 0% | 0% | 100% | 0% | **no** (OK-idiom / no TP) | +| **r3** (CX004) | **3** | **100%** | 0% | 0% | **0%** | **yes** | + +r2→r3:将 CX001–CX003 降级 → **关闭**;添加 **CX004**(提示)。spdlog 1→**3** 真实 TP。严重性:CX004 **提示**(无 G4 JSONL)。CX001、CX003 和 CX004 在同一天晚些时候在抖动控制下被淘汰(参见 [rule-ids.md](rule-ids.md));上面的表格是历史记录。 + +## 库存 + +```text +/ + json/ + fmt/ + spdlog/ +``` + +不要将语料库来源提交到 `packages/cppperf`。 + +## 2026-10-23 — 新提示规则,未分类 + +下面的 slug 是 **提示**。它们尚未经过 G2/G3。2026-10-22 可用于语料库的裁决不涵盖它们。 + +- `cx-clock-hot` 有 JSONL(约 147× 与仅采样一次相比)并保持提示。 diff --git a/packages/cppperf/docs/zh-CN/cpp-performance-guide.md b/packages/cppperf/docs/zh-CN/cpp-performance-guide.md new file mode 100644 index 0000000..0fdc090 --- /dev/null +++ b/packages/cppperf/docs/zh-CN/cpp-performance-guide.md @@ -0,0 +1,59 @@ +# C++ 性能指南(v0) + +少做会重新分配的容器,让热路径更安静。 + +> **范围:C++17。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 +> +> **cppperf 与 cperf:** 本 kit 专攻 C++。同级 **cperf** 覆盖 C11/C17(+ 轻度 C++)。`std::vector` / `std::string` / iostream 类 smell 优先用本 package。 + +先画像(`perf`、Tracy、`std::chrono`)。libstdc++ 与 libc++ 不同——请重测。 + +## 目录 + +- [1. 容器与字符串](#1-容器与字符串) +- [2. 热 I/O](#2-热-io) +- [3. Regex、flush 与时钟](#3-regexflush-与时钟) +- [Lint 对照](#lint-对照) + +## 1. 容器与字符串 + +不 `reserve` 就反复 `push_back`,在扩容拷贝占主导时(大 N / 许多 builder)会重新分配。已知最终大小时,优先一次定容。 + +`std::string` 对多字节块或已知最终大小做 `+=` / `append` 时,`reserve` 有益。`-O2` 下单字符 `+=` 往往已摊销(约 1×)——不要把每次 `+=` 都当成速度 bug。 + +`const std::string` 或 `const std::vector` 形参,以及 range-for 里按值拷贝这些类型,会在每次调用或每个元素上拷贝。跨翻译单元时,64 字节字符串形参相对 const 引用约 15×,vector 里一个 `int` 约 17×。128 字节字符串的 range-for 约 21×,64-int vector 的 range-for 约 21×。4 字符字符串约 1.01×,空 vector 约 1.2×,对 4096 字节字符串按字节求和约 1.3×。lint 不碰非 const 按值形参,因为那也是所有权转移的写法。 + +遍历打包的 bool 特化(`vector`)相对 `vector`,在 2,000,000 元素上约 12×。下标读约 4.2×,带 reserve 的 `push_back` 约 6.9×,未过线。八百万元素在打包容器里占 1,000,000 字节,在字节容器里占 8,000,000 字节。hint 针对遍历。它不是打包其他 bool 字段的理由;当更小体积才是目标时,保留打包布局是合理的。 + +Lint: `cx-const-by-value`, `cx-vector-bool`(默认 hint;升级前请测量). 50 万次 int `push_back` 在两次运行上不 `reserve` 约 2.0×;单字符 `string::push_back` 带 `reserve` 更慢(约 0.82× 与 0.79×)。CX001 保持退役:一个匹配器分不清这些情况。2026-10-22 语料裁决不覆盖 `cx-const-by-value` 或 `cx-vector-bool`。 +## 2. 热 I/O + +紧密循环里的 `std::cout << ...` 会刷新并格式化。画像显示有压力时,优先缓冲写入,或把 `{fmt}` / `printf` 放到循环外。 + +没有 lint 覆盖该 smell(CX003 已退役)。 +## 3. Regex、flush 与时钟 + +循环里构造 `std::regex` 每轮都编译。短 haystack 上相对构造一次约 7.7×。外部流上的 `std::endl` 每次插入都 flush(相对换行 + 一次 flush 约 23×)。字符串流上的同一操纵符约 0.87×,因此那种形态不是 finding。每轮采 `steady_clock::now` 相对采一次约 147×;需要新鲜时间戳的循环没有可外提的东西。每轮建 string stream 在 `-O2` 下约 1.2×,该规则已移除。这些 lint 保持 hint。它们不属于 2026-10-22 corpus 裁决。 + +对同一路径 `ifstream` 约 2025×(相对打开一次)。每轮都变的路径不能外提。这不会复活已移除的 CX003 id。 + +Lint: `cx-regex-ctor-hot`, `cx-endl-hot`, `cx-clock-hot`, `cx-ifstream-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| CX001 | 已移除(抖动) | — | +| cx-regex-ctor-hot | 循环内构造 `std::regex` | hint(有 JSONL;未 corpus 分级) | +| cx-endl-hot | 循环内 `endl` flush | hint(有 JSONL;未 corpus 分级) | +| cx-clock-hot | 循环内采时钟 | hint(有 JSONL;相对一次采样约 147×;未 corpus 分级) | +| cx-ifstream-hot | 循环内打开文件流 | hint(有 JSONL;单字节文件约 2025×;未 corpus 分级) | +| cx-const-by-value | 按值拷贝 `const` string 或 vector | hint(有 JSONL;拷贝会分配时约 15–21×;未 corpus 分级) | +| cx-vector-bool | 打包 bool 元素容器 | hint(有 JSONL;遍历相对字节 vector 约 12×;未 corpus 分级) | +| CX003 | 已移除(抖动) | — | +| CX004 | 已移除(抖动) | — | + +仅在有 `evidence/cx-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。退役的 CX004 表明 `-O2` 可能抹掉效果;若声称速度,请在 `-O0` / sanitizer 下测量。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/cppperf/docs/zh-CN/evidence-policy.md b/packages/cppperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..76ef635 --- /dev/null +++ b/packages/cppperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,28 @@ +# cppperf 证据政策 + +## 门槛 + +只有当以下两者都存在于 `packages/cppperf/evidence/` 下时,规则才可被视为 **`error`**: + +1. `.md` — 声明,如何测量,工具链版本,限制 +2. `.jsonl` — ≥1 条前/后行,包含 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts` + +否则规则保持 **`hint`**。 + +## 当前映射 + +截至2026-10-23。2026-10-22的语料库裁决不包括在该日期之后添加的 slug。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| cx-regex-ctor-hot | **提示** | 已测量(`cx-regex-ctor-hot.md` + `cx-regex-ctor-hot.jsonl`);保持提示 | 在循环中构造的 `std::regex`;未经过语料排查 | +| cx-endl-hot | **提示** | 已测量(`cx-endl-hot.md` + `cx-endl-hot.jsonl`);保持提示 | 循环中的 `endl` 刷新;未进行语料库筛选 | +| cx-clock-hot | **提示** | 已测量 (`cx-clock-hot.md` + `cx-clock-hot.jsonl`); 保持提示 | 循环中的时钟采样;约 147 倍于单次采样;未经过语料库筛选 | +| cx-ifstream-hot | **提示** | 测量(`cx-ifstream-hot.md` + `cx-ifstream-hot.jsonl`);保持提示 | 循环中打开的 `ifstream`;仅相同路径;未经过语料库筛选 | +| cx-const-by-value | **提示** | 测量值(`cx-const-by-value.md` + `cx-const-by-value.jsonl`);保持提示 | 按值传递的 `const` 字符串或向量(参数或范围 for);当复制分配时大约增加 15–21 倍;短字符串和完全扫描时保持接近 1×;未经过语料库分类 | +| cx-vector-bool | **提示** | 测量过的 (`cx-vector-bool.md` + `cx-vector-bool.jsonl`); 保留提示 | 压缩布尔容器;迭代器遍历约为字节向量的 12 倍;索引约为 4.2 倍,推入约为 6.9 倍;字节数更小;未进行语料库筛选 | +| CX001 | **已移除** | 墓碑 | 见 `evidence/_removed/` | +| CX003 | **已移除** | 墓碑 | 见 `evidence/_removed/` | +| CX004 | **已移除** | 墓碑 | 见 `evidence/_removed/` | + +活动 slug 计数:**6**(`cx-regex-ctor-hot`、`cx-endl-hot`、`cx-clock-hot`、`cx-ifstream-hot`、`cx-const-by-value`、`cx-vector-bool`)。`cx-string-append-unreserved` 在记录的 `-O2` 运行维持约 1.09× 后被移除。测量后的 JSONL 保持 **提示**。`cx-sstream-in-loop` 在公平的 `-O2` 运行维持约 1.2× 后被移除。抖动移除:CX001/CX003/CX004。仅测量 JSONL 并不会将规则升级超过 **提示**。一个未知的 slug 保持 **提示**。 diff --git a/packages/cppperf/docs/zh-CN/rule-ids.md b/packages/cppperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..a35895c --- /dev/null +++ b/packages/cppperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,35 @@ +# cppperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `cx-regex-ctor-hot` | +| `cx-endl-hot` | +| `cx-clock-hot` | +| `cx-ifstream-hot` | +| `cx-const-by-value` | +| `cx-vector-bool` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `cx-clock-in-loop` | `cx-clock-hot` | +| `cx-endl-in-loop` | `cx-endl-hot` | +| `cx-ifstream-in-loop` | `cx-ifstream-hot` | +| `cx-regex-ctor-in-loop` | `cx-regex-ctor-hot` | + +## Removed + +- `cx-string-append-unreserved` — about 1.09× at `-O2`; see `evidence/_removed/` + +## Removed (no slug) + +- `CX001` — see `evidence/_removed/` +- `CX003` — see `evidence/_removed/` +- `CX004` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/cppperf/evidence/zh-CN/README.md b/packages/cppperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..746026f --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# cppperf 证据 + +更新于 **2026-10-22** 亚洲/上海。 + +`cx-string-append-unreserved` 已被移除(约 1.09×);旧的 JSONL 文件位于 `_removed/` 下。`cx-const-by-value` 和 `cx-vector-bool` 于 2026-10-23 测量,保持 **hint**。2026-10-22 的语料库判定不涵盖这两个标识符。默认保持 **hint**;不升级为 `error`。已退役的规则保留墓碑及其旧的 JSONL 文件在 `_removed/` 下。 + +请参阅 [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录下的 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/cppperf/evidence/zh-CN/cx-clock-hot.md b/packages/cppperf/evidence/zh-CN/cx-clock-hot.md new file mode 100644 index 0000000..0b3a35a --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-clock-hot.md @@ -0,0 +1,17 @@ +# cx-clock-hot — 已测量,仍有暗示 + +## 状态 +**提示**。实时时钟 JSONL:[cx-clock-hot.jsonl](../cx-clock-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +在每次迭代之前读取 `steady_clock::now`。之后只读取一次,并且不会在循环内采样。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 样本一次 | 0.013 | 100000 | 1 | +| 之前 | 每次迭代采样 | 1.974 | 100000 | 147.117 | + +每次运行都需要新时间戳的代码没有可以提升的内容。默认保持提示。 + +## 解除阻塞 +不要仅从此文件将默认值设置为 `error`。另一个输入或冷路径可能会消除差异。在进行任何默认更改之前,仍然需要进行语料库分类处理。 diff --git a/packages/cppperf/evidence/zh-CN/cx-const-by-value.md b/packages/cppperf/evidence/zh-CN/cx-const-by-value.md new file mode 100644 index 0000000..c938412 --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-const-by-value.md @@ -0,0 +1,41 @@ +# cx-const-by-value — 已测量,仍然提示 + +## 状态 +**提示**. 实时时钟 JSONL: [cx-const-by-value.jsonl](../cx-const-by-value.jsonl)。未进行语料库分流。2026-10-22 的语料库裁定不涵盖此标识。 + +## 测量 +`clang++ -std=c++17 -O2`(Apple clang 版本 21.0.0 (clang-2100.1.1.101))。参数情况调用另一个翻译单元中的函数,因此复制是 ABI 所生成的。每一方返回 `size + first byte`(或 `size + first int`)。测试程序在经过 3 次预热后,将返回值在 9 个计时轮中求和。校验和匹配。之后的时间保持在 0 以上。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在向量中的一个 `int` 之后, 常量引用 | 0.148417 | 200000 | 1 | +| 在向量中之前的一个 `int`,按值常量 | 2.471000 | 200000 | 16.649 | +| 之后 | 64字节字符串,常量引用 | 0.039792 | 40000 | 1 | +| 之前 | 64字节字符串,按值常量 | 0.610709 | 40000 | 15.348 | +| 在128字节字符串的range-for之后,常量引用 | 0.016042 | 20000 | 1 | +| 前 | 128 字节字符串的 range-for,按值常量 | 0.329708 | 20000 | 20.553 | +| 在 64 位整型向量的 range-for 后,常量引用 | 0.005708 | 8000 | 1 | +| 之前 | 64 位整型向量的 range-for,按值常量传递 | 0.122083 | 8000 | 21.388 | + +相同形状,也高于基准线,在 JSONL 中未重复:一个 23 字节的字符串参数为 16.728×(1.700666 / 0.101667 毫秒,100000 次操作),32 字节 17.504×(1.423625 / 0.081333 毫秒,80000 次),256 字节 22.681×(0.462125 / 0.020375 毫秒,20000 次),4096 字节 59.952×(1.206541 / 0.020125 毫秒,20000 次)。一个 4 整数向量为 16.145×(2.396875 / 0.148458 毫秒,200000 次)。一个 1024 整数向量,访问第一个元素时,为 74.110×(1.120916 / 0.015125 毫秒,20000 次)。 + +同一文件的 4096 字节触碰,加上了一个易变的零,这样引用循环就不会消失,为 115.971×(1.178958 / 0.010166 毫秒,20000 次)。内联并没有消除那个复制。 + +## 限制 +这些都保持在标准以下。扫描器仍会标记拼写错误,因为它无法看到长度或正文: + +| 案例 | 前_ms | 后_ms | 操作 | 比率 | +| --- | ---: | ---: | ---: | ---: | +| 4字符字符串参数 | 0.303834 | 0.301250 | 300000 | 1.009 | +| 空向量参数 | 0.371000 | 0.303334 | 300000 | 1.223 | +| 64字节字符串,对每个字节求和 | 0.356583 | 0.081708 | 20000 | 4.364 | +| 4096 字节字符串,每个字节求和 | 0.980375 | 0.767625 | 4000 | 1.277 | +| 4 字符字符串的 range-for | 0.020292 | 0.010625 | 40000 | 1.910 | +| 1024-整数向量,对每个元素求和 | 0.324125 | 0.151917 | 4000 | 2.134 | + +匹配器是一行:`const std::string` / `const std::vector<...>`(以及 east-const 拼写),其名称后跟 `,` 或 `)`,加上 `for (` 头部中 `:` 之前的相同类型。它会遗漏 `const auto`、下一行的名称、默认参数(`=`)以及嵌套的 `>>`。按值传递的非常量参数会被保留:在一个 4096 字节的字符串上,这个复制是 60.510×(1.235375 / 0.020416 毫秒,20000 次操作),它也是函数获取所有权的方式。局部的 `const std::string copy = ...` 会被保留。 + +默认保持 **hint**。这些比率不是通用加速。 + +## 解除阻塞 +不要仅从这个文件将默认值设置为 `error`。一个短字符串、一个空向量,或一个读取每个元素的主体都可以抹去差异。在任何默认值更改之前,仍然需要对语料库进行初步筛选。 diff --git a/packages/cppperf/evidence/zh-CN/cx-endl-hot.md b/packages/cppperf/evidence/zh-CN/cx-endl-hot.md new file mode 100644 index 0000000..cc750e9 --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-endl-hot.md @@ -0,0 +1,19 @@ +# cx-endl-hot — 测量过的,仍然有一点提示 + +## 状态 +**提示**。挂钟 JSONL:[cx-endl-hot.jsonl](../cx-endl-hot.jsonl)。未进行语料库分流。2026-10-22 的语料库裁决不涵盖此标识。JSONL 中有两个形状。 + +## 测量 +双方都写相同的整数。在绑定到 `/dev/null` 的 `cout` 上,`endl` 会刷新每次插入。在字符串流上,`endl` 不会刷新文件。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在 cout 上换行后,一次刷新 | 1.022 | 20000 | 1 | +| 在 cout 上使用 `endl` 之前 | 23.128 | 20000 | 22.622 | +| 在字符串流上的换行后 | 3.923 | 50000 | 1 | +| 在字符串流上的 `endl` 之前 | 3.397 | 50000 | 0.866 | + +外部流刷新显示了成本。字符串流形状没有显示(约 0.87×)。默认保持提示。 + +## 解除阻塞 +不要仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。在进行任何默认更改之前,仍然需要语料分流。 diff --git a/packages/cppperf/evidence/zh-CN/cx-ifstream-hot.md b/packages/cppperf/evidence/zh-CN/cx-ifstream-hot.md new file mode 100644 index 0000000..7e8ab6e --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-ifstream-hot.md @@ -0,0 +1,17 @@ +# cx-ifstream-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[cx-ifstream-hot.jsonl](../cx-ifstream-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +一个一字节文件的`std::ifstream`,重复2000次,与只打开一次相比。双方都读取一个字节。这不会恢复被移除的CX003。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在…之后 | `open_once` | 0.00725 | 2000 | 1 | +| 在...之前 | `open_each_iter` | 14.6835 | 2000 | 2025.32 | + +该基准重新打开一个单字节文件。每次迭代都变化的路径无法被提升。默认保持 **hint**。这个比例不是通用的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/cppperf/evidence/zh-CN/cx-regex-ctor-hot.md b/packages/cppperf/evidence/zh-CN/cx-regex-ctor-hot.md new file mode 100644 index 0000000..e4955e3 --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-regex-ctor-hot.md @@ -0,0 +1,17 @@ +# cx-regex-ctor-hot — 已测量,仍提示 + +## 状态 +**提示**。实时时钟 JSONL:[cx-regex-ctor-hot.jsonl](../cx-regex-ctor-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +双方都搜索短干草堆 `id-2026`。在构建 `std::regex` 之前每次迭代。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 后 | 构造一次 | 0.305 | 400 | 1 | +| 之前 | 构建每次迭代 | 2.355 | 400 | 7.725 | + +一个长干草堆隐藏了这个构造器(之前一次运行大约是1.55×)。默认保持提示。 + +## 解除阻塞 +不要仅仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。任何默认更改前仍需进行语料库分流处理。 diff --git a/packages/cppperf/evidence/zh-CN/cx-vector-bool.md b/packages/cppperf/evidence/zh-CN/cx-vector-bool.md new file mode 100644 index 0000000..c68c876 --- /dev/null +++ b/packages/cppperf/evidence/zh-CN/cx-vector-bool.md @@ -0,0 +1,24 @@ +# cx-vector-bool — 已测量,仍为提示 + +## 状态 +**提示**。实际时间 JSONL: [cx-vector-bool.jsonl](../cx-vector-bool.jsonl)。未进行语料库分级。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 +`clang++ -std=c++17 -O2`(Apple clang 版本 21.0.0 (clang-2100.1.1.101))。两个容器都包含 2,000,000 个 true 元素。先遍历打包的 bool 容器。后遍历 `vector`。测试程序在 3 次预热后执行 9 轮定时,汇总元素。校验和匹配(18,000,000)。计时结束后保持大于 0。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 字节迭代器遍历 | 0.092833 | 2000000 | 1 | +| 之前 | 打包布尔迭代器遍历 | 1.126333 | 2000000 | 12.133 | + +对相同容器的索引读取为 4.237×(0.393333 / 0.092834 毫秒,2000000 次操作)。向预留容器中 `push_back` 400,000 个值为 6.939×(0.721375 / 0.103958 毫秒)。这两者都保持在标准之下。 + +八百万个元素,在使用这个标准库的分配器进行计数时:packed 容器在一次分配中分配了 1,000,000 字节;byte 容器在一次分配中分配了 8,000,000 字节。packed 容器更小。这个提示是说明情况,并不是让其他 bool 字段进行打包的理由。 + +## 限制 +扫描器会在拼写 `vector`(无论是否加上 `std::`)的行上标记它。它不知道热点使用是遍历(约 12×)、索引(约 4.2×)还是推入(约 6.9×)。当节省字节比遍历更重要时,保留压缩容器是正确的选择。 + +默认保持 **hint**。这个比率不是一般的加速。 + +## 解锁 +不要仅从此文件将默认值设置为 `error`。仅用于索引,或者需要更小布局的程序,可能会使字节容器成为错误的修复。在进行任何默认更改之前,仍然需要进行语料库分类。 diff --git a/packages/cppperf/zh-CN/CONTRIBUTING.md b/packages/cppperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/cppperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/cppperf/zh-CN/README.md b/packages/cppperf/zh-CN/README.md new file mode 100644 index 0000000..f5c5b10 --- /dev/null +++ b/packages/cppperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# cppperf + +> **范围说明:** **cppperf** 专用于 **C++17**。同类 [cperf](../../cperf/) 依旧支持 **C11/C17**(+ 轻量级 C++)。请勿合并这两个工具包。 + +C++17 性能笔记、小例子,以及 `cppperf-lint`。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。针对 2026-10-22 规则集 **语料库就绪**。regex、endl、clock、ifstream、const-by-value 和 vector-bool 提示带有 JSONL。已移除字符串追加和字符串流。删除了三个较旧的规则(tombstone, 从未得到 slug)。Guide v0 骨架。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/cpp-performance-guide.md) — 来自 `docs/guide-src/` +- [`cppperf-lint`](../cppperf-lint/README.md) — 字符串追加、正则表达式、endl 和时钟提示;移除了三个规则(tombstone,从未获得 slug) +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +在进行热 `push_back` 之前预留空间,避免字符串二次增长,在紧密循环中不要使用 `iostream`(建议在外部使用缓冲/`fmt`)。 + +## 运行 lint + +```sh +cd packages/cppperf/cppperf-lint && make +./cppperf-lint path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅在 **错误** 严重级别的发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ measured JSONL; _removed/ tombstones including cx-string-append-unreserved +examples/ tiny demos +cppperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/csharpperf/docs/csharp-performance-guide.md b/packages/csharpperf/docs/csharp-performance-guide.md index ce31c62..c6a61f8 100644 --- a/packages/csharpperf/docs/csharp-performance-guide.md +++ b/packages/csharpperf/docs/csharp-performance-guide.md @@ -2,19 +2,15 @@ Fewer reallocating lists, quieter hot paths. -> **Scope: modern C# (.NET).** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: modern C# (.NET).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (BenchmarkDotNet, `dotnet-trace`). -2. Treat each chapter as a checklist. -3. Runtime / GC settings differ — re-bench. +Profile first (BenchmarkDotNet, `dotnet-trace`). Runtime and GC settings differ — re-bench. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists and strings](#1-lists-and-strings) - [2. Hot I/O](#2-hot-io) +- [3. Materialize and format](#3-materialize-and-format) - [Lint map](#lint-map) ## 1. Lists and strings @@ -23,23 +19,23 @@ Fewer reallocating lists, quieter hot paths. Calling static `Regex.IsMatch` / `Regex.Match` with a pattern string on every iteration looks the pattern up in the static regex cache each time; prefer one reused `Regex` instance (`RegexOptions.Compiled` when the pattern is hot). -Companion lint: **cs-list-add-no-capacity**, **cs-string-plus-hot**, **cs-regex-hot**. +Lint: `cs-list-add-no-capacity`, `cs-string-plus-hot` (default off); `cs-regex-hot` (default hint). ## 2. Hot I/O `Console.Write` / `WriteLine` in a tight loop is expensive. Gate debug output or buffer. -Companion lint: **cs-console-hot**. +Lint: `cs-console-hot` (default off). ## 3. Materialize and format -`ToList` / `ToArray` / `ToDictionary` inside a loop allocates a fresh collection every iteration. `string.Format` inside a loop allocates a new string every iteration. `JsonSerializer.Serialize` / `Deserialize` and `JsonConvert.SerializeObject` / `DeserializeObject` inside a loop encode or decode on every iteration. `cs-json-hot` stays **hint**. The microbench is a fixed payload. It is not part of the 2026-10-22 corpus-ready verdict. +`ToList` / `ToArray` / `ToDictionary` inside a loop allocates a fresh collection every iteration. `string.Format` inside a loop allocates a new string every iteration. `JsonSerializer.Serialize` / `Deserialize` and `JsonConvert.SerializeObject` / `DeserializeObject` inside a loop encode or decode on every iteration. `cs-json-hot` stays hint. The microbench is a fixed payload. It is not part of the 2026-10-22 corpus-ready verdict. `File.ReadAllText` of one path was about 338× on .NET 10 versus reading once. `Stopwatch.GetTimestamp` was about 37× versus one sample. `new Regex` every iteration, with both sides still matching, was about 7× and is the same hint as the static `Regex.IsMatch` rule. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. `GetField` plus `GetValue` every iteration was about 62× versus reading the field. Holding the `FieldInfo` and still calling `GetValue` was about 42× versus the field, so moving only `GetField` outside is a small change. A name that changes every iteration cannot be turned into a direct field. This is the same family as Go `FieldByName`. -Companion lint: **cs-linq-materialize-hot**, **cs-format-hot**, **cs-json-hot**, **cs-regex-hot**, **cs-file-read-hot**, **cs-clock-hot**, **cs-field-by-name-hot**. +Lint: `cs-linq-materialize-hot`, `cs-format-hot` (default hint; pending measure); `cs-json-hot`, `cs-regex-hot`, `cs-file-read-hot`, `cs-clock-hot`, `cs-field-by-name-hot` (default hint). ## Lint map diff --git a/packages/csharpperf/docs/guide-src/00_head.md b/packages/csharpperf/docs/guide-src/00_head.md index eb324b8..ad5fde7 100644 --- a/packages/csharpperf/docs/guide-src/00_head.md +++ b/packages/csharpperf/docs/guide-src/00_head.md @@ -2,18 +2,14 @@ Fewer reallocating lists, quieter hot paths. -> **Scope: modern C# (.NET).** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: modern C# (.NET).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (BenchmarkDotNet, `dotnet-trace`). -2. Treat each chapter as a checklist. -3. Runtime / GC settings differ — re-bench. +Profile first (BenchmarkDotNet, `dotnet-trace`). Runtime and GC settings differ — re-bench. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists and strings](#1-lists-and-strings) - [2. Hot I/O](#2-hot-io) +- [3. Materialize and format](#3-materialize-and-format) - [Lint map](#lint-map) diff --git a/packages/csharpperf/docs/guide-src/01_builders.md b/packages/csharpperf/docs/guide-src/01_builders.md index 13a1571..6efb34f 100644 --- a/packages/csharpperf/docs/guide-src/01_builders.md +++ b/packages/csharpperf/docs/guide-src/01_builders.md @@ -4,5 +4,5 @@ Calling static `Regex.IsMatch` / `Regex.Match` with a pattern string on every iteration looks the pattern up in the static regex cache each time; prefer one reused `Regex` instance (`RegexOptions.Compiled` when the pattern is hot). -Companion lint: **cs-list-add-no-capacity**, **cs-string-plus-hot**, **cs-regex-hot**. +Lint: `cs-list-add-no-capacity`, `cs-string-plus-hot` (default off); `cs-regex-hot` (default hint). diff --git a/packages/csharpperf/docs/guide-src/02_hot_io.md b/packages/csharpperf/docs/guide-src/02_hot_io.md index 89258f3..8634783 100644 --- a/packages/csharpperf/docs/guide-src/02_hot_io.md +++ b/packages/csharpperf/docs/guide-src/02_hot_io.md @@ -2,5 +2,5 @@ `Console.Write` / `WriteLine` in a tight loop is expensive. Gate debug output or buffer. -Companion lint: **cs-console-hot**. +Lint: `cs-console-hot` (default off). diff --git a/packages/csharpperf/docs/guide-src/03_hot_calls.md b/packages/csharpperf/docs/guide-src/03_hot_calls.md index 5b2b6b3..d48257f 100644 --- a/packages/csharpperf/docs/guide-src/03_hot_calls.md +++ b/packages/csharpperf/docs/guide-src/03_hot_calls.md @@ -1,10 +1,10 @@ ## 3. Materialize and format -`ToList` / `ToArray` / `ToDictionary` inside a loop allocates a fresh collection every iteration. `string.Format` inside a loop allocates a new string every iteration. `JsonSerializer.Serialize` / `Deserialize` and `JsonConvert.SerializeObject` / `DeserializeObject` inside a loop encode or decode on every iteration. `cs-json-hot` stays **hint**. The microbench is a fixed payload. It is not part of the 2026-10-22 corpus-ready verdict. +`ToList` / `ToArray` / `ToDictionary` inside a loop allocates a fresh collection every iteration. `string.Format` inside a loop allocates a new string every iteration. `JsonSerializer.Serialize` / `Deserialize` and `JsonConvert.SerializeObject` / `DeserializeObject` inside a loop encode or decode on every iteration. `cs-json-hot` stays hint. The microbench is a fixed payload. It is not part of the 2026-10-22 corpus-ready verdict. `File.ReadAllText` of one path was about 338× on .NET 10 versus reading once. `Stopwatch.GetTimestamp` was about 37× versus one sample. `new Regex` every iteration, with both sides still matching, was about 7× and is the same hint as the static `Regex.IsMatch` rule. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. `GetField` plus `GetValue` every iteration was about 62× versus reading the field. Holding the `FieldInfo` and still calling `GetValue` was about 42× versus the field, so moving only `GetField` outside is a small change. A name that changes every iteration cannot be turned into a direct field. This is the same family as Go `FieldByName`. -Companion lint: **cs-linq-materialize-hot**, **cs-format-hot**, **cs-json-hot**, **cs-regex-hot**, **cs-file-read-hot**, **cs-clock-hot**, **cs-field-by-name-hot**. +Lint: `cs-linq-materialize-hot`, `cs-format-hot` (default hint; pending measure); `cs-json-hot`, `cs-regex-hot`, `cs-file-read-hot`, `cs-clock-hot`, `cs-field-by-name-hot` (default hint). diff --git a/packages/csharpperf/docs/guide-src/zh-CN/00_head.md b/packages/csharpperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..1d68d72 --- /dev/null +++ b/packages/csharpperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# C# 性能指南(v0) + +少做会重新分配的 list,让热路径更安静。 + +> **范围:现代 C#(.NET)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(BenchmarkDotNet、`dotnet-trace`)。运行时与 GC 设置不同——请重测。 + +## 目录 + +- [1. List 与字符串](#1-list-与字符串) +- [2. 热 I/O](#2-热-io) +- [3. 物化与格式化](#3-物化与格式化) +- [Lint 对照](#lint-对照) + diff --git a/packages/csharpperf/docs/guide-src/zh-CN/01_builders.md b/packages/csharpperf/docs/guide-src/zh-CN/01_builders.md new file mode 100644 index 0000000..4f0e05c --- /dev/null +++ b/packages/csharpperf/docs/guide-src/zh-CN/01_builders.md @@ -0,0 +1,7 @@ +## 1. List 与字符串 + +循环里的 `List.Add` 若不设 `Capacity` 会反复扩容。优先一次定容。紧密循环里的字符串 `+=` 会分配;优先 `StringBuilder`。 + +每轮调用静态 `Regex.IsMatch` / `Regex.Match` 并传入模式字符串,都会在静态正则缓存里查找;优先复用一个 `Regex` 实例(热模式可用 `RegexOptions.Compiled`)。 + +Lint: `cs-list-add-no-capacity`, `cs-string-plus-hot`(默认 off); `cs-regex-hot`(默认 hint) diff --git a/packages/csharpperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/csharpperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..2882017 --- /dev/null +++ b/packages/csharpperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,5 @@ +## 2. 热 I/O + +紧密循环里的 `Console.Write` / `WriteLine` 很贵。门控调试输出或做缓冲。 + +Lint: `cs-console-hot`(默认 off) diff --git a/packages/csharpperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/csharpperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..778e8e4 --- /dev/null +++ b/packages/csharpperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,9 @@ +## 3. 物化与格式化 + +循环里的 `ToList` / `ToArray` / `ToDictionary` 每轮分配新集合。循环里的 `string.Format` 每轮分配新字符串。循环里的 `JsonSerializer.Serialize` / `Deserialize` 与 `JsonConvert.SerializeObject` / `DeserializeObject` 每轮编解码。`cs-json-hot` 保持 hint。微基准是固定 payload。它不属于 2026-10-22 corpus-ready 裁决。 + +对同一路径 `File.ReadAllText` 在 .NET 10 上约 338×(相对读一次)。`Stopwatch.GetTimestamp` 约 37×(相对采一次样)。每轮 `new Regex`(两侧仍匹配)约 7×,与静态 `Regex.IsMatch` 同为 hint。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +每轮 `GetField` 再 `GetValue` 相对直接读字段约 62×。持有 `FieldInfo` 仍调用 `GetValue` 约 42×,因此只把 `GetField` 挪到循环外改动很小。每轮都变的名字不能变成直接字段。这与 Go 的 `FieldByName` 同族。 + +Lint: `cs-linq-materialize-hot`, `cs-format-hot`(默认 hint; pending measure); `cs-json-hot`, `cs-regex-hot`, `cs-file-read-hot`, `cs-clock-hot`, `cs-field-by-name-hot`(默认 hint) diff --git a/packages/csharpperf/docs/guide-src/zh-CN/99_tail.md b/packages/csharpperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..ddadf87 --- /dev/null +++ b/packages/csharpperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,17 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| cs-list-add-no-capacity | 循环内无 Capacity 的 `List.Add` | off | +| cs-string-plus-hot | 循环内字符串 `+=` | off | +| cs-console-hot | 循环内 `Console.Write*` | off | +| cs-regex-hot | 循环内静态 `Regex.IsMatch/Match` | hint | +| cs-linq-materialize-hot | 循环内 `ToList` / `ToArray` / `ToDictionary` | hint(待测) | +| cs-format-hot | 循环内 `string.Format` | hint(待测) | +| cs-json-hot | 循环内 JSON 编解码 | hint(有 JSONL,仅固定 payload) | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/csharpperf/docs/zh-CN/corpus.md b/packages/csharpperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..6ebdbda --- /dev/null +++ b/packages/csharpperf/docs/zh-CN/corpus.md @@ -0,0 +1,36 @@ +# csharpperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任意本地目录,并将该路径传递给代码检查器。 + +```sh +cd packages/csharpperf/csharpperf-lint && dotnet run -c Release -- / --format json --no-fail +``` + +检查器:`csharpperf-lint`。规则 cs-list-add-no-capacity–cs-regex-hot。 + +## 固定目标 + +克隆的树,r1/after → r2 → r3(重新设计)。提交指针未被记录;重新克隆时请记录提交。 + +| 树 | 钉 | 绒毛 (之后 → r2 → **r3**) | +|------|-----|------------------------------| +| **Newtonsoft.Json** | 未记录 | **38 → 33 → 3**(cs-regex-hot)| +| **serilog** | 未记录 | **3 → 1 → 0** | +| **Humanizer** | 未记录 | **24 → 3 → 0** | + +## G3 TP/FP 分诊(2026-10-22 亚洲/上海) + +| 阶段 | n 样本 | 真阳性 | 假阳性+文档+跳过 | 语料库准备好了吗? | +|-------|----------|----|-------------|----------------| +| after/r1 | 65 | 0% | 21.5% | **no** | +| r2 | 37 | 0% | 0% | **no** (OK-idiom) | +| **r3** (cs-regex-hot) | **3** | **100%** | **0%** | **是** | + +r2→r3:降级 cs-list-add-no-capacity–cs-console-hot → **关闭**;添加 **cs-regex-hot**(提示)。没有 G4 JSONL。 + +## 2026-10-23 — 新提示规则,未分类 + +这些软体动物是**提示**。它们没有经过 G2/G3。上面的针数总计不包括它们。2026-10-22 可用于语料库的裁定不涵盖它们。 + +- `cs-linq-materialize-hot`、`cs-format-hot` —— 墙钟时间 JSONL 在磁盘上;默认仍然是提示 +- `cs-json-hot` —— 一个固定负载的 JSONL(大约 1500 倍);可变负载无法提升;仍然是提示 diff --git a/packages/csharpperf/docs/zh-CN/csharp-performance-guide.md b/packages/csharpperf/docs/zh-CN/csharp-performance-guide.md new file mode 100644 index 0000000..15b57e1 --- /dev/null +++ b/packages/csharpperf/docs/zh-CN/csharp-performance-guide.md @@ -0,0 +1,53 @@ +# C# 性能指南(v0) + +少做会重新分配的 list,让热路径更安静。 + +> **范围:现代 C#(.NET)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(BenchmarkDotNet、`dotnet-trace`)。运行时与 GC 设置不同——请重测。 + +## 目录 + +- [1. List 与字符串](#1-list-与字符串) +- [2. 热 I/O](#2-热-io) +- [3. 物化与格式化](#3-物化与格式化) +- [Lint 对照](#lint-对照) + +## 1. List 与字符串 + +循环里的 `List.Add` 若不设 `Capacity` 会反复扩容。优先一次定容。紧密循环里的字符串 `+=` 会分配;优先 `StringBuilder`。 + +每轮调用静态 `Regex.IsMatch` / `Regex.Match` 并传入模式字符串,都会在静态正则缓存里查找;优先复用一个 `Regex` 实例(热模式可用 `RegexOptions.Compiled`)。 + +Lint: `cs-list-add-no-capacity`, `cs-string-plus-hot`(默认 off); `cs-regex-hot`(默认 hint) +## 2. 热 I/O + +紧密循环里的 `Console.Write` / `WriteLine` 很贵。门控调试输出或做缓冲。 + +Lint: `cs-console-hot`(默认 off) +## 3. 物化与格式化 + +循环里的 `ToList` / `ToArray` / `ToDictionary` 每轮分配新集合。循环里的 `string.Format` 每轮分配新字符串。循环里的 `JsonSerializer.Serialize` / `Deserialize` 与 `JsonConvert.SerializeObject` / `DeserializeObject` 每轮编解码。`cs-json-hot` 保持 hint。微基准是固定 payload。它不属于 2026-10-22 corpus-ready 裁决。 + +对同一路径 `File.ReadAllText` 在 .NET 10 上约 338×(相对读一次)。`Stopwatch.GetTimestamp` 约 37×(相对采一次样)。每轮 `new Regex`(两侧仍匹配)约 7×,与静态 `Regex.IsMatch` 同为 hint。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +每轮 `GetField` 再 `GetValue` 相对直接读字段约 62×。持有 `FieldInfo` 仍调用 `GetValue` 约 42×,因此只把 `GetField` 挪到循环外改动很小。每轮都变的名字不能变成直接字段。这与 Go 的 `FieldByName` 同族。 + +Lint: `cs-linq-materialize-hot`, `cs-format-hot`(默认 hint; pending measure); `cs-json-hot`, `cs-regex-hot`, `cs-file-read-hot`, `cs-clock-hot`, `cs-field-by-name-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| cs-list-add-no-capacity | 循环内无 Capacity 的 `List.Add` | off | +| cs-string-plus-hot | 循环内字符串 `+=` | off | +| cs-console-hot | 循环内 `Console.Write*` | off | +| cs-regex-hot | 循环内静态 `Regex.IsMatch/Match` | hint | +| cs-linq-materialize-hot | 循环内 `ToList` / `ToArray` / `ToDictionary` | hint(待测) | +| cs-format-hot | 循环内 `string.Format` | hint(待测) | +| cs-json-hot | 循环内 JSON 编解码 | hint(有 JSONL,仅固定 payload) | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/csharpperf/docs/zh-CN/evidence-policy.md b/packages/csharpperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..169e107 --- /dev/null +++ b/packages/csharpperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,22 @@ +# csharpperf 证据政策 + +## 门槛 + +一条规则只有在有 `evidence/.md` + `.jsonl` **以及**明确的政策决定的情况下,才可被视为 **`error`**。仅测量的 JSONL 并不会自动升级。Batch B(2026-10-22)保持了软件包默认设置。 + +## 当前映射 + +截至2026-10-22(B批次 G4)。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| cs-list-add-no-capacity | 关闭 | **是** ~1.31× | 列表.Add 无容量 | +| cs-string-plus-hot | 关闭 | **是** ~3758× | string += | +| cs-console-hot | 关闭 | **是** ~6× | 循环中的控制台 | +| cs-regex-hot | 提示 | **是** ~2.6× | Regex.IsMatch | +| cs-linq-materialize-hot | 提示 | 已测量(`cs-linq-materialize-hot.md` + `cs-linq-materialize-hot.jsonl`);保持提示 | 循环中的 `ToList` / `ToArray` / `ToDictionary`;未经过语料库筛选 | +| cs-format-hot | 提示 | 已测量(`cs-format-hot.md` + `cs-format-hot.jsonl`);保持提示 | 循环中的 `string.Format`;未进行语料库筛选 | +| cs-json-hot | 提示 | 测量(`cs-json-hot.md` + `cs-json-hot.jsonl`);保留提示 | 循环中的 JSON 编码/解码;仅固定有效负载;非语料库筛选 | +| cs-file-read-hot | 提示 | 测量 (`cs-file-read-hot.md` + `cs-file-read-hot.jsonl`); 保持提示 | 循环中使用 `File.ReadAllText` / `File.Open`; 仅相同路径; 未经过语料库筛选 | +| cs-clock-hot | 提示 | 测量(`cs-clock-hot.md` + `cs-clock-hot.jsonl`);保持提示 | 循环中的 `Stopwatch.GetTimestamp`;未进行语料库分类 | +| cs-field-by-name-hot | 提示 | 测量 (`cs-field-by-name-hot.md` + `cs-field-by-name-hot.jsonl`); 保持提示 | 循环中的 `GetField` / `GetProperty`; 未经过语料库筛选 | diff --git a/packages/csharpperf/docs/zh-CN/rule-ids.md b/packages/csharpperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..6237adc --- /dev/null +++ b/packages/csharpperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,34 @@ +# csharpperf 规则 ID(别名) + +规范 ID 仅为 **slug**。旧地图中的数字是历史性的,**不**被接受。 + +| Slug | +|------| +| `cs-list-add-no-capacity` | +| `cs-string-plus-hot` | +| `cs-console-hot` | +| `cs-regex-hot` | +| `cs-linq-materialize-hot` | +| `cs-format-hot` | +| `cs-json-hot` | +| `cs-file-read-hot` | +| `cs-clock-hot` | +| `按名称热字段` | + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或相应的工具包选项)。旧的 `*-in-loop` ID 会误导工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `cs-clock-in-loop` | `cs-clock-hot` | +| `cs-console-in-loop` | `cs-console-hot` | +| `循环中的按名称字段` | `热门按名称字段` | +| `cs-file-read-in-loop` | `cs-file-read-hot` | +| `cs-循环中格式` | `cs-格式热点` | +| `cs-json-in-loop` | `cs-json-hot` | +| `cs-linq-在循环中实现` | `cs-linq-实现热数据` | +| `cs-regex-in-loop` | `cs-regex-hot` | +| `cs-string-plus-in-loop` | `cs-string-plus-hot` | diff --git a/packages/csharpperf/evidence/zh-CN/README.md b/packages/csharpperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..eb4e820 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# csharpperf 证据 + +更新于 **2026-10-22** 亚洲/上海(B批次 G4 填充)。 + +在 .NET 10.0.13 上重新运行了 Stock harness 和其余的 harness。默认设置未更改。`cs-list-add-no-capacity` 大约是 1.31× 并保持关闭。`cs-json-hot` 是一个固定的负载并保持提示。 + +请参阅每条规则的 `*.md` + `*.jsonl`(或被阻止的存根)。政策:[../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。根矩阵:[../../../docs/rule-regression.md](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-clock-hot.md b/packages/csharpperf/evidence/zh-CN/cs-clock-hot.md new file mode 100644 index 0000000..c33412a --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-clock-hot.md @@ -0,0 +1,17 @@ +# cs-clock-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[cs-clock-hot.jsonl](../cs-clock-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`Stopwatch.GetTimestamp` 每次迭代与一个样本。 .NET **10.0.13**,linux x64,负载 0.09。早期的 .NET 8 配对大约是 15 倍。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在…之后 | `sample_once` | 0.0347 | 100000 | 1 | +| 之前 | `sample_each_iter` | 1.2729 | 100000 | 36.683 | + +在采样一次之后,并且不会在循环内部读取时钟。需要每次迭代获取新时间戳的代码没有可以提升的内容。默认保持 **hint**。这个比例并不是普遍的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-console-hot.md b/packages/csharpperf/evidence/zh-CN/cs-console-hot.md new file mode 100644 index 0000000..f10142b --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-console-hot.md @@ -0,0 +1,32 @@ +# cs-console-hot — 循环中的 Console.WriteLine + +**状态:** 已测量 + +## 主张 + +每次迭代将 `Console.WriteLine` 输出到 `TextWriter.Null` 比安静循环加一次写入要慢(在 .NET 10.0.13 上约慢 17.9 倍)。 + +## 如何测量 + +- 安装工具:EvidenceCsBench +- N=20000;控制台重定向到空 + +- 原始行:[`cs-console-hot.jsonl`](../cs-console-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dotnet | **8.0.32** | +| 循环内控制台前的中位数 (`console_in_loop`) | 0.2333 毫秒 | +| (`console_once`) 后的中位数 | 0.0130 毫秒 | +| **前后比例** | **~17.95×** | + +## 与 lint 的对应 + +`csharpperf-lint` cs-console-hot:保持默认严重性。 + +## 限制 + +- 依赖环境;默认保持 **关闭**。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-field-by-name-hot.md b/packages/csharpperf/evidence/zh-CN/cs-field-by-name-hot.md new file mode 100644 index 0000000..e3ffa8f --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-field-by-name-hot.md @@ -0,0 +1,17 @@ +# cs-field-by-name-hot — 已测量,仍然提示 + +## 状态 +**提示**. 实时时钟 JSONL: [cs-field-by-name-hot.jsonl](../cs-field-by-name-hot.jsonl)。未进行语料库分流。2026-10-22 的语料库裁定不涵盖此标识。 + +## 测量 +每次迭代使用 `typeof(Box).GetField("A").GetValue` 与直接读取字段。20000 次操作。 .NET **10.0.13**,linux x64,负载 0.02。之前的 .NET 8 对比约为 62×。另一组 .NET 8 测试中,调用 `GetField` 一次并在循环中使用 `GetValue`,耗时 0.514 毫秒;该拆分未重新运行。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `direct_field` | 0.0133 | 20000 | 1 | +| 之前 | `getfield_each` | 0.7084 | 20000 | 53.263 | + +每次迭代都会变化的名称无法成为直接字段。默认保持 **提示**。这一比率不是一般的加速。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在任何默认更改之前,仍然需要进行语料库分类处理。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-file-read-hot.md b/packages/csharpperf/evidence/zh-CN/cs-file-read-hot.md new file mode 100644 index 0000000..a45f420 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-file-read-hot.md @@ -0,0 +1,17 @@ +# cs-file-read-hot — 已测量,仍有提示 + +## 状态 +**提示**。挂钟 JSONL: [cs-file-read-hot.jsonl](../cs-file-read-hot.jsonl)。未进行语料库分级。2026-10-22 的语料库裁决不涵盖此条目。 + +## 测量 +`File.ReadAllText` 对一个一字节文件读取 2000 次,与只读取一次的对比。 .NET **10.0.13**,Linux x64,负载 0.07。一次 `read_once` 样本为 1.740 毫秒;中位数在 0.015–0.026 毫秒区间。早期的 .NET 8 对应值约为 1560×。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `read_once` | 0.0178 | 2000 | 1 | +| 之前 | `read_each_iter` | 6.0142 | 2000 | 337.876 | + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在任何默认更改之前,仍然需要进行语料库分类处理。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-format-hot.md b/packages/csharpperf/evidence/zh-CN/cs-format-hot.md new file mode 100644 index 0000000..ff3cb1f --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-format-hot.md @@ -0,0 +1,20 @@ +# cs-format-hot — 稳定测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[cs-format-hot.jsonl](../cs-format-hot.jsonl)。未经过语料库筛选。2026-10-22 的语料库裁定不涵盖此标签。 + +## 测量 + +50000 次迭代。之前使用 `string.Format` 对循环索引进行调用;之后使用 `Int32.ToString`。两者都会分配一个字符串。 .NET **10.0.13**,Linux x64,负载 0.08。前三个 `ToString` 样本为 0.49–0.69 毫秒,其余接近 0.29 毫秒;中位数为 0.3132 毫秒。`Format` 样本为 1.53–1.59 毫秒。早期的 .NET 8 对照大约是 13.7 倍。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| after | `tostring` | 0.3132 | 50000 | 1 | +| 之前 | `format_each_iter` | 1.565 | 50000 | 4.997 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。输入的变化、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍然需要进行语料库分类。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-json-hot.md b/packages/csharpperf/evidence/zh-CN/cs-json-hot.md new file mode 100644 index 0000000..9cbf395 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-json-hot.md @@ -0,0 +1,17 @@ +# cs-json-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[cs-json-hot.jsonl](../cs-json-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +在每次迭代之前反序列化一次固定的有效负载。之后只反序列化一次。 .NET **10.0.13**,linux x64,负载 0.04。早期的 .NET 8 配对大约是 1500×。一个 JSON 数字变化的 .NET 8 循环对于相同的 20000 次操作耗时 19.602 毫秒;该变化负载的形状未重新运行。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 反序列化一次 | 0.0139 | 20000 | 1 | +| 之前 | 每次迭代相同的有效负载 | 7.9792 | 20000 | 574.043 | + +只有在有效载荷不变时,较大的比率才成立。默认保持提示。 + +## 解除阻塞 +不要仅从此文件将默认值设置为 `error`。另一个输入或冷路径可能会抹去差异。任何默认更改前仍需进行语料库分流处理。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-linq-materialize-hot.md b/packages/csharpperf/evidence/zh-CN/cs-linq-materialize-hot.md new file mode 100644 index 0000000..5a2318d --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-linq-materialize-hot.md @@ -0,0 +1,20 @@ +# cs-linq-materialize-hot — 测量过的,仍然有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[cs-linq-materialize-hot.jsonl](../cs-linq-materialize-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 + +对一个32整数数组进行20000次迭代。每次迭代前调用一次`ToList`;之后只调用一次。.NET **10.0.13**,linux x64,负载0.04。之前的.NET 8 配对大约是42倍。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `tolist_once` | 0.0158 | 20000 | 1 | +| 之前 | `tolist_each_iter` | 0.6147 | 20000 | 38.905 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-list-add-no-capacity.md b/packages/csharpperf/evidence/zh-CN/cs-list-add-no-capacity.md new file mode 100644 index 0000000..5978131 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-list-add-no-capacity.md @@ -0,0 +1,32 @@ +# cs-list-add-no-capacity — List.Add 无需容量 + +**状态:** 已测量 + +## 主张 + +在 .NET 10.0.13 中,`new List()` 然后添加 N 次大约是 `new List(N)` 的 1.31 倍。之前一个繁忙机器的测试大约是 1.25 倍。随后在同一主机上的两次运行大约是 1.72 倍(0.462 毫秒 vs 0.269 毫秒,以及 0.426 毫秒 vs 0.248 毫秒);没有容量的样本范围大约从 0.30 毫秒到 0.90 毫秒,因此这些运行不能替代下面的那行。之前约 13 倍的数据不是这次运行的。规则保持关闭。 + +## 如何测量 + +- 测试工具:`examples/bench/EvidenceCsBench`(`dotnet run -c Release`) +- N=200000 + +- 原始行: [`cs-list-add-no-capacity.jsonl`](../cs-list-add-no-capacity.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 UTC(linux x64,负载 0.58) | +| dotnet | **10.0.13** | +| 中位数之前 (`list_add_nocap`) | 0.4057 毫秒 | +| (`list_capacity`) 后的中位数 | 0.3101 毫秒 | +| **前后比例** | **~1.31×** | + +## 与 lint 的对应 + +`csharpperf-lint` cs-list-add-no-capacity:保持默认严重性。 + +## 限制 + +- 默认保持**关闭**(G3)。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-regex-hot.md b/packages/csharpperf/evidence/zh-CN/cs-regex-hot.md new file mode 100644 index 0000000..1d0acb7 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-regex-hot.md @@ -0,0 +1,36 @@ +# cs-regex-hot — 循环中的 Regex.IsMatch(pattern) + +**状态:** 已测量 + +## 主张 + +在每次迭代中调用 `Regex.IsMatch(name, pattern)` 比使用一个 `RegexOptions.Compiled` 实例慢(在 .NET 10.0.13 上约为 2.58 倍)。 + +## 如何测量 + +- 测试工具:EvidenceCsBench +- N=50000;模式 `^[a-z]+$` + +- 原始行: [`cs-regex-hot.jsonl`](../cs-regex-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dotnet | **8.0.32** | +| 调用 `regex_ismatch_loop` 前的中位数 | 4.1372 毫秒 | +| (`compiled_regex`) 后的中位数 | 1.6045 毫秒 | +| **前/后比例** | **~2.58×** | + +## 与 lint 的对应 + +`csharpperf-lint` cs-regex-hot:保持默认严重性。 + +## 限制 + +- 适度比例;默认保持 **提示**;仅凭约 2.6× 不要升级为错误。 + +## 构造函数形状 + +`new Regex` 每次迭代与使用一个实例。双方仍然在 `id-2026` 上调用 `IsMatch`。2000 次操作。之前 3.453 毫秒,之后 0.483 毫秒,比例 7.145。匹配器现在会标记 `new Regex(` 以及静态的 `Regex.IsMatch` / `Match` 调用。默认保持 **hint**。 diff --git a/packages/csharpperf/evidence/zh-CN/cs-string-plus-hot.md b/packages/csharpperf/evidence/zh-CN/cs-string-plus-hot.md new file mode 100644 index 0000000..9972860 --- /dev/null +++ b/packages/csharpperf/evidence/zh-CN/cs-string-plus-hot.md @@ -0,0 +1,32 @@ +# cs-string-plus-hot — 循环中的 string += + +**状态:** 已测量 + +## 主张 + +在循环中使用不可变的 `string +=` 比使用 `StringBuilder` 对于 N=30000 慢得多。 + +## 如何测量 + +- 测试工具:EvidenceCsBench +- 样本量=30000 + +- 原始行: [`cs-string-plus-hot.jsonl`](../cs-string-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dotnet | **8.0.32** | +| `string_plus_equals` 之前的中位数 | 226.0176 毫秒 | +| (`stringbuilder`) 后的中位数 | 0.0885 毫秒 | +| **压缩前/后比例** | **~2554×** | + +## 与 lint 的对应 + +`csharpperf-lint` cs-string-plus-hot:保持默认严重性。 + +## 限制 + +- 默认保持**关闭**。大比例是无I/O的CPU/分配。 diff --git a/packages/csharpperf/zh-CN/CONTRIBUTING.md b/packages/csharpperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/csharpperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/csharpperf/zh-CN/README.md b/packages/csharpperf/zh-CN/README.md new file mode 100644 index 0000000..3934569 --- /dev/null +++ b/packages/csharpperf/zh-CN/README.md @@ -0,0 +1,38 @@ +# csharp性能 + +C# 性能笔记、小示例,以及 `csharpperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md) 的一部分。共享文档: [docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**;cs-regex-hot=提示;cs-list-add-no-capacity–cs-console-hot **关闭**(启发式;升级需要 JSONL 证据以及政策决定)。指南 v0。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/csharp-performance-guide.md) — 来自 `docs/guide-src/` +- [`csharpperf-lint`](../csharpperf-lint/README.md) — cs-list-add-no-capacity–cs-regex-hot +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +预先设置 `List` 容量,避免字符串呈二次方增长,避免在紧密循环中使用 `Console.Write*`。 + +## 运行 lint + +```sh +cd packages/csharpperf/csharpperf-lint +dotnet build -c Release +dotnet run -c Release --no-build -- path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅在 **错误** 严重性发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ pending stubs until benches land +examples/ tiny demos +csharpperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh (both skip when `dotnet` is absent) +``` diff --git a/packages/dartperf/docs/dart-performance-guide.md b/packages/dartperf/docs/dart-performance-guide.md index 06a1572..40ffd30 100644 --- a/packages/dartperf/docs/dart-performance-guide.md +++ b/packages/dartperf/docs/dart-performance-guide.md @@ -2,16 +2,12 @@ Fewer list resizes, fewer string `+` temporaries, quieter hot loops. -> **Scope: Dart 3.x (VM / Flutter).** Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Dart 3.x (VM / Flutter).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`benchmark_harness`, DevTools CPU profiler). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (`benchmark_harness`, DevTools CPU profiler). Treat chapters as checklists, not rewrite mandates. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists](#1-lists) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) @@ -20,13 +16,13 @@ Fewer list resizes, fewer string `+` temporaries, quieter hot loops. `List.add` in a hot loop is fine when capacity growth amortizes, but allocating a fresh growable list every iteration is not. Prefer `List.filled` / `List.generate` / known length when possible. -Companion lint: **dt-list-add-empty**, **dt-growable-add-empty** (empty growable list + `add` in a size-hinted loop). +Lint: `dt-list-add-empty` (default off); `dt-growable-add-empty` (empty growable list + `add` in a size-hinted loop; default hint). ## 2. Strings and hot I/O `s = s + x` in a loop builds many strings. Prefer `StringBuffer`. Keep `print` out of tight loops. -Companion lint: **dt-string-plus-hot**, **dt-print-hot**. +Lint: `dt-string-plus-hot`, `dt-print-hot` (default off). ## Lint map diff --git a/packages/dartperf/docs/guide-src/00_head.md b/packages/dartperf/docs/guide-src/00_head.md index 8589fdb..092d95a 100644 --- a/packages/dartperf/docs/guide-src/00_head.md +++ b/packages/dartperf/docs/guide-src/00_head.md @@ -2,16 +2,12 @@ Fewer list resizes, fewer string `+` temporaries, quieter hot loops. -> **Scope: Dart 3.x (VM / Flutter).** Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Dart 3.x (VM / Flutter).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`benchmark_harness`, DevTools CPU profiler). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (`benchmark_harness`, DevTools CPU profiler). Treat chapters as checklists, not rewrite mandates. ## Contents -- [How to read this](#how-to-read-this) - [1. Lists](#1-lists) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) diff --git a/packages/dartperf/docs/guide-src/01_lists.md b/packages/dartperf/docs/guide-src/01_lists.md index 7e81741..870e6f8 100644 --- a/packages/dartperf/docs/guide-src/01_lists.md +++ b/packages/dartperf/docs/guide-src/01_lists.md @@ -2,5 +2,5 @@ `List.add` in a hot loop is fine when capacity growth amortizes, but allocating a fresh growable list every iteration is not. Prefer `List.filled` / `List.generate` / known length when possible. -Companion lint: **dt-list-add-empty**, **dt-growable-add-empty** (empty growable list + `add` in a size-hinted loop). +Lint: `dt-list-add-empty` (default off); `dt-growable-add-empty` (empty growable list + `add` in a size-hinted loop; default hint). diff --git a/packages/dartperf/docs/guide-src/02_strings.md b/packages/dartperf/docs/guide-src/02_strings.md index c72a01e..9a11118 100644 --- a/packages/dartperf/docs/guide-src/02_strings.md +++ b/packages/dartperf/docs/guide-src/02_strings.md @@ -2,5 +2,5 @@ `s = s + x` in a loop builds many strings. Prefer `StringBuffer`. Keep `print` out of tight loops. -Companion lint: **dt-string-plus-hot**, **dt-print-hot**. +Lint: `dt-string-plus-hot`, `dt-print-hot` (default off). diff --git a/packages/dartperf/docs/guide-src/zh-CN/00_head.md b/packages/dartperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..3771731 --- /dev/null +++ b/packages/dartperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,14 @@ +# Dart 性能指南(v0) + +少做 list 扩容,少产生字符串 `+` 临时对象,让热循环更安静。 + +> **范围:Dart 3.x(VM / Flutter)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`benchmark_harness`、DevTools CPU profiler)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. List](#1-list) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/dartperf/docs/guide-src/zh-CN/01_lists.md b/packages/dartperf/docs/guide-src/zh-CN/01_lists.md new file mode 100644 index 0000000..e2730a2 --- /dev/null +++ b/packages/dartperf/docs/guide-src/zh-CN/01_lists.md @@ -0,0 +1,5 @@ +## 1. List + +热循环里的 `List.add` 在容量摊销时可以接受,但每次迭代新建可增长 list 不行。尽可能用 `List.filled` / `List.generate` / 已知长度。 + +Lint: `dt-list-add-empty`(默认 off); `dt-growable-add-empty`(empty growable list + `add` in a size-hinted loop; 默认 hint) diff --git a/packages/dartperf/docs/guide-src/zh-CN/02_strings.md b/packages/dartperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..b59bf55 --- /dev/null +++ b/packages/dartperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串与热 I/O + +循环里的 `s = s + x` 会造出许多字符串。优先 `StringBuffer`。不要把 `print` 放进紧密循环。 + +Lint: `dt-string-plus-hot`, `dt-print-hot`(默认 off) diff --git a/packages/dartperf/docs/guide-src/zh-CN/99_tail.md b/packages/dartperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..76cadef --- /dev/null +++ b/packages/dartperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,14 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| dt-list-add-empty | List 增长 / 无容量线索的 `add` | off | +| dt-string-plus-hot | 循环内字符串 `+` | off | +| dt-print-hot | 循环内 `print` | off | +| dt-growable-add-empty | 空可增长 list + 有大小提示循环里的 `add` | hint | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/dartperf/docs/zh-CN/corpus.md b/packages/dartperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..337a11f --- /dev/null +++ b/packages/dartperf/docs/zh-CN/corpus.md @@ -0,0 +1,64 @@ +# dartperf 语料库目标 + +大型树木**不会**被供应,语料库也不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查工具。 +使用以下方式进行检查: + +```sh +export PATH="$HOME/dart-sdk/bin:$PATH" +cd packages/dartperf/dartperf-lint && dart pub get +dart run bin/dartperf_lint.dart / --format json --no-fail +``` + +检查器是 **Dart** (`dartperf-lint`)。规则 dt-list-add-empty–dt-growable-add-empty。 + +## 固定目标 + +克隆于 2026-10-21(亚洲/上海),r1。 + +| 树 | 提交 | 大小 | Lint (基线 → r1) | +|------|--------|------|----------------------| +| **bloc** | `b9be1e2` 浅拷贝 `https://github.com/felangel/bloc.git` | 64M | **10 → 3** (dt-list-add-empty:3) | +| **dio** | `f1bef46` 浅克隆 `https://github.com/cfug/dio.git` | 9.6M | **19 → 5** (dt-list-add-empty:5) | +| **http** | `1217181` 浅拷贝 `https://github.com/dart-lang/http.git` | 8.8M | **29 → 4** (dt-list-add-empty:4) | + +路径跳过(r1 reverse-opt):`test/`、`tests/`、`example/`、`examples/`、`example_dart/`、`example_flutter/`、`scripts/`、`benchmark(s)/`、`integration_test/`、`fixtures/`、`docs/`,以及以 `*_test.dart` 结尾的文件和以 `_tests` 结尾或包含 `conformance_test` 的目录。 + +dt-list-add-empty:跳过 StreamController / Sink / `*C.add`(队列/流)模式。 + +剩余(保留作为信号):在 bloc LSP 辅助工具中的生产 `List.add`,dio form_data / 适配器,http2 帧/流队列。 + +## 库存 + +```text +/ + bloc/ + dio/ + http/ +``` + +不要将语料库来源提交到 `packages/dartperf`。 + +## G3 TP/FP 分诊(2026-10-22 亚洲/上海) + +| | r1 n=12 | r2 n=7 | +|--|---------|--------| +| TP | 0% | 0% | +| FP | 33.3% | 0% | +| OK-idiom | 66.7% | **100%** | +| **FP+DOC+SKIP** | **33.3%** | **0%** | + +**准备好语料库?`否`** — BytesBuilder/Headers/Set FPs 已修复;残留是故意的列表收集。保持 dt-list-add-empty **提示**。 + +### 修复已完成 +- 非列表 `.add` 白名单;`??= []` 不再触发 growAssign +- r2 计数:bloc **3**,dio **2**,http **2** + +## 重新设计 → 准备语料库(2026-10-22 亚洲/上海) + +| 树 | r2 | r3(默认) | 是否准备好语料库? | +|------|----|---------------|---------------| +| bloc | 3 | **0** | | +| dio | 2 | **0** | | +| http | 2 | **1** | | + +**裁定:语料库可用 `是`。** 示例 n=1:TP 100% / FP+DOC+SKIP 0%。默认设置:dt-list-add-empty–dt-print-hot **关闭**;**dt-growable-add-empty** **提示**。检查工具为 Dart (`dartperf-lint`)。 diff --git a/packages/dartperf/docs/zh-CN/dart-performance-guide.md b/packages/dartperf/docs/zh-CN/dart-performance-guide.md new file mode 100644 index 0000000..ff312ae --- /dev/null +++ b/packages/dartperf/docs/zh-CN/dart-performance-guide.md @@ -0,0 +1,38 @@ +# Dart 性能指南(v0) + +少做 list 扩容,少产生字符串 `+` 临时对象,让热循环更安静。 + +> **范围:Dart 3.x(VM / Flutter)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`benchmark_harness`、DevTools CPU profiler)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. List](#1-list) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + +## 1. List + +热循环里的 `List.add` 在容量摊销时可以接受,但每次迭代新建可增长 list 不行。尽可能用 `List.filled` / `List.generate` / 已知长度。 + +Lint: `dt-list-add-empty`(默认 off); `dt-growable-add-empty`(empty growable list + `add` in a size-hinted loop; 默认 hint) +## 2. 字符串与热 I/O + +循环里的 `s = s + x` 会造出许多字符串。优先 `StringBuffer`。不要把 `print` 放进紧密循环。 + +Lint: `dt-string-plus-hot`, `dt-print-hot`(默认 off) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| dt-list-add-empty | List 增长 / 无容量线索的 `add` | off | +| dt-string-plus-hot | 循环内字符串 `+` | off | +| dt-print-hot | 循环内 `print` | off | +| dt-growable-add-empty | 空可增长 list + 有大小提示循环里的 `add` | hint | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/dartperf/docs/zh-CN/evidence-policy.md b/packages/dartperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..e617a58 --- /dev/null +++ b/packages/dartperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,16 @@ +# dartperf 证据政策 + +## 门槛 + +一条规则只有在有 `evidence/.md` + `.jsonl` **以及**明确的政策决定的情况下才可以被视为 **`error`**。仅测量的 JSONL 并不会自动升级。批次 B(2026-10-22)保持了软件包默认设置。 + +## 当前映射 + +截至2026-10-22(B批次 G4)。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| dt-list-add-empty | 关闭 | **是** ~3.1× | 空列表 + 添加 | +| dt-字符串-plus-hot | 关闭 | **是** ~247× | 字符串 + | +| dt-print-hot | 关闭 | **是** ~128× | 打印/写入 | +| dt-growable-add-empty | 提示 | **是** ~2.4× | 空 + 添加大小 | diff --git a/packages/dartperf/docs/zh-CN/rule-ids.md b/packages/dartperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..395fffa --- /dev/null +++ b/packages/dartperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,21 @@ +# dartperf 规则 ID(简写) + +规范 ID 仅为 **slug**。旧地图中的数字是历史性的,**不**被接受。 + +| Slug | +|------| +| `dt-list-add-empty` | +| `dt-string-plus-hot` | +| `dt-print-hot` | +| `dt-growable-add-empty` | + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或相应的工具包选项)。旧的 `*-in-loop` ID 会误导工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `dt-print-in-loop` | `dt-print-hot` | +| `dt-string-plus-in-loop` | `dt-string-plus-hot` | diff --git a/packages/dartperf/evidence/zh-CN/README.md b/packages/dartperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..2c4da68 --- /dev/null +++ b/packages/dartperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# dartperf 证据 + +更新于 **2026-10-22** 亚洲/上海(B批次 G4 填充)。 + +在 Dart 3.13.6 上测量的 dt-list-add-empty–dt-growable-add-empty。默认值保持不变。 + +请参阅每条规则的 `*.md` + `*.jsonl`(或被阻止的存根)。政策:[../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。根矩阵:[../../../docs/rule-regression.md](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/dartperf/evidence/zh-CN/dt-growable-add-empty.md b/packages/dartperf/evidence/zh-CN/dt-growable-add-empty.md new file mode 100644 index 0000000..39af91d --- /dev/null +++ b/packages/dartperf/evidence/zh-CN/dt-growable-add-empty.md @@ -0,0 +1,32 @@ +# dt-growable-add-empty — 空可扩展 + 添加(提示规则) + +**状态:** 已测量 + +## 主张 + +与 dt-list-add-empty 相同的内核:空可增长 + 添加 与 `List.filled`(约 3.12×)在 Dart 3.13.7 上的比较。 + +## 如何测量 + +- Harness: evidence_dt.dart +- N=200000 + +- 原始行:[`dt-growable-add-empty.jsonl`](../dt-growable-add-empty.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dart | **3.13.6** | +| 中值之前 (`empty_add`) | 3.820 毫秒 | +| (`list_filled`) 后的中位数 | 1.226 毫秒 | +| **前/后比例** | **~3.12×** | + +## 与 lint 的对应 + +`dartperf-lint` dt-growable-add-empty: 保持默认严重性。 + +## 限制 + +- 谦虚;默认保留 **提示**;不升级。 diff --git a/packages/dartperf/evidence/zh-CN/dt-list-add-empty.md b/packages/dartperf/evidence/zh-CN/dt-list-add-empty.md new file mode 100644 index 0000000..60799fe --- /dev/null +++ b/packages/dartperf/evidence/zh-CN/dt-list-add-empty.md @@ -0,0 +1,32 @@ +# dt-list-add-empty — 空列表 + 添加 + +**状态:** 已测量 + +## 主张 + +在 Dart 3.13.7 中,可增长的 `[]` + `add` 比 `List.filled` + 索引写入慢(约 4.40 倍)。 + +## 如何测量 + +- 工具:`examples/bench/evidence_dt.dart` +- N=200000 + +- 原始行: [`dt-list-add-empty.jsonl`](../dt-list-add-empty.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dart | **3.13.6** | +| 中值之前 (`add_loop_empty`) | 9.505 毫秒 | +| (`list_filled`) 后的中位数 | 2.158 毫秒 | +| **前/后比例** | **~4.40×** | + +## 与 lint 的对应 + +`dartperf-lint` dt-list-add-empty:保持默认严重性。 + +## 限制 + +- 默认保持**关闭**。 diff --git a/packages/dartperf/evidence/zh-CN/dt-print-hot.md b/packages/dartperf/evidence/zh-CN/dt-print-hot.md new file mode 100644 index 0000000..e6382cf --- /dev/null +++ b/packages/dartperf/evidence/zh-CN/dt-print-hot.md @@ -0,0 +1,32 @@ +# dt-print-hot — 循环打印/写入 + +**状态:** 已测量 + +## 主张 + +每次迭代同步写入 `/dev/null` 比循环结束后一次写入慢得多(在 Dart 3.13.7 上约为 2040 倍)。循环后的样本为 0.004 毫秒,因此比例接近计时器的最小值。 + +## 如何测量 + +- 测试工具:evidence_dt.dart +- N=10000 + +- 原始行:[`dt-print-hot.jsonl`](../dt-print-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dart | **3.13.6** | +| 中值之前 (`print_in_loop`) | 8.159 毫秒 | +| (`print_once`) 之后的中位数 | 0.004 毫秒 | +| **前/后比例** | **~2040×** | + +## 与 lint 的对应 + +`dartperf-lint` dt-print-hot:保持默认严重性。 + +## 限制 + +- `print` 的 I/O 代理;默认保持 **关闭**。 diff --git a/packages/dartperf/evidence/zh-CN/dt-string-plus-hot.md b/packages/dartperf/evidence/zh-CN/dt-string-plus-hot.md new file mode 100644 index 0000000..0303ef4 --- /dev/null +++ b/packages/dartperf/evidence/zh-CN/dt-string-plus-hot.md @@ -0,0 +1,31 @@ +# dt-string-plus-hot — 循环中的字符串 + + +**状态:** 已测量 + +## 主张 + +`s = s + i.toString()` 比 `StringBuffer` 慢得多(约慢 208 倍),在 Dart 3.13.7 中,N=30000 时。 + +## 如何测量 + +- 安全带:evidence_dt.dart + +- 原始行: [`dt-string-plus-hot.jsonl`](../dt-string-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| dart | **3.13.6** | +| `string_plus` 之前的中位数 | 95.331 毫秒 | +| (`string_buffer`) 之后的中位数 | 0.458 毫秒 | +| **前/后比例** | **~208×** | + +## 与 lint 的对应 + +`dartperf-lint` dt-string-plus-hot:保持默认严重性。 + +## 限制 + +- 默认保持**关闭**。 diff --git a/packages/dartperf/zh-CN/CONTRIBUTING.md b/packages/dartperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/dartperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/dartperf/zh-CN/README.md b/packages/dartperf/zh-CN/README.md new file mode 100644 index 0000000..7112629 --- /dev/null +++ b/packages/dartperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# dartperf + +> **范围说明:** **dartperf** 针对 **Dart**(Flutter/VM)。启发式 lint;在升级前使用 `benchmark_harness` 测量。 + +Dart 性能笔记、小示例,以及 `dartperf-lint`。 + +部分 [XPerf](../../../zh-CN/README.md)。共享文档: [docs/README.md](../../../docs/zh-CN/README.md)。**语料库准备**。dt-list-add-empty–dt-print-hot **关闭**;**dt-growable-add-empty** 提示。指南 v0。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/dart-performance-guide.md) — 来自 `docs/guide-src/` +- [`dartperf-lint`](../dartperf-lint/README.md) — dt-list-add-empty–dt-growable-add-empty +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +优先使用固定长度/容量感知的列表,避免在循环中使用字符串 `+`(使用 `StringBuffer`),在热点循环中不要使用 `print`。 + +## 运行 lint + +```sh +cd packages/dartperf/dartperf-lint && dart pub get +dart run bin/dartperf_lint.dart path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅在 **错误** 严重性发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ pending stubs until benches land +examples/ tiny demos +dartperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/goperf/docs/go-performance-guide.md b/packages/goperf/docs/go-performance-guide.md index 95a8121..7f9f614 100644 --- a/packages/goperf/docs/go-performance-guide.md +++ b/packages/goperf/docs/go-performance-guide.md @@ -2,40 +2,37 @@ Less allocation chatter, fewer surprise `fmt` / string-growth stalls. -> **Scope: Go 1.21+.** Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Go 1.21+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`go test -bench`, `pprof`, `benchstat`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Escape analysis and inlining change costs — re-bench after toolchain bumps. +Profile first (`go test -bench`, `pprof`, `benchstat`). Escape analysis and inlining change costs — re-bench after toolchain bumps. ## Contents -- [How to read this](#how-to-read-this) - [1. Allocation](#1-allocation) - [2. Strings and formatting](#2-strings-and-formatting) +- [3. Compile, encode, and reflect](#3-compile-encode-and-reflect) - [Lint map](#lint-map) ## 1. Allocation -Prefer reusing buffers (`sync.Pool`, pre-sized slices) over `make`/`new` inside tight loops when profiles show alloc pressure. Escape analysis may keep small values on the stack — **do not** rewrite without a bench. +Prefer reusing buffers (`sync.Pool`, pre-sized slices) over `make`/`new` inside tight loops when profiles show alloc pressure. Escape analysis may keep small values on the stack — do not rewrite without a bench. -Companion lint: **gp-make-hot** (off after G3), **gp-append-nil-slice** (empty slice + append in size-hinted loop). +Lint: `gp-make-hot` (default off after G3); `gp-append-nil-slice` (empty slice + append in size-hinted loop; default hint). ## 2. Strings and formatting Repeated `s += piece` in a loop is quadratic. Prefer `strings.Builder` or `bytes.Buffer`. Hot-path `fmt.Sprintf` / `fmt.Printf` also allocate; gate debug prints or use cheaper appenders when measured. -Companion lint: **gp-string-plus-hot**/**gp-fmt-hot** (off after G3), **gp-defer-hot** (`defer` in loop). +Lint: `gp-string-plus-hot`, `gp-fmt-hot` (default off after G3); `gp-defer-hot` (`defer` in loop; default hint). ## 3. Compile, encode, and reflect -`regexp.Compile` / `MustCompile` and `json.Marshal` / `Unmarshal` inside a loop repeat work that can be done once when the pattern or payload does not change. `FieldByName`, `MethodByName`, and `reflect.New` inside a loop are the reflect lint. `TypeOf` on a concrete value was about 1.3× and is not matched. `time.Parse` was removed: a fixed timestamp is faster only when the parse is skipped, and a changing timestamp has nothing to hoist. These lints are **hint**. +`regexp.Compile` / `MustCompile` and `json.Marshal` / `Unmarshal` inside a loop repeat work that can be done once when the pattern or payload does not change. `FieldByName`, `MethodByName`, and `reflect.New` inside a loop are the reflect lint. `TypeOf` on a concrete value was about 1.3× and is not matched. `time.Parse` was removed: a fixed timestamp is faster only when the parse is skipped, and a changing timestamp has nothing to hoist. These lints are hint. `os.Open` of one path was about 2300× versus opening once. `time.Now` was about 104× versus one sample. That clock rule is not the removed `time.Parse` rule. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. -Companion lint: **gp-regexp-compile-hot**, **gp-json-marshal-hot**, **gp-reflect-hot**, **gp-os-open-hot**, **gp-time-now-hot**. +Lint: `gp-regexp-compile-hot`, `gp-json-marshal-hot`, `gp-reflect-hot`, `gp-os-open-hot`, `gp-time-now-hot` (default hint). + ## Lint map | ID | Smell | Default | diff --git a/packages/goperf/docs/guide-src/00_head.md b/packages/goperf/docs/guide-src/00_head.md index 887a2fc..709124c 100644 --- a/packages/goperf/docs/guide-src/00_head.md +++ b/packages/goperf/docs/guide-src/00_head.md @@ -2,18 +2,14 @@ Less allocation chatter, fewer surprise `fmt` / string-growth stalls. -> **Scope: Go 1.21+.** Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Go 1.21+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`go test -bench`, `pprof`, `benchstat`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Escape analysis and inlining change costs — re-bench after toolchain bumps. +Profile first (`go test -bench`, `pprof`, `benchstat`). Escape analysis and inlining change costs — re-bench after toolchain bumps. ## Contents -- [How to read this](#how-to-read-this) - [1. Allocation](#1-allocation) - [2. Strings and formatting](#2-strings-and-formatting) +- [3. Compile, encode, and reflect](#3-compile-encode-and-reflect) - [Lint map](#lint-map) diff --git a/packages/goperf/docs/guide-src/01_allocation.md b/packages/goperf/docs/guide-src/01_allocation.md index 3431718..58661e7 100644 --- a/packages/goperf/docs/guide-src/01_allocation.md +++ b/packages/goperf/docs/guide-src/01_allocation.md @@ -1,6 +1,6 @@ ## 1. Allocation -Prefer reusing buffers (`sync.Pool`, pre-sized slices) over `make`/`new` inside tight loops when profiles show alloc pressure. Escape analysis may keep small values on the stack — **do not** rewrite without a bench. +Prefer reusing buffers (`sync.Pool`, pre-sized slices) over `make`/`new` inside tight loops when profiles show alloc pressure. Escape analysis may keep small values on the stack — do not rewrite without a bench. -Companion lint: **gp-make-hot** (off after G3), **gp-append-nil-slice** (empty slice + append in size-hinted loop). +Lint: `gp-make-hot` (default off after G3); `gp-append-nil-slice` (empty slice + append in size-hinted loop; default hint). diff --git a/packages/goperf/docs/guide-src/02_strings.md b/packages/goperf/docs/guide-src/02_strings.md index e663a3c..be2eddc 100644 --- a/packages/goperf/docs/guide-src/02_strings.md +++ b/packages/goperf/docs/guide-src/02_strings.md @@ -2,5 +2,5 @@ Repeated `s += piece` in a loop is quadratic. Prefer `strings.Builder` or `bytes.Buffer`. Hot-path `fmt.Sprintf` / `fmt.Printf` also allocate; gate debug prints or use cheaper appenders when measured. -Companion lint: **gp-string-plus-hot**/**gp-fmt-hot** (off after G3), **gp-defer-hot** (`defer` in loop). +Lint: `gp-string-plus-hot`, `gp-fmt-hot` (default off after G3); `gp-defer-hot` (`defer` in loop; default hint). diff --git a/packages/goperf/docs/guide-src/03_hot_calls.md b/packages/goperf/docs/guide-src/03_hot_calls.md index 6119452..03e168c 100644 --- a/packages/goperf/docs/guide-src/03_hot_calls.md +++ b/packages/goperf/docs/guide-src/03_hot_calls.md @@ -1,7 +1,8 @@ ## 3. Compile, encode, and reflect -`regexp.Compile` / `MustCompile` and `json.Marshal` / `Unmarshal` inside a loop repeat work that can be done once when the pattern or payload does not change. `FieldByName`, `MethodByName`, and `reflect.New` inside a loop are the reflect lint. `TypeOf` on a concrete value was about 1.3× and is not matched. `time.Parse` was removed: a fixed timestamp is faster only when the parse is skipped, and a changing timestamp has nothing to hoist. These lints are **hint**. +`regexp.Compile` / `MustCompile` and `json.Marshal` / `Unmarshal` inside a loop repeat work that can be done once when the pattern or payload does not change. `FieldByName`, `MethodByName`, and `reflect.New` inside a loop are the reflect lint. `TypeOf` on a concrete value was about 1.3× and is not matched. `time.Parse` was removed: a fixed timestamp is faster only when the parse is skipped, and a changing timestamp has nothing to hoist. These lints are hint. `os.Open` of one path was about 2300× versus opening once. `time.Now` was about 104× versus one sample. That clock rule is not the removed `time.Parse` rule. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. -Companion lint: **gp-regexp-compile-hot**, **gp-json-marshal-hot**, **gp-reflect-hot**, **gp-os-open-hot**, **gp-time-now-hot**. +Lint: `gp-regexp-compile-hot`, `gp-json-marshal-hot`, `gp-reflect-hot`, `gp-os-open-hot`, `gp-time-now-hot` (default hint). + diff --git a/packages/goperf/docs/guide-src/zh-CN/00_head.md b/packages/goperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..c04a4da --- /dev/null +++ b/packages/goperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Go 性能指南(v0) + +少一点分配噪声,少一点意外的 `fmt` / 字符串增长停顿。 + +> **范围:Go 1.21+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`go test -bench`、`pprof`、`benchstat`)。逃逸分析与内联会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. 分配](#1-分配) +- [2. 字符串与格式化](#2-字符串与格式化) +- [3. 编译、编码与反射](#3-编译编码与反射) +- [Lint 对照](#lint-对照) + diff --git a/packages/goperf/docs/guide-src/zh-CN/01_allocation.md b/packages/goperf/docs/guide-src/zh-CN/01_allocation.md new file mode 100644 index 0000000..dbe6f28 --- /dev/null +++ b/packages/goperf/docs/guide-src/zh-CN/01_allocation.md @@ -0,0 +1,5 @@ +## 1. 分配 + +画像显示分配压力时,紧密循环里优先复用缓冲区(`sync.Pool`、预分配 slice),而不是 `make`/`new`。逃逸分析可能把小值留在栈上——没有 bench 就不要改。 + +Lint: `gp-make-hot`(默认 off,G3 后); `gp-append-nil-slice`(空 slice + 有大小提示循环里的 append;默认 hint) diff --git a/packages/goperf/docs/guide-src/zh-CN/02_strings.md b/packages/goperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..6cd64e8 --- /dev/null +++ b/packages/goperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串与格式化 + +循环里反复 `s += piece` 是平方级的。优先 `strings.Builder` 或 `bytes.Buffer`。热路径上的 `fmt.Sprintf` / `fmt.Printf` 也会分配;门控调试打印,或在测得更便宜时改用更轻的 appender。 + +Lint: `gp-string-plus-hot`, `gp-fmt-hot`(默认 off,G3 后); `gp-defer-hot`(循环内 `defer`;默认 hint) diff --git a/packages/goperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/goperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..e6004f7 --- /dev/null +++ b/packages/goperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,7 @@ +## 3. 编译、编码与反射 + +循环里的 `regexp.Compile` / `MustCompile` 与 `json.Marshal` / `Unmarshal` 会重复本可一次完成的工作(当模式或 payload 不变时)。循环里的 `FieldByName`、`MethodByName`、`reflect.New` 属于 reflect lint。对具体值的 `TypeOf` 约 1.3×,不匹配。`time.Parse` 已移除:固定时间戳仅在跳过解析时更快,变化的时间戳没有可外提的东西。这些 lint 是 hint。 + +对同一路径 `os.Open` 约 2300×(相对打开一次)。`time.Now` 约 104×(相对采一次样)。该时钟规则不是已移除的 `time.Parse` 规则。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +Lint: `gp-regexp-compile-hot`, `gp-json-marshal-hot`, `gp-reflect-hot`, `gp-os-open-hot`, `gp-time-now-hot`(默认 hint) diff --git a/packages/goperf/docs/guide-src/zh-CN/99_tail.md b/packages/goperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..386be3d --- /dev/null +++ b/packages/goperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,18 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| gp-make-hot | 循环内 `make`/`new` | off(G3) | +| gp-string-plus-hot | 循环内字符串 `+=` | off(G3) | +| gp-fmt-hot | 循环内 `fmt.Sprintf`/`Printf` | off(G3) | +| gp-append-nil-slice | 空 slice + 有大小提示循环里的 append | hint | +| gp-defer-hot | 循环内 `defer` | hint | +| gp-regexp-compile-hot | 循环内 `regexp.Compile` | hint(有测量;未 corpus 分级) | +| gp-json-marshal-hot | 循环内 `json.Marshal` / `Unmarshal` | hint(有测量;未 corpus 分级) | +| gp-reflect-hot | 循环内 `FieldByName` / `MethodByName` / `reflect.New` | hint(有 JSONL;不匹配 `TypeOf`) | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/goperf/docs/zh-CN/corpus.md b/packages/goperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..7ab2b53 --- /dev/null +++ b/packages/goperf/docs/zh-CN/corpus.md @@ -0,0 +1,34 @@ +# goperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +| 树 | 链接 | 提交 | 备注 | +|------|-----|--------|-------| +| gin | https://github.com/gin-gonic/gin | `0f09c3a9b4626d9fe9979cf4d0521d6ae32ab646` | 浅层;默认设置下无发现 | +| testify | https://github.com/stretchr/testify | `87a7b9d57689f6579db2da795ab8deeab29cb724` | 浅层 | +| go (src/) | https://github.com/golang/go | `05dbbf9175d021c5a61a69e3cc22abbd483d7fb0` | 稀疏检出 `src/`;`cmd/` 被跳过 | + +## G3 摘要(2026-10-22 亚洲/上海)——重新设计 + +| 树 | r2(重新设计前) | r6(默认设置) | 准备好语料库? | +|------|-------------------|---------------|---------------| +| 杜松子酒 | 0 | **0** | (根据启发式方法清理) | +| 作证 | 5 | **13** | OK-习语 利用 +| go-src | 429 | **210** | gp-append-nil-slice/gp-defer-hot | + +**判决:语料库准备就绪 `是`。** 样本 n=93:TP 77.4% / OK-习语 22.6% / FP+DOC+跳过 0%。默认值:gp-make-hot–gp-fmt-hot **关闭**;gp-append-nil-slice/gp-defer-hot **提示**。没有虚构的证据 JSONL / 没有 G4 升级。 + +### 重新设计说明 +- gp-append-nil-slice:空切片 + 在带大小提示的循环中 append +- gp-defer-hot:循环中的 defer(非嵌套闭包;字符串已清空) +- 跳过:`cmd/`、`generate.go`、`_codegen/` + +不要将语料库来源提交到 `packages/goperf`。 + +## 2026-10-23 — 提示规则不在 2026-10-22 判决范围内 + +2026-10-22 的语料库准备判决不包括以下子弹头。它们仍然保持 **提示** 状态,尚未经过 G2/G3。 + +- `gp-regexp-compile-hot`、`gp-json-marshal-hot`、`gp-reflect-hot` — 磁盘上的 JSONL;仍然是提示 diff --git a/packages/goperf/docs/zh-CN/evidence-policy.md b/packages/goperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..8f236e8 --- /dev/null +++ b/packages/goperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,29 @@ +# goperf 证据政策 + +## 门槛 + +只有当两者都存在于 `packages/goperf/evidence/` 下时,规则才可以被视为 **`error`**: + +1. `.md` — 声明、如何测量、`go` 版本 / 标志、限制 +2. `.jsonl` — ≥1 个前/后行,包含 `rule`、`case`、`median_ns`(或 `median_ms`)、`ops`、`ratio`、`ts` + +否则该规则保持 **`hint`**(或在 G3 OK-idiom 降级后为 **`off`**)。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| gp-make-hot | **关** | **是** ~265× | 循环中的 `make`/`new` (G3 可接受的惯用法) | +| gp-string-plus-hot | **关闭** | **是** ~2919× | 循环中的字符串 `+=` (G3 推荐用法) | +| gp-fmt-hot | **关闭** | **是** ~2.8× | 循环中的 `fmt.*printf`(G3 合适用法) | +| gp-append-nil-slice | 提示 | **是** ~12.2× | 在带大小提示的循环中对空切片使用 append | +| gp-defer-hot | 提示 | **是** ~33× | 循环中的 `defer` | +| gp-regexp-compile-hot | 提示 | 测量过的 (`gp-regexp-compile-hot.md` + `gp-regexp-compile-hot.jsonl`); 保持提示 | 循环中的 `regexp.Compile`; 未经过语料库分类 | +| gp-json-marshal-hot | 提示 | 测量过的 (`gp-json-marshal-hot.md` + `gp-json-marshal-hot.jsonl`); 保持提示 | 循环中的 `json.Marshal` / `Unmarshal`; 未进行语料库筛选 | +| gp-reflect-hot | 提示 | 已测量(`gp-reflect-hot.md` + `gp-reflect-hot.jsonl`);保持提示 | 循环中的 `FieldByName` / `MethodByName` / `reflect.New`;`TypeOf` 不匹配;未经过语料库分类 | +| gp-os-open-hot | 提示 | 已测量(`gp-os-open-hot.md` + `gp-os-open-hot.jsonl`);保持提示 | 循环中的 `os.Open`;仅同一路径;未进行语料库筛选 | +| gp-time-now-hot | 提示 | 已测量 (`gp-time-now-hot.md` + `gp-time-now-hot.jsonl`); 保持提示 | 循环中的 `time.Now`; 不是 `time.Parse`; 不是语料库分类 | + +所有五条规则都有对应的 `.md` + `.jsonl` 测量文件,但没有任何一条会自动升级:gp-append-nil-slice/gp-defer-hot 保持 **提示** 状态,三条降级规则保持 **关闭** 状态(G3 OK-idiom 历史)。可以使用 `--severity =hint` 重新启用降级规则(例如 `--severity gp-make-hot=hint`)。 diff --git a/packages/goperf/docs/zh-CN/go-performance-guide.md b/packages/goperf/docs/zh-CN/go-performance-guide.md new file mode 100644 index 0000000..11ba117 --- /dev/null +++ b/packages/goperf/docs/zh-CN/go-performance-guide.md @@ -0,0 +1,50 @@ +# Go 性能指南(v0) + +少一点分配噪声,少一点意外的 `fmt` / 字符串增长停顿。 + +> **范围:Go 1.21+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`go test -bench`、`pprof`、`benchstat`)。逃逸分析与内联会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. 分配](#1-分配) +- [2. 字符串与格式化](#2-字符串与格式化) +- [3. 编译、编码与反射](#3-编译编码与反射) +- [Lint 对照](#lint-对照) + +## 1. 分配 + +画像显示分配压力时,紧密循环里优先复用缓冲区(`sync.Pool`、预分配 slice),而不是 `make`/`new`。逃逸分析可能把小值留在栈上——没有 bench 就不要改。 + +Lint: `gp-make-hot`(默认 off,G3 后); `gp-append-nil-slice`(空 slice + 有大小提示循环里的 append;默认 hint) +## 2. 字符串与格式化 + +循环里反复 `s += piece` 是平方级的。优先 `strings.Builder` 或 `bytes.Buffer`。热路径上的 `fmt.Sprintf` / `fmt.Printf` 也会分配;门控调试打印,或在测得更便宜时改用更轻的 appender。 + +Lint: `gp-string-plus-hot`, `gp-fmt-hot`(默认 off,G3 后); `gp-defer-hot`(循环内 `defer`;默认 hint) +## 3. 编译、编码与反射 + +循环里的 `regexp.Compile` / `MustCompile` 与 `json.Marshal` / `Unmarshal` 会重复本可一次完成的工作(当模式或 payload 不变时)。循环里的 `FieldByName`、`MethodByName`、`reflect.New` 属于 reflect lint。对具体值的 `TypeOf` 约 1.3×,不匹配。`time.Parse` 已移除:固定时间戳仅在跳过解析时更快,变化的时间戳没有可外提的东西。这些 lint 是 hint。 + +对同一路径 `os.Open` 约 2300×(相对打开一次)。`time.Now` 约 104×(相对采一次样)。该时钟规则不是已移除的 `time.Parse` 规则。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +Lint: `gp-regexp-compile-hot`, `gp-json-marshal-hot`, `gp-reflect-hot`, `gp-os-open-hot`, `gp-time-now-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| gp-make-hot | 循环内 `make`/`new` | off(G3) | +| gp-string-plus-hot | 循环内字符串 `+=` | off(G3) | +| gp-fmt-hot | 循环内 `fmt.Sprintf`/`Printf` | off(G3) | +| gp-append-nil-slice | 空 slice + 有大小提示循环里的 append | hint | +| gp-defer-hot | 循环内 `defer` | hint | +| gp-regexp-compile-hot | 循环内 `regexp.Compile` | hint(有测量;未 corpus 分级) | +| gp-json-marshal-hot | 循环内 `json.Marshal` / `Unmarshal` | hint(有测量;未 corpus 分级) | +| gp-reflect-hot | 循环内 `FieldByName` / `MethodByName` / `reflect.New` | hint(有 JSONL;不匹配 `TypeOf`) | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/goperf/docs/zh-CN/rule-ids.md b/packages/goperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..95b3187 --- /dev/null +++ b/packages/goperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,38 @@ +# goperf 规则 ID(别名) + +规范 ID 仅为 **slug**。旧地图中的数字是历史性的,**不**被接受。 + +| Slug | +|------| +| `gp-make-hot` | +| `gp-string-plus-hot` | +| `gp-fmt-hot` | +| `gp-append-nil-slice` | +| `gp-defer-hot` | +| `gp-regexp-compile-hot` | +| `gp-json-marshal-hot` | +| `gp-reflect-hot` | +| `gp-os-open-hot` | +| `gp-time-now-hot` | + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或工具包等效选项)。旧的 `*-in-loop` ID 误导了工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `gp-defer-in-loop` | `gp-defer-hot` | +| `gp-fmt-in-loop` | `gp-fmt-hot` | +| `gp-json-marshal-in-loop` | `gp-json-marshal-hot` | +| `gp-make-in-loop` | `gp-make-hot` | +| `gp-os-open-in-loop` | `gp-os-open-hot` | +| `gp-reflect-in-loop` | `gp-reflect-hot` | +| `gp-regexp-compile-in-loop` | `gp-regexp-compile-hot` | +| `gp-string-plus-in-loop` | `gp-string-plus-hot` | +| `gp-time-now-in-loop` | `gp-time-now-hot` | + +## 已移除(墓碑;永不复用) + +- `gp-time-parse-in-loop` — 见 `evidence/_removed/` + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/goperf/evidence/zh-CN/README.md b/packages/goperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..69e2cb9 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/README.md @@ -0,0 +1,5 @@ +# goperf 证据 + +每条规则 `.md` + `.jsonl` 针对原来的五条规则(gp-make-hot–gp-defer-hot),通过 `examples/bench/evidence_gp.go` 测量。`gp-regexp-compile-hot`、`gp-json-marshal-hot` 和 `gp-reflect-hot` 有 JSONL 微基准测试并保持提示状态。`gp-reflect-hot` 匹配名称查找和 `reflect.New`(一次 `TypeOf` 重跑约为 1.3×,不算作匹配)。`gp-time-parse-in-loop` 已移除。默认值保持不变:gp-make-hot–gp-fmt-hot **关闭**,gp-append-nil-slice/gp-defer-hot **提示**。 + +请参阅 [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录下的 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/goperf/evidence/zh-CN/gp-append-nil-slice.md b/packages/goperf/evidence/zh-CN/gp-append-nil-slice.md new file mode 100644 index 0000000..6a2460a --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-append-nil-slice.md @@ -0,0 +1,35 @@ +# gp-append-nil-slice — 在 nil 切片上使用 append 与 make(...,0,len) + +## 主张 + +在这台机器上的测量:主要热案例 **`append_grow`** 比率前/后约 **~7.28×**(之后更快)。默认严重性保持 **`hint`**。仅凭这一填充不要升级为 `error` —— 比率虽高,但策略/默认设置和 G3 OK-惯用历史保持严重性不变。 + +## 如何测量 + +- 测试工具: `packages/goperf/examples/bench/evidence_gp.go` +- 协议: warm=3; 取9轮的中位数; 如适用,GC/收集 +- 原始行: [`gp-append-nil-slice.jsonl`](../gp-append-nil-slice.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **go1.27.3** | +| (`presize_make`) 之后的中位数 | 0.336292 毫秒 (操作次数=500000,比例=1.000000) | +| 之前的中值(`append_grow`)| 2.448459 毫秒(操作=500000,比例=7.280753)| +| 中位数(`small_presize`之后) | 0.000167 毫秒(操作=100,比率=1.000000) | +| 之前的中值 (`small_append_grow`) | 0.000166 毫秒 (操作次数=100,比例=0.994012) | +| **主要比率 前/后** | **~7.28×** (`append_grow`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试环境。 + +## 与 lint 的对应 + +`goperf-lint` **gp-append-nil-slice** 默认保持 **`hint`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比率声明,建议使用热(非小)情况。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/goperf/evidence/zh-CN/gp-defer-hot.md b/packages/goperf/evidence/zh-CN/gp-defer-hot.md new file mode 100644 index 0000000..0404413 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-defer-hot.md @@ -0,0 +1,35 @@ +# gp-defer-hot — 在循环中延迟 vs 关闭每个(合成关闭器) + +## 主张 + +在这台机器上测量:主要热情况下 **`defer_in_loop`** 的比率前/后约为 **~46.02×**(之后更快)。默认严重性仍为 **`hint`**。仅凭这一填充**不要**将其升级为 `error` —— 比率很高,但策略/默认设置和 G3 的 OK-惯用历史保持严重性不变。 + +## 如何测量 + +- 测试工具:`packages/goperf/examples/bench/evidence_gp.go` +- 协议:warm=3;9轮的中位数;适用时进行GC/收集 +- 原始数据行:[`gp-defer-hot.jsonl`](../gp-defer-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **go1.27.3** | +| (`close_each`) 后的中位数 | 0.038000 毫秒 (操作=20000,比率=1.000000) | +| 循环中延期前的中位数 (`defer_in_loop`) | 1.748917 毫秒 (操作次数=20000, 比率=46.024132) | +| 在 (`small_close_each`) 之后的中位数 | 0.000084 毫秒 (操作数=100,比例=1.000000) | +| 中位数之前 (`small_defer_in_loop`) | 0.002209 毫秒 (操作=100,比例=26.297619) | +| **主要比率 前/后** | **~46.02×** (`defer_in_loop`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试环境。 + +## 与 lint 的对应 + +`goperf-lint` **gp-defer-hot** 默认保持 **`hint`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能概况。 +- 小行数的测试结果可能接近计时器噪声;在进行比率声明时,最好使用热点(非小规模)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 依赖环境。 diff --git a/packages/goperf/evidence/zh-CN/gp-fmt-hot.md b/packages/goperf/evidence/zh-CN/gp-fmt-hot.md new file mode 100644 index 0000000..6efd660 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-fmt-hot.md @@ -0,0 +1,35 @@ +# gp-fmt-hot — 循环中的 fmt.Sprintf 与 strconv/Builder + +## 主张 + +在这台机器上测得:主要热用例 **`fmt_sprintf_vs_itoa`** 的前/后比约为 **~2.81×**(之后更快)。默认严重性保持 **`off`**。仅凭这一填充 **不要** 升级为 `error`。 + +## 如何测量 + +- 测试工具:`packages/goperf/examples/bench/evidence_gp.go` +- 协议:预热=3;9轮的中位数;在适用的地方进行垃圾收集/收集 +- 原始文件:[`gp-fmt-hot.jsonl`](../gp-fmt-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **go1.27.3** | +| 使用 (`strconv_builder`) 后的中位数 | 5.100292 毫秒 (操作=100000,比率=1.000000) | +| `fmt_sprintf_loop` 之前的中位数 | 6.008417 毫秒 (操作=100000, 比率=1.178054) | +| `strconv_itoa_only` 之后的中位数 | 2.137083 毫秒 (操作次数=100000, 比率=1.000000) | +| `fmt_sprintf_vs_itoa` 之前的中位数 | 6.008417 毫秒 (操作次数=100000, 比例=2.811504) | +| **主要比率 前/后** | **~2.81×** (`fmt_sprintf_vs_itoa`) | + +方向和绝对毫秒数对机器/工具/选项敏感。在工具链或机器更改后,请重新运行测试工具。 + +## 与 lint 的对应 + +`goperf-lint` **gp-fmt-hot** 默认保持 **`off`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比率声明,建议使用热(非小)情况。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/goperf/evidence/zh-CN/gp-json-marshal-hot.md b/packages/goperf/evidence/zh-CN/gp-json-marshal-hot.md new file mode 100644 index 0000000..2c74dea --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-json-marshal-hot.md @@ -0,0 +1,22 @@ +# gp-json-marshal-hot — 已测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[gp-json-marshal-hot.jsonl](../gp-json-marshal-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 + +20000次对一个固定JSON对象的反序列化。反序列化一次之后。形状改变的负载在此之后不能使用。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `unmarshal_once` | 0.007416 | 20000 | 1 | +| 之前 | `unmarshal_each_iter` | 5.870792 | 20000 | 791.639 | + +go1.27.3,2026-10-23。温度3,中位数为9。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流处理。 diff --git a/packages/goperf/evidence/zh-CN/gp-make-hot.md b/packages/goperf/evidence/zh-CN/gp-make-hot.md new file mode 100644 index 0000000..39a0070 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-make-hot.md @@ -0,0 +1,35 @@ +# gp-make-hot — 每次迭代创建([]byte) vs 重用 / 单个切片 + +## 主张 + +在这台机器上的测量:主要热案例 **`make_each_iter`** 比率前/后 **~160.26×**(之后更快)。默认严重性保持 **`off`**。仅凭这一配置不要升级为 `error`——比率很高,但策略/默认设置以及 G3 OK-idiom 历史保持严重性不变。 + +## 如何测量 + +- 测试工具:`packages/goperf/examples/bench/evidence_gp.go` +- 协议:预热=3;9轮的中位数;在适用的地方进行垃圾收集/收集 +- 原始行:[`gp-make-hot.jsonl`](../gp-make-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **go1.27.3** | +| 使用 (`reuse_buf`) 后的中位数 | 0.057417 毫秒 (操作=200000,比率=1.000000) | +| 中位数之前(`make_each_iter`) | 9.201458 毫秒(操作=200000,比例=160.256684) | +| 之后的中值(`single_slice_fill`) | 0.362708 毫秒(操作=50000,比例=1.000000) | +| `make_append_slices` 之前的中位数 | 2.214292 毫秒 (操作次数=50000, 比率=6.104889) | +| **主要比率 前/后** | **~160.26×** (`make_each_iter`) | + +方向和绝对毫秒数对机器/工具/选项敏感。在工具链或机器更改后,请重新运行测试工具。 + +## 与 lint 的对应 + +`goperf-lint` **gp-make-hot** 默认保持 **`off`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比率声明,建议使用热(非小)情况。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/goperf/evidence/zh-CN/gp-os-open-hot.md b/packages/goperf/evidence/zh-CN/gp-os-open-hot.md new file mode 100644 index 0000000..59ea531 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-os-open-hot.md @@ -0,0 +1,19 @@ +# gp-os-open-hot — 测量过的,仍然暗示 + +## 状态 +**提示**。实时时钟 JSONL:[gp-os-open-hot.jsonl](../gp-os-open-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`os.Open` 加上对一个一字节文件的短读取,执行 2000 次,与只打开一次相比。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在…之后 | `open_once` | 0.031917 | 2000 | 1 | +| 之前 | `open_each_iter` | 14.717375 | 2000 | 461.114 | + +go1.27.3, 2026-10-23。温度 3,中位数 9。打开并读取一次后。 + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在进行任何默认更改之前,仍需进行语料库筛选。 diff --git a/packages/goperf/evidence/zh-CN/gp-reflect-hot.md b/packages/goperf/evidence/zh-CN/gp-reflect-hot.md new file mode 100644 index 0000000..1ff1c76 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-reflect-hot.md @@ -0,0 +1,19 @@ +# gp-reflect-hot — 测量过的,仍有暗示 + +## 状态 +**提示**. 墙钟 JSONL: [gp-reflect-hot.jsonl](../gp-reflect-hot.jsonl)。未经过语料分流。2026-10-22 的语料裁决不涵盖此标识。匹配器是 `FieldByName`、`MethodByName` 和 `reflect.New`。单独的 `TypeOf` 和 `ValueOf` 不匹配。 + +## 测量 +在每次迭代之前调用 `FieldByName("A")`。之后读取该字段。之前的 `TypeOf` 微基准测试是 1.05×。对已保存类型的公平重跑是 1.31×(200000 次操作),所以该调用被取消。对一个 int 使用 `reflect.New` 与栈零值相比大约是 41×(100000 次操作);之后的操作不分配内存。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 后 | 直接字段 | 0.061292 | 200000 | 1 | +| 之前 | 每次迭代使用 `FieldByName` | 6.220041 | 200000 | 101.482 | + +go1.27.3,2026-10-23。温度3,中位数为9。 + +名称查找是显示成本的形状。默认保持提示。 + +## 解除阻塞 +不要仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。在进行任何默认更改之前,仍然需要语料分流。 diff --git a/packages/goperf/evidence/zh-CN/gp-regexp-compile-hot.md b/packages/goperf/evidence/zh-CN/gp-regexp-compile-hot.md new file mode 100644 index 0000000..7e8e8f7 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-regexp-compile-hot.md @@ -0,0 +1,22 @@ +# gp-regexp-compile-hot — 已测量,仍然提示 + +## 状态 + +**提示**。挂钟 JSONL:[gp-regexp-compile-hot.jsonl](../gp-regexp-compile-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +固定字符串的2000次匹配。在每次迭代之前使用regexp.MustCompile;之后只编译一次。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `compile_once` | 0.076459 | 2000 | 1 | +| 之前 | `compile_each_iter` | 0.858166 | 2000 | 11.224 | + +go1.27.3,2026-10-23。温度3,中位数为9。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/goperf/evidence/zh-CN/gp-string-plus-hot.md b/packages/goperf/evidence/zh-CN/gp-string-plus-hot.md new file mode 100644 index 0000000..8ef7810 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-string-plus-hot.md @@ -0,0 +1,35 @@ +# gp-string-plus-hot — string += 与 strings.Builder + +## 主张 + +在这台机器上的测量:主要热案例 **`string_plus`** 比例前/后约 **~1481.22×**(之后更快)。默认严重性保持 **`off`**。仅凭这一填充不要升级为 `error` —— 比例虽高,但策略/默认设置及 G3 OK-idiom 历史保持严重性不变。 + +## 如何测量 + +- 测试工具:`packages/goperf/examples/bench/evidence_gp.go` +- 协议:warm=3;9轮的中位数;适用时进行GC/收集 +- 原始数据行:[`gp-string-plus-hot.jsonl`](../gp-string-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **go1.27.3** | +| 使用 (`strings_builder`) 后的中位数 | 0.217000 毫秒 (操作=50000,比率=1.000000) | +| 中位数之前 (`string_plus`) | 321.424750 毫秒 (操作=50000, 比例=1481.220046) | +| (`small_builder`) 之后的中位数 | 0.000334 毫秒 (操作次数=100,比率=1.000000) | +| 中位数之前 (`small_string_plus`) | 0.001667 毫秒 (操作次数=100,比率=4.991018) | +| **主要比例 前/后** | **~1481.22×** (`string_plus`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试套件。 + +## 与 lint 的对应 + +`goperf-lint` **gp-string-plus-hot** 默认保持 **`off`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能分析。 +- 小样本行数可能接近计时器噪声;对于比例声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)会丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/goperf/evidence/zh-CN/gp-time-now-hot.md b/packages/goperf/evidence/zh-CN/gp-time-now-hot.md new file mode 100644 index 0000000..6a2f3c5 --- /dev/null +++ b/packages/goperf/evidence/zh-CN/gp-time-now-hot.md @@ -0,0 +1,19 @@ +# gp-time-now-hot — 测量过的,仍然暗示 + +## 状态 +**提示**。实时时钟 JSONL:[gp-time-now-hot.jsonl](../gp-time-now-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`time.Now` 每次迭代与一个样本。这不是移除的 `time.Parse` 规则。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `sample_once` | 0.078625 | 100000 | 1 | +| 之前 | `sample_each_iter` | 3.337792 | 100000 | 42.452 | + +go1.27.3,2026-10-23。温度3,中位数为9。 + +在采样一次之后,并且不会在循环内部读取时钟。需要每次迭代获取新时间戳的代码没有可以提升的内容。默认保持 **hint**。这个比例并不是普遍的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/goperf/zh-CN/CONTRIBUTING.md b/packages/goperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/goperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/goperf/zh-CN/README.md b/packages/goperf/zh-CN/README.md new file mode 100644 index 0000000..33b9208 --- /dev/null +++ b/packages/goperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# goperf + +Go 性能笔记、小示例,以及用 **Go** 编写的 `goperf-lint`。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**(G3 2026-10-22)。默认配置:gp-make-hot–gp-fmt-hot **关闭**,gp-append-nil-slice/gp-defer-hot **提示**。没有虚构的 JSONL。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/go-performance-guide.md) — 来自 `docs/guide-src/` +- [`goperf-lint`](../goperf-lint/README.md) — gp-make-hot–gp-defer-hot +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +带有大小提示的空切片增长(`make([]T, 0, n)`),循环中的 `defer` 堆积。旧的 make/string/fmt 循环启发式在 G3 OK-idiom 降级后仍为可选。 + +## 运行 lint + +```sh +cd packages/goperf/goperf-lint +go build -o goperf-lint ./cmd/goperf-lint +./goperf-lint path/to/sources +./goperf-lint path/to/sources --severity gp-append-nil-slice=error +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅当存在**错误**严重性的发现时才退出 `1`(提示信息会打印,但除非使用 `--strict-hints`,否则退出码为 `0`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ per-rule claims + measured JSONL +examples/ tiny Go demos +goperf-lint/ Go checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/jsperf/docs/guide-src/00_head.md b/packages/jsperf/docs/guide-src/00_head.md index ae4947a..85a8cab 100644 --- a/packages/jsperf/docs/guide-src/00_head.md +++ b/packages/jsperf/docs/guide-src/00_head.md @@ -1,20 +1,14 @@ # JavaScript performance guide (v0) -Stable shapes, fewer per-iteration allocations — plus a separate chapter on tooling / cold-start. +Stable shapes, fewer per-iteration allocations — plus a separate chapter on tooling and cold-start. -> **Focus: modern Node and V8 ICs / GC.** QuickJS and 小程序-like embeds get a dedicated chapter (§3) — avoid advice that only makes sense with a DOM or a full browser profiler. **§4 deepens V8** (maps, elements kinds, deopts, Ignition→Maglev/Turbofan); it does **not** apply to §3 runtimes. **§5** covers **startup and tooling** (module resolution, eager graphs, barrels) after Marvin Hagemeister’s ecosystem series — orthogonal to the jp-* lint rules. +> **Focus: modern Node and V8 (inline caches / GC).** QuickJS and 小程序-like embeds get their own chapter (§3) — skip advice that needs a DOM or a full browser profiler. §4 is V8-only (maps, elements kinds, deopts, Ignition → Maglev/Turbofan) and does not apply to §3 runtimes. §5 covers startup and tooling (module resolution, eager graphs, barrels) after Marvin Hagemeister’s ecosystem series; that cost sits outside the jp-* lint rules. > -> Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. §4.6 cites single-machine microbench medians only. §5 cites Marvin’s **methods** (URLs); re-measure his timings locally. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. §4.6 cites single-machine microbench medians only. §5 cites Marvin’s methods (URLs); re-measure his timings locally. > -> Companion: `jsperf-lint/` — the jp-* rules, see the [Lint map](#lint-map) (line windows; not a full parser). Dogfooded on Node `lib/`, Preact `src/`, quickjs `*.js` (tests skipped), and TypeScript `packages/typescript/src` (2026-10-21 Asia/Shanghai). See [corpus.md](corpus.md). Startup/FS cost is **out of band** for the jp-* rules — see §5. +> Companion: `jsperf-lint/` — the jp-* rules, see the [Lint map](#lint-map) (line windows; not a full parser). Run against Node `lib/`, Preact `src/`, quickjs `*.js` (tests skipped), and TypeScript `packages/typescript/src` (2026-10-21 Asia/Shanghai). See [corpus.md](corpus.md). Startup and FS cost are out of band for the jp-* rules — see §5. -## How to read this - -1. Profile first (`node --prof`, `node --cpu-prof` + speedscope, Chrome Performance, clinic). Lint findings are review prompts. -2. Prefer consistent object shapes and batch allocation over micro-optimizing single property access. -3. Embed / 小程序 runtimes: skip DOM-only tips; watch alloc + `delete` the same way (§3). Do **not** apply Turbofan/Maglev advice there. -4. On Node/Chrome, use §4 for elements kinds, IC polymorphism, and deopt checks — then measure. -5. For CLIs, lint/test runners, and package cold-start, use §5 (resolution, lazy `require`, barrels) — not the jp-* rules alone. +Profile first (`node --prof`, `node --cpu-prof` + speedscope, Chrome Performance, clinic). Prefer consistent object shapes and batch allocation over micro-optimizing a single property access. On embeds / 小程序, skip DOM tips and do not apply Turbofan/Maglev advice. On Node/Chrome, use §4 for elements kinds, IC polymorphism, and deopt checks. For CLIs, lint/test runners, and package cold-start, use §5. ## Contents @@ -24,6 +18,7 @@ Stable shapes, fewer per-iteration allocations — plus a separate chapter on to - [3. Embed and 小程序](#3-embed-and-小程序) - [4. V8 optimizations (Node / Chrome)](#4-v8-optimizations-node--chrome) - [5. Startup and tooling (cold paths)](#5-startup-and-tooling-cold-paths) +- [6. Constructors, JSON, DOM queries, and date formats](#6-constructors-json-dom-queries-and-date-formats) - [Lint map](#lint-map) - [References](#references) @@ -31,15 +26,12 @@ Stable shapes, fewer per-iteration allocations — plus a separate chapter on to ## 0. Scope and method -1. **Shapes first** — initialize fields in one order; avoid polymorphic hot paths (§1, §4.1). -2. **Alloc pressure** — `.push({…})`, `new`, non-empty object binders; default scope reports these outside loops too (§1). -3. **`delete`** — prefer tombstones or parallel structures on hot objects (§2); avoid `delete arr[i]` holes (§4.2, jp-delete-array-hole). -4. **Embed** — same alloc/shape rules without assuming `window` / DOM or V8 tiers (§3). -5. **V8 Node/Chrome** — elements kinds, IC state, deopts, pipeline sketch (§4). -6. **Startup / tooling** — profile with `--cpu-prof`; cut FS/resolution; lazy-load rare paths; watch barrels (§5). -7. **Measure** — `--prof` / `--cpu-prof` / `--trace-deopt` / Performance panel / `examples/bench` before rewriting. +1. Shapes first — initialize fields in one order; avoid polymorphic hot paths (§1, §4.1). +2. Alloc pressure — `.push({…})`, `new`, non-empty object binders; default scope reports these outside loops too (§1). +3. `delete` — prefer tombstones or parallel structures on hot objects (§2); avoid `delete arr[i]` holes (§4.2, `jp-delete-array-hole`). +4. Embed — same alloc/shape rules without assuming `window` / DOM or V8 tiers (§3). +5. V8 on Node/Chrome — elements kinds, IC state, deopts, pipeline sketch (§4). +6. Startup / tooling — profile with `--cpu-prof`; cut FS/resolution; lazy-load rare paths; watch barrels (§5). +7. Measure — `--prof` / `--cpu-prof` / `--trace-deopt` / Performance panel / `examples/bench` before rewriting. Sources: [XPerf playbook](../../../docs/playbook.md) · [corpus.md](corpus.md) · [Marvin series](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) - ---- - diff --git a/packages/jsperf/docs/guide-src/01_allocation_shapes.md b/packages/jsperf/docs/guide-src/01_allocation_shapes.md index 94ba51c..29a6ac7 100644 --- a/packages/jsperf/docs/guide-src/01_allocation_shapes.md +++ b/packages/jsperf/docs/guide-src/01_allocation_shapes.md @@ -24,7 +24,7 @@ Prefer typed arrays (`examples/p01_typed_array.js`), object pools, or preallocat **Evidence (this machine):** [`evidence/jp-alloc-hot`](../evidence/jp-alloc-hot.md) — `new Uint8Array` in-loop vs once **~19×**; `.push({i})` vs prealloc fill **~6.4×** (Node v20.19.2). Mechanism is alloc/GC (STATIC-VS-V8 PARTIAL for deopt). Default severity is **off** (`--preset all` or `--severity jp-alloc-hot=hint` enables it) — see [evidence-policy](evidence-policy.md). Default **`--scope all`** reports these patterns outside loops too; `--scope loop` keeps the former in-loop-only match. -### 1.3 Corpus dogfood (heuristic) +### 1.3 Corpus hit counts Counts below come from earlier line-window revisions of the linter (r1–r3) and show how the heuristics were narrowed; current corpus results are in [corpus.md](corpus.md). @@ -73,7 +73,7 @@ On this machine (same Node as §1.5), both sides build the result array: The 3-row filter-then-map case is under 2×. Four passes on 3 rows is above 2×, and 20000 calls still finish in under a millisecond. The 35000-row cases are where one pass is clearly faster. The hint names that length. Rewriting a short list is optional. -Lint: **jp-map-filter-chain** — default **hint**. `//` comments and block comments are stripped before the match. Codegen `gen-*.js` files are path-skipped. Evidence: [`jp-map-filter-chain`](../evidence/jp-map-filter-chain.md). +Lint: `jp-map-filter-chain` — default hint. `//` comments and block comments are stripped before the match. Codegen `gen-*.js` files are path-skipped. Evidence: [`jp-map-filter-chain`](../evidence/jp-map-filter-chain.md). --- diff --git a/packages/jsperf/docs/guide-src/02_delete_and_shape.md b/packages/jsperf/docs/guide-src/02_delete_and_shape.md index 186cd3b..192b87d 100644 --- a/packages/jsperf/docs/guide-src/02_delete_and_shape.md +++ b/packages/jsperf/docs/guide-src/02_delete_and_shape.md @@ -8,7 +8,7 @@ - A parallel `Set` / bitset / dense side table for “present?” when absence is common. - Rebuild a new object in a cold path instead of deleting in a hot loop. -Lint: **jp-delete-shape** — `delete` on a member expression (file-level heuristic; not loop-only by default). Measured gate: [`evidence/jp-delete-shape`](../evidence/jp-delete-shape.md) (~**474×** vs tombstone on Node v20.19.2); the JSONL agrees, but corpus triage found the hits were almost all OK-idiom (see [evidence-policy](evidence-policy.md)), so the default is **off** (`--preset all` or `--severity jp-delete-shape=error` enables it). +Lint: `jp-delete-shape` (`delete` on a member expression; file-level heuristic, not loop-only by default). Measured gate: [`evidence/jp-delete-shape`](../evidence/jp-delete-shape.md) (~474× vs tombstone on Node v20.19.2); the JSONL agrees, but corpus triage found the hits were almost all OK-idiom (see [evidence-policy](evidence-policy.md)), so the default is off (`--preset all` or `--severity jp-delete-shape=error` enables it). ### 2.2 Intentional `delete` (bootstrap / feature stripping) @@ -16,7 +16,7 @@ Node’s `lib/internal/bootstrap/**` uses `delete` for module cache and feature **jsperf-lint:** skips files under `internal/bootstrap/`, filenames matching `*bootstrap*`, and inspect/debugger/test-runner paths for jp-delete-shape and jp-delete-array-hole. -**Evidence (dogfood):** Node `lib/` jp-delete-shape **27 → 21** after bootstrap skip (r1→r2); held at **21** in r3 while jp-alloc-hot moved. Remaining deletes need case-by-case review (CLI teardown, rare maps). +Corpus hits: Node `lib/` jp-delete-shape **27 → 21** after bootstrap skip (r1→r2); held at **21** in r3 while jp-alloc-hot moved. Remaining deletes need case-by-case review (CLI teardown, rare maps). ### 2.3 Optional future diff --git a/packages/jsperf/docs/guide-src/03_embed_and_miniprogram.md b/packages/jsperf/docs/guide-src/03_embed_and_miniprogram.md index ac6286e..e0e3d98 100644 --- a/packages/jsperf/docs/guide-src/03_embed_and_miniprogram.md +++ b/packages/jsperf/docs/guide-src/03_embed_and_miniprogram.md @@ -6,19 +6,19 @@ QuickJS, Hermes-like embeds, and 小程序 JS runtimes still pay for: - Per-iteration object allocation (jp-alloc-hot patterns). - Shape changes via `delete` or divergent property order. -- Logging in tight loops (**jp-console-hot** — `console.log` / `console.debug`). Evidence: [`jp-console-hot`](../evidence/jp-console-hot.md) (~**1300×** vs quiet even with discarded stdout) — default **off** (`app`) / **hint** (`engine`), not a CI error. +- Logging in tight loops (`jp-console-hot` — `console.log` / `console.debug`). Evidence: [`jp-console-hot`](../evidence/jp-console-hot.md) (~1300× vs quiet even with discarded stdout) — default off (`app`) / hint (`engine`), not a CI error. They often **lack** a browser Performance panel, DOM, or large IC budgets. Advice that starts with “open DevTools → …” does not transfer. Prefer: - Alloc/shape discipline from §1–§2. - Batching work (`examples/p02_batch_note.js`) instead of per-item microtasks when the host exposes a batch API. -- Measuring with the **host** profiler (Node `--prof`, QuickJS timing hooks, vendor 小程序 perf panels) — not Chrome-only workflows. +- Measuring with the host profiler (Node `--prof`, QuickJS timing hooks, vendor 小程序 perf panels) — not Chrome-only workflows. -**Do not** apply §4 (Maglev/Turbofan, elements-kind transitions, `--trace-deopt`) to these runtimes. Stable shapes and fewer allocs still help; V8 tier talk does not. +Do not apply §4 (Maglev/Turbofan, elements-kind transitions, `--trace-deopt`) to these runtimes. Stable shapes and fewer allocations still help; V8 tier talk does not. ### 3.2 Shared quickjs tree -cperf dogfoods the **C** engine; jsperf dogfoods the `*.js` beside it (tests and `gen-*` skipped). The current rules report **0** findings there ([corpus.md](corpus.md)); earlier revisions reported 2 jp-alloc-hot. Low pressure, but useful as an embed-adjacent sample. Do not expect Preact-level cleanliness on generator scripts. +cperf covers the C engine; jsperf covers the `*.js` beside it (tests and `gen-*` skipped). The current rules report 0 findings there ([corpus.md](corpus.md)); earlier revisions reported 2 jp-alloc-hot. Low pressure, but useful as an embed-adjacent sample. Do not expect Preact-level cleanliness on generator scripts. ### 3.3 What not to recommend here diff --git a/packages/jsperf/docs/guide-src/04_v8_optimizations.md b/packages/jsperf/docs/guide-src/04_v8_optimizations.md index 8f3a815..8fa193c 100644 --- a/packages/jsperf/docs/guide-src/04_v8_optimizations.md +++ b/packages/jsperf/docs/guide-src/04_v8_optimizations.md @@ -1,12 +1,12 @@ ## 4. V8 optimizations (Node / Chrome) -This chapter deepens §1 for **V8** specifically. It does **not** apply to QuickJS, Hermes-like embeds, or 小程序 runtimes — see §3. Those engines share “stable shapes / fewer allocs” physics; they do **not** get Ignition → Maglev/Turbofan advice, elements-kind transitions, or `--trace-deopt` workflows. +This chapter is V8-only. It does not apply to QuickJS, Hermes-like embeds, or 小程序 runtimes — see §3. Those engines still benefit from stable shapes and fewer allocations; they do not get Ignition → Maglev/Turbofan advice, elements-kind transitions, or `--trace-deopt` workflows. -Dated as a **Node 22+ / modern V8 continuum** (measured on Node v22.22.3, V8 12.4.x). Pipeline tier names and flags drift; treat version pins below as “what we ran,” not forever guarantees. +Measured on Node v22.22.3 (V8 12.4.x). Pipeline tier names and flags drift; treat the pins below as what we ran, not forever guarantees. ### 4.1 Hidden classes / maps and ICs (extends §1.1) -V8 attaches a **map** (hidden class) to each object. Property addition order and type feedback build a transition tree. Property loads/stores go through **inline caches (ICs)**: +V8 attaches a map (hidden class) to each object. Property addition order and type feedback build a transition tree. Property loads and stores go through inline caches (ICs): | IC state | Meaning (practical) | |----------|---------------------| @@ -20,7 +20,7 @@ Microbench: `examples/bench/shape_mono_vs_poly.js` (same-order vs divergent cons ### 4.2 Elements kinds -Arrays are not one representation. V8 tracks **elements kinds**, roughly: +Arrays are not one representation. V8 tracks elements kinds, roughly: | Kind (concept) | Typical contents | |----------------|------------------| @@ -36,13 +36,13 @@ Arrays are not one representation. V8 tracks **elements kinds**, roughly: - For numeric hot loops, prefer **typed arrays** (`Float64Array`, `Uint32Array`, …): fixed element type, no holey transition, contiguous storage. Ordinary `number[]` can stay packed, but mixed Smi/double transitions and boxing still cost. - Reading past `length` or relying on holes as “optional slots” is a shape smell; use a parallel length, a typed bitmask, or `undefined` tombstones on a **packed** array if you must. -Lint: **jp-delete-array-hole** flags `delete arr[i]`-style member deletes (holey risk); measured gate [`evidence/jp-delete-array-hole`](../evidence/jp-delete-array-hole.md) (~**3.2×**). jp-delete-shape covers `delete obj.key` on ordinary members; it historically missed computed index deletes — jp-delete-array-hole closes that gap. +Lint: `jp-delete-array-hole` flags `delete arr[i]`-style member deletes (holey risk); measured gate [`evidence/jp-delete-array-hole`](../evidence/jp-delete-array-hole.md) (~3.2×). jp-delete-shape covers `delete obj.key` on ordinary members; it historically missed computed index deletes — jp-delete-array-hole closes that gap. Microbenches: `examples/bench/elements_packed_vs_holey.js`, `examples/bench/typed_array_vs_number_array.js`. ### 4.3 Deopts — when they matter; how to check -A **deopt** throws optimized code back to a slower tier because assumptions broke (unexpected map, elements kind, bounds, etc.). Occasional deopts at startup or on rare paths are normal. Deopts **matter** when they sit on a profiled hot function and repeat every iteration. +A deopt throws optimized code back to a slower tier because assumptions broke (unexpected map, elements kind, bounds, etc.). Occasional deopts at startup or on rare paths are normal. Deopts matter when they sit on a profiled hot function and repeat every iteration. **Static jsperf-lint cannot detect JIT outcomes.** IC state, deopt, and optimization tier are runtime facts. The jp-* rules only flag allocation/shape *smells*. JIT verification = profile / `--trace-deopt` / bench — never “the linter said so.” @@ -61,7 +61,7 @@ On this machine (Node v22.22.3), `deopt_probe` warmed `hotLoad` monomorphic then `[bailout … reason: wrong map … JSFunction hotLoad … Code TURBOFAN]` -One-shot `-e` scripts often also print “Insufficient type feedback …” — treat that as **startup noise**, not proof your smell is hot. +One-shot `-e` scripts often also print “Insufficient type feedback …” — treat that as startup noise, not proof the smell is hot. **Check (Chrome):** Performance panel → V8 markers where available; correlate with wall-clock before/after. No fake speedup percentages. @@ -69,11 +69,11 @@ One-shot `-e` scripts often also print “Insufficient type feedback …” — High level, modern V8/Node: -1. **Ignition** — bytecode interpreter; gathers type feedback. -2. **Maglev** — mid-tier optimizing compiler (present on current Node 22+ / recent V8 builds; exact enablement can depend on flags and workload). -3. **Turbofan** — top-tier optimizing compiler for hot code. +1. Ignition — bytecode interpreter; gathers type feedback. +2. Maglev — mid-tier optimizing compiler (present on current Node 22+ / recent V8 builds; exact enablement can depend on flags and workload). +3. Turbofan — top-tier optimizing compiler for hot code. -Code may climb tiers as it heats up and fall back on deopt. **Do not** overclaim “always Maglev on Node N” or pin exact tier thresholds — they move. For embeds without this pipeline (§3), skip tier talk entirely. +Code may climb tiers as it heats up and fall back on deopt. Do not claim “always Maglev on Node N” or pin exact tier thresholds — they move. For embeds without this pipeline (§3), skip tier talk entirely. ### 4.5 What jsperf-lint can and cannot see @@ -88,11 +88,11 @@ Code may climb tiers as it heats up and fall back on deopt. **Do not** overclaim | jp-string-plus-in-loop | removed | `push` + `join` was not about 2× on the matched `+=` shapes | | jp-map-filter-chain | chained `.map` / `.filter` / `.flatMap` on one line | Array length; a 3-row chain and a 35000-row chain are different costs | -Lint = allocation/shape smells. JIT verification = `jit-check.sh` / `--trace-deopt` / `examples/bench`. Polymorphic ICs leave **no** jp-* fingerprint. +Lint = allocation/shape smells. JIT verification = `jit-check.sh` / `--trace-deopt` / `examples/bench`. Polymorphic ICs leave no jp-* signal. ### 4.6 Measured on this machine (Node v22.22.3) -Ran 2026-10-21 18:49 Asia/Shanghai via `scripts/run-v8-benches.sh` → `examples/bench/out/`. Node **v22.22.3**, V8 **12.4.254.21-node.56**, linux x64. Warm-up 3 + 9 rounds; medians below. **Single-machine numbers only** — not a cross-machine claim. +Ran 2026-10-21 18:49 Asia/Shanghai via `scripts/run-v8-benches.sh` → `examples/bench/out/`. Node v22.22.3, V8 12.4.254.21-node.56, linux x64. Warm-up 3 + 9 rounds; medians below. Single-machine numbers only — not a cross-machine claim. | Bench | Variant | N | Median ms | Ops/s (approx.) | |-------|---------|---|-----------|-----------------| @@ -110,7 +110,7 @@ Full JSONL: `examples/bench/out/results.jsonl` (written by the script). ### 4.7 Correlation: static smell vs V8 time (PARTIAL) -Experiment: `examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh` (Node **v22.22.3**). +Experiment: `examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh` (Node v22.22.3). | Case | Static smell? | Ratio (smell/clean) | Agree? | |------|---------------|---------------------|--------| diff --git a/packages/jsperf/docs/guide-src/05_startup_and_tooling.md b/packages/jsperf/docs/guide-src/05_startup_and_tooling.md index 309ed4f..d01d24b 100644 --- a/packages/jsperf/docs/guide-src/05_startup_and_tooling.md +++ b/packages/jsperf/docs/guide-src/05_startup_and_tooling.md @@ -1,8 +1,8 @@ ## 5. Startup and tooling (cold paths) -This chapter is about **tooling / cold-start / module-graph** cost — not the hot-loop shape and allocation smells in §1–§2 / §4. +This chapter is about tooling, cold-start, and module-graph cost — not the hot-loop shape and allocation smells in §1–§2 / §4. -Primary source: Marvin Hagemeister’s series **[Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)** (parts 1–13). Methods below are paraphrased; **do not treat his wall-clock numbers as ours** — re-measure on your machine. +Primary source: Marvin Hagemeister’s series [Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) (parts 1–13). Methods below are paraphrased; do not treat his wall-clock numbers as ours — re-measure on your machine. | Concern | Chapters | What finds it | |---------|----------|---------------| @@ -15,9 +15,9 @@ Before rewriting a library or CLI: 1. Capture a CPU profile: `node --cpu-prof your-tool.js …` (or `node --cpu-prof $(which npm) run …` for npm scripts — see [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/)). 2. Open the `.cpuprofile` in [speedscope](https://www.speedscope.app/) (or Chrome DevTools). -3. Ask: is time in **your** hot function, or in **loading / resolving** modules, casting, regex, FS? +3. Ask: is time in your hot function, or in loading / resolving modules, casting, regex, FS? -Marvin’s recurring method ([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)): find a top frame → open source → remove avoidable work → re-profile. Same loop as XPerf’s “measure before rewrite,” aimed at **build/lint/CLI** rather than app render loops. +Marvin’s recurring method ([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)): find a top frame → open source → remove avoidable work → re-profile. Same loop as XPerf’s “measure before rewrite,” aimed at build/lint/CLI rather than app render loops. ### 5.2 Hot casts, short-string regex, inner functions (part 1 methods) @@ -28,18 +28,18 @@ Paraphrased patterns from [part 1](https://marvinh.dev/blog/speeding-up-javascri - **Functions created inside hot outer functions** — closures recreated every call hurt ICs; lift helpers or pass args explicitly instead of capturing. - **Transpiler artifacts** — e.g. `for…of` rewritten into a fresh `_loop` function per iteration; inspect **published** output, not only source. -These can overlap §4 (ICs) when the path is hot at runtime. In tooling they often dominate **startup or per-file** cost instead. +These can overlap §4 (ICs) when the path is hot at runtime. In tooling they often dominate startup or per-file cost instead. ### 5.3 Module resolution (part 2 methods) From [part 2](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2/): resolution can outweigh parsing in lint/test/build traces. -Adopt as checklist (paraphrase of his four takeaways): +His four takeaways, paraphrased: -1. **Avoid FS** — every `stat`/`exists` is expensive at scale. -2. **Cache** resolved paths (watch mode: invalidate carefully). -3. **`fs.statSync(path, { throwIfNoEntry: false })`** — avoid throwing + capturing stack traces for “file missing” (ENOENT path). -4. **Limit upward `node_modules` walks** — especially when path aliases are mistaken for bare package imports. +1. Avoid FS — every `stat`/`exists` is expensive at scale. +2. Cache resolved paths (watch mode: invalidate carefully). +3. `fs.statSync(path, { throwIfNoEntry: false })` — avoid throwing + capturing stack traces for “file missing” (ENOENT path). +4. Limit upward `node_modules` walks — especially when path aliases are mistaken for bare package imports. Also watch **extension guessing** (`.js` → `.jsx` → … → `.ts`) and alias plugins that do not share the same resolver as TypeScript/`tsconfig` paths. @@ -59,7 +59,7 @@ From [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/) | | Runtime chapters (§1–§2, §4) | This chapter (§5) | |--|----------------------------|-------------------| | Typical win | Hot loop IC / GC / elements kind | Cold start, `npm run`, lint/test graph | -| Lint | the jp-* rules (line-window heuristics) | **None today** — document-only companions | +| Lint | the jp-* rules (line windows) | none today — document-only companions | | Evidence | `examples/bench/*`, `--trace-deopt` | `--cpu-prof` + speedscope; require-graph size | Do **not** interpret a clean jsperf-lint self-lint as “startup is fine.” Do **not** treat Marvin’s published speedups as transferable without a local profile. diff --git a/packages/jsperf/docs/guide-src/06_hot_calls.md b/packages/jsperf/docs/guide-src/06_hot_calls.md index e588502..45bfbf6 100644 --- a/packages/jsperf/docs/guide-src/06_hot_calls.md +++ b/packages/jsperf/docs/guide-src/06_hot_calls.md @@ -1,6 +1,6 @@ ## 6. Constructors, JSON, DOM queries, and date formats -Each hint below is one measured shape. The hint line states that premise. A call that only looks similar is not given that rewrite. Default **`--scope all`** reports these smells everywhere — extreme stacks treat drip-by-drip cost as in-scope even off a loop. Opt in to **`--scope loop`** for the former in-loop-only match. Messages do not tell readers or models that non-loop sites can be ignored. +Each hint below is one measured shape. The hint line states that premise. A call that only looks similar is not given that rewrite. Default `--scope all` reports these smells everywhere — extreme stacks treat drip-by-drip cost as in-scope even off a loop. Opt in to `--scope loop` for the former in-loop-only match. Messages do not tell readers or models that non-loop sites can be ignored. `new RegExp(pattern)` compiles on every call when the pattern stays the same. A regex literal is cached. If the pattern is the loop variable, or `parts[i]`, the checker does not say to compile once. The bench compiled one fixed pattern, about 4.4× on Node 26. @@ -12,8 +12,8 @@ Each hint below is one measured shape. The hint line states that premise. A call `Date.now` was about 26× and `performance.now` about 19× versus one sample, in a tight loop that did not wait. A loop that contains `await`, or a `for await` loop, is not reported for the clock rule. Sample once when a fresh time is not required each call. A rate limiter that must read the clock each pass has nothing to move. -**jp-regex-ctor-hot** and **jp-json-hot** stay **hint** on the app and engine presets. JSONL exists for those two, and the 2026-10-22 verdict does not cover them. **jp-dom-query-hot** (about 16× on Node 26, a tiny linkedom document) and **jp-intl-datetimeformat-hot** (about 36× on Node 26, both sides still format) have JSONL and stay **hint**. They are outside that verdict. `--preset all` is error for every rule that still exists. +`jp-regex-ctor-hot` and `jp-json-hot` stay hint on the app and engine presets. JSONL exists for those two, and the 2026-10-22 verdict does not cover them. `jp-dom-query-hot` (about 16× on Node 26, a tiny linkedom document) and `jp-intl-datetimeformat-hot` (about 36× on Node 26, both sides still format) have JSONL and stay hint. They are outside that verdict. `--preset all` is error for every rule that still exists. `Reflect.get` was about 8× and was not given a rule. -Companion lint: **jp-regex-ctor-hot**, **jp-json-hot**, **jp-dom-query-hot**, **jp-intl-datetimeformat-hot**, **jp-readfile-sync-hot**, **jp-clock-hot**. +Lint: `jp-regex-ctor-hot`, `jp-json-hot`, `jp-dom-query-hot`, `jp-intl-datetimeformat-hot`, `jp-readfile-sync-hot`, `jp-clock-hot` (default hint). diff --git a/packages/jsperf/docs/guide-src/99_tail.md b/packages/jsperf/docs/guide-src/99_tail.md index 09557b7..734a3e8 100644 --- a/packages/jsperf/docs/guide-src/99_tail.md +++ b/packages/jsperf/docs/guide-src/99_tail.md @@ -1,4 +1,4 @@ -### Lint map +## Lint map | ID | Topic | Severity | Evidence / note | |----|--------|----------|-----------------| @@ -15,9 +15,9 @@ | jp-clock-hot | `Date.now` / `performance.now` | **hint** | Node 26 about 70× / 110× with no `await`; sample once when fresh time is not required | | *(none)* | module resolution / eager graph / barrels | — | §5 — use `--cpu-prof` | -Severities above are the default `app` preset. Matching scope defaults to **`--scope all`** (everywhere); `--scope loop` restores in-loop-only matching. Policy: [`evidence-policy.md`](evidence-policy.md). Fixtures: `jsperf-lint/fixtures/{bad,good}/`. +Severities above are the default `app` preset. Matching scope defaults to `--scope all` (everywhere); `--scope loop` restores in-loop-only matching. Policy: [`evidence-policy.md`](evidence-policy.md). Fixtures: `jsperf-lint/fixtures/{bad,good}/`. -### References +## References - [corpus.md](corpus.md) - `examples/bench/` · `scripts/run-v8-benches.sh` · `scripts/jit-check.sh` · `scripts/run-static-vs-v8.sh` · `scripts/regen-evidence.sh` · `examples/bench/require_cost_sketch.js` diff --git a/packages/jsperf/docs/guide-src/zh-CN/00_head.md b/packages/jsperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..7290587 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,39 @@ +# JavaScript 性能指南(v0) + +稳定的 shape、更少的每轮分配——另有一章讲工具链 / 冷启动。 + +> **焦点:现代 Node 与 V8(inline cache / GC)。** QuickJS 与小程序类嵌入有专章(§3)——跳过需要 DOM 或完整浏览器 profiler 的建议。§4 只讲 V8(map、elements kind、deopt、Ignition → Maglev/Turbofan),不适用于 §3 运行时。§5 讲启动与工具链(模块解析、急切图、barrel),依据 Marvin Hagemeister 的生态系列;这类成本不在 jp-* 规则里。 +> +> 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。§4.6 只引用单机微基准中位数。§5 引用 Marvin 的方法(URL);他的计时请在本地重测。 +> +> 配套:`jsperf-lint/` —— jp-* 规则,见 [Lint 对照表](#lint-对照表)(行窗;非完整解析器)。已在 Node `lib/`、Preact `src/`、quickjs `*.js`(跳过测试)以及 TypeScript `packages/typescript/src` 上跑过(2026-10-21 Asia/Shanghai)。见 [corpus.md](corpus.md)。启动与 FS 成本对 jp-* 规则是带外的——见 §5。 + +先做 profile(`node --prof`、`node --cpu-prof` + speedscope、Chrome Performance、clinic)。优先一致的对象 shape 与批量分配,而不是抠单次属性访问。嵌入 / 小程序跳过 DOM 提示,也不要套用 Turbofan/Maglev。在 Node/Chrome 上用 §4 看 elements kind、IC 多态与 deopt。对 CLI、lint/测试跑手与包冷启动用 §5。 + +## 目录 + +- [0. 范围与方法](#0-范围与方法) +- [1. 分配与 shape](#1-分配与-shape) +- [2. `delete` 与 shape](#2-delete-与-shape) +- [3. 嵌入与小程序](#3-嵌入与小程序) +- [4. V8 优化(Node / Chrome)](#4-v8-优化node--chrome) +- [5. 启动与工具链(冷路径)](#5-启动与工具链冷路径) +- [6. 构造、JSON、DOM 查询与日期格式](#6-构造jsondom-查询与日期格式) +- [Lint 对照表](#lint-对照表) +- [参考](#参考) + +--- + +## 0. 范围与方法 + +1. 先管 shape —— 按同一顺序初始化字段;避免多态热路径(§1、§4.1)。 +2. 分配压力 —— `.push({…})`、`new`、非空对象绑定;默认作用域在循环外也会报(§1)。 +3. `delete` —— 热对象上优先墓碑或并行结构(§2);避免 `delete arr[i]` 打洞(§4.2、jp-delete-array-hole)。 +4. 嵌入 —— 同样的分配/shape 规则,不假设 `window` / DOM 或 V8 分层(§3)。 +5. V8 Node/Chrome —— elements kind、IC 状态、deopt、流水线素描(§4)。 +6. 启动 / 工具链 —— 用 `--cpu-prof` profile;削减 FS/解析;惰性加载稀有路径;留意 barrel(§5)。 +7. 测量 —— 重写前用 `--prof` / `--cpu-prof` / `--trace-deopt` / Performance 面板 / `examples/bench`。 + +来源:[XPerf playbook](../../../../docs/zh-CN/playbook.md) · [corpus.md](corpus.md) · [Marvin 系列](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/01_allocation_shapes.md b/packages/jsperf/docs/guide-src/zh-CN/01_allocation_shapes.md new file mode 100644 index 0000000..0fe2903 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/01_allocation_shapes.md @@ -0,0 +1,78 @@ +## 1. 分配与 shape + +### 1.1 Hidden class(shape) + +引擎(V8 及同类)按属性添加顺序分配 **hidden class** / map。热循环里分叉的 shape 会迫使多态或 megamorphic IC。 + +- 按一致顺序初始化全部字段(`return { x, y, z: 0 }`)。 +- 避免先加不同属性的 `if` 分支。 +- 见 `examples/m01_hidden_class.js`(示意;无计时声明)。 + +### 1.2 循环内分配(jp-alloc-hot) + +每轮分配会在循环很热时制造垃圾与 GC 停顿: + +| 模式 | Lint? | 备注 | +|------|--------|------| +| `.push({…})` / `.push([…])` | 是 | 经典 builder 噪声 | +| 循环内非空 `{…}` / `[…]` 绑定 | 是 | 每轮逃逸的 options bag | +| 循环内 `new Ctor` / `Object.create` | 是(r2+) | 除外 `throw new` / `Error` / `ERR_*` | +| 空 `{}` / `[]` 绑定 | **否**(r2) | 每重试一次的 opts bag 在 Node 上是噪声 | +| `throw new …` | 否 | 错误路径 | + +当 profile 显示有 GC 时间时,优先 typed array(`examples/p01_typed_array.js`)、对象池或预分配结构——不要默认改掉每一个 `.push`。 + +**证据(本机):** [`evidence/jp-alloc-hot`](../../evidence/zh-CN/jp-alloc-hot.md) —— 循环内 `new Uint8Array` 对比一次 **~19×**;`.push({i})` 对比预分配填充 **~6.4×**(Node v20.19.2)。机制是分配/GC(STATIC-VS-V8 对 deopt 为 PARTIAL)。默认严重级别 off(`--preset all` 或 `--severity jp-alloc-hot=hint` 开启)——见 [evidence-policy](evidence-policy.md)。默认 **`--scope all`** 在循环外也报;`--scope loop` 恢复仅循环内匹配。 + +### 1.3 语料命中(lint 计数) + +下表来自 lint 行窗的较早修订(r1–r3),展示启发式如何收窄;当前语料结果见 [corpus.md](corpus.md)。 + +| 语料 | r1 → r2 → r3 | 备注 | +|------|--------------|------| +| Node `lib/` | 60 → 179 → **115**(jp-alloc-hot **33 → 158 → 94**;jp-delete-shape 27 → 21 → 21) | r2 有意纳入 `new` / 非空绑定抬高了 jp-alloc-hot;r3 注释/`for` 误报削减。残留:`inspect.js` `new SafeMap()`(~16)、stream `pull.js` / `share.js` / `broadcast.js` 的 `IterResult` / `Uint8Array`、`abort_controller.js` 真实 `for` 里的 `??= new SafeSet()` | +| Preact `src/` | **0 → 0 → 0** | 干净基线——对框架核心规则可能偏窄(很少 `.push({` / 循环 `new`) | +| quickjs `*.js` | 5 → 4 → **2** jp-alloc-hot | 跳过 tests/;剩 codegen 味脚本 | +| TypeScript `packages/typescript/src` | — → 47 → **15** jp-alloc-hot | Go 迁移后 tip(~50 `.ts`);注释/循环修复有帮助。经典 `src/compiler` 不在本树 | + +**怎么读 Node 残留:** 真实循环里的 once-init `??= new` 往往是**真**分配(相对 `.push({` 的严重档仍开放)。`inspect.js` 的 SafeMap 构造可能邻接循环却非每元素热路径——评审,不要批量重写。 + +### 1.4 真正管用的反向优化 + +- 丢掉空 `{}`/`[]` 绑定(r2)——消灭 Node options-bag 噪声;保留 `.push({`。 +- 多行块注释 + 无花括号 `for` 修复(r3)——JSDoc 里的 `for` 与 `for (…)\n stmt;` 不再打开假循环(Console 原型安装等)。 +- 夹具:`good/empty_opts_in_loop.js`、`good/comment_for_not_loop.js`、`good/throw_new_in_loop.js`、`good/jp-alloc-hot_reuse_buffer.js`、`bad/jp-alloc-hot_new_in_loop.js`、`bad/jp-alloc-hot_push_object.js`。 + +### 1.5 循环里字符串 `+=`(已移除) + +`jp-string-plus-in-loop` 曾匹配字符串字面量或 `String.fromCharCode` 的 `+=`,并建议改成 `push` 再 `join`。在匹配器命中的 shape 上,该重写大约没有 2× 收益(Node v24.18.0,V8 13.6.233.17-node.50)。 + +| Shape | `+=` ms | `join` ms | `+=` / `join` | +| --- | ---: | ---: | ---: | +| `"\n"` × 100,重复 200 | 0.194 | 0.126 | 1.53 | +| `"\n"` × 22000,重复 20 | 1.648 | 2.756 | 0.60 | +| `"\n"` × 44000,重复 10 | 1.459 | 3.176 | 0.46 | +| `String.fromCharCode` × 22000,重复 10 | 0.893 | 2.647 | 0.34 | +| 64 字符字面量 × 700,重复 40 | 0.070 | 0.226 | 0.31 | +| `obj.s += "\n"` × 22000,重复 10 | 0.759 | 1.816 | 0.42 | + +比值接近 2 表示 `join` 快一倍。100 换行那例是 1.53×,未达门槛。22000 与 44000 字符时 `join` 更慢。V8 已对循环 `+=` 建串。`push` 再 `join` 还要分配指针数组。匹配器已删。墓碑:[`jp-string-plus-in-loop`](../../evidence/_removed/jp-string-plus-in-loop.REMOVED.md)。 + +### 1.6 链式 `.map` / `.filter`(jp-map-filter-chain) + +`.filter().map()` 每次调用分配中间数组。成本随长度与趟数增长。3 行列表与 35000 行表不是同一成本。 + +本机(与 §1.5 相同 Node)两边都构建结果数组: + +| Shape | 链式 ms | 单趟 ms | 比值 | +| --- | ---: | ---: | ---: | +| 3 行,filter 再 map,× 20000 | 0.289 | 0.226 | 1.28× | +| 3 行,四次 filter 再 map,× 20000 | 0.761 | 0.220 | 3.46× | +| 35000 行,filter 再 map,× 20 | 8.728 | 1.898 | 4.60× | +| 35000 行,四次 filter 再 map,× 20 | 12.850 | 1.306 | 9.84× | + +3 行 filter-then-map 低于 2×。3 行四趟高于 2×,但 20000 次仍在一毫秒内。35000 行才是单趟明显更快的地方。hint 点名长度。短列表重写可选。 + +Lint:**jp-map-filter-chain** —— 默认 hint。匹配前剥掉 `//` 与块注释。codegen `gen-*.js` 路径跳过。证据:[`jp-map-filter-chain`](../../evidence/zh-CN/jp-map-filter-chain.md)。 + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/02_delete_and_shape.md b/packages/jsperf/docs/guide-src/zh-CN/02_delete_and_shape.md new file mode 100644 index 0000000..0436ba8 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/02_delete_and_shape.md @@ -0,0 +1,25 @@ +## 2. `delete` 与 shape + +### 2.1 为什么热对象上的 `delete` 伤性能 + +`delete obj.key` 改变对象的 hidden class。引擎可能掉下 monomorphic IC;空洞在某些路径上会逼进 dictionary 模式。优先: + +- 字段可选且希望 shape 稳定时,用 `obj.key = undefined` / `null`。 +- 「是否存在」很常见时,用并行 `Set` / bitset / 稠密旁表。 +- 在冷路径重建新对象,而不是在热循环里 delete。 + +Lint:**jp-delete-shape** —— 成员表达式上的 `delete`(文件级启发式;默认不限循环)。实测门禁:[`evidence/jp-delete-shape`](../../evidence/zh-CN/jp-delete-shape.md)(相对墓碑约 **474×**,Node v20.19.2);JSONL 同意,但语料 triage 发现命中几乎全是 OK 惯用法(见 [evidence-policy](evidence-policy.md)),故默认 off(`--preset all` 或 `--severity jp-delete-shape=error` 开启)。 + +### 2.2 有意的 `delete`(bootstrap / 功能剥离) + +Node 的 `lib/internal/bootstrap/**` 用 `delete` 做模块缓存与功能剥离。那是启动 / 一次性,不是每请求热路径。 + +**jsperf-lint:** 对 jp-delete-shape 与 jp-delete-array-hole 跳过 `internal/bootstrap/` 下文件、匹配 `*bootstrap*` 的文件名,以及 inspect/debugger/test-runner 路径。 + +证据(语料命中): Node `lib/` jp-delete-shape 在 bootstrap 跳过后 **27 → 21**(r1→r2);r3 保持 **21**,同时 jp-alloc-hot 在动。剩余 delete 需逐案评审(CLI 拆卸、稀有 map)。 + +### 2.3 可选的未来 + +`--hot-only`(要求 jp-delete-shape 有循环上下文)尚未交付。在此之前,把循环外的 jp-delete-shape 当作 shape 味道,而不是自动 bug。 + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/03_embed_and_miniprogram.md b/packages/jsperf/docs/guide-src/zh-CN/03_embed_and_miniprogram.md new file mode 100644 index 0000000..5b99010 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/03_embed_and_miniprogram.md @@ -0,0 +1,32 @@ +## 3. 嵌入与小程序 + +### 3.1 同样的物理,更小的运行时 + +QuickJS、Hermes 类嵌入与小程序 JS 运行时仍要为这些付钱: + +- 每轮对象分配(jp-alloc-hot 模式)。 +- 经 `delete` 或分叉属性顺序导致的 shape 变化。 +- 紧循环里的日志(**jp-console-hot** —— `console.log` / `console.debug`)。证据:[`jp-console-hot`](../../evidence/zh-CN/jp-console-hot.md)(即便丢弃 stdout 仍约 **1300×** 对比安静)——默认 off(`app`)/ hint(`engine`),不是 CI error。 + +它们往往**没有**浏览器 Performance 面板、DOM 或大 IC 预算。以「打开 DevTools → …」开头的建议迁不过来。优先: + +- §1–§2 的分配/shape 纪律。 +- 宿主暴露批处理 API 时批量工作(`examples/p02_batch_note.js`),而不是每帧 microtask。 +- 用**宿主** profiler 测量(Node `--prof`、QuickJS 计时钩子、厂商小程序 perf 面板)——不是仅 Chrome 工作流。 + +不要把 §4(Maglev/Turbofan、elements-kind 迁移、`--trace-deopt`)套到这些运行时。稳定 shape 与更少分配仍然有用;V8 分层话术没有。 + +### 3.2 共享的 quickjs 树 + +cperf 在 **C** 引擎上跑;jsperf 在 旁边的 `*.js`(跳过测试与 `gen-*`)。当前规则在那里报 **0**([corpus.md](corpus.md));较早修订报过 2 个 jp-alloc-hot。压力低,但作为邻接嵌入的样本有用。不要指望生成脚本有 Preact 级干净。 + +### 3.3 这里不该推荐什么 + +- 把 DOM 批处理 / `requestAnimationFrame` 当主修复。 +- `innerHTML` vs `createElement` 争论。 +- 仅浏览器 API(嵌入可能不提供的 `WeakRef` + FinalizationRegistry 模式)。 +- §4 的 Turbofan / Maglev / elements-kind 调优。 + +Typed array 与一致 shape 仍然可移植(`examples/p01_typed_array.js`、`m01_hidden_class.js`)。 + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/04_v8_optimizations.md b/packages/jsperf/docs/guide-src/zh-CN/04_v8_optimizations.md new file mode 100644 index 0000000..33ea127 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/04_v8_optimizations.md @@ -0,0 +1,127 @@ +## 4. V8 优化(Node / Chrome) + +本章针对 **V8** 深化 §1。不适用于 QuickJS、Hermes 类嵌入或小程序运行时——见 §3。那些引擎共享「稳定 shape / 更少分配」的物理;它们**得不到** Ignition → Maglev/Turbofan 建议、elements-kind 迁移或 `--trace-deopt` 工作流。 + +按 **Node 22+ / 现代 V8 连续体** 标注日期(测于 Node v22.22.3,V8 12.4.x)。流水线层级名与标志会漂;把下面的版本钉当作「我们跑过什么」,不是永远保证。 + +### 4.1 Hidden class / map 与 IC(扩展 §1.1) + +V8 给每个对象挂一个 **map**(hidden class)。属性添加顺序与类型反馈建过渡树。属性读写走 **内联缓存(IC)**: + +| IC 状态 | 含义(实践) | +|---------|--------------| +| Monomorphic | 只见过一个 map —— 热读写的最佳情况 | +| Polymorphic | 少数 map(小固定集)—— 仍可优化,更慢 | +| Megamorphic | 很多 map —— IC 放弃;走通用慢路径 | + +**要做:** 用一种字段顺序构造(`{ x, y, z: 0 }`);让热调用点落在同一 shape。**不要:** 分支成有的对象先 `x` 后 `y`、有的反过来;不要在热对象上 `delete` 字段(§2)。 + +微基准:`examples/bench/shape_mono_vs_poly.js`(同序 vs 分叉构造;数字见 §4.6)。 + +### 4.2 Elements kind + +数组不是单一表示。V8 跟踪 **elements kind**,大致: + +| Kind(概念) | 典型内容 | +|--------------|----------| +| `PACKED_SMI_ELEMENTS` | 稠密小整数,无洞 | +| `PACKED_DOUBLE_ELEMENTS` | 稠密浮点(出现非 Smi 数之后) | +| `PACKED_ELEMENTS` | 稠密对象/值混合 | +| `HOLEY_*` | 同族**带洞**(缺索引) | + +**实践要/不要:** + +- 优先稠密 `push` / 预分配(`new Array(n)` 再填 0..n-1),而不是稀疏写。 +- **不要打洞:** `delete arr[i]`,或跳索引(`arr[0]=1; arr[2]=2`),会逼进 holey kind 与更弱的优化代码。 +- 数值热循环优先 **typed array**(`Float64Array`、`Uint32Array` …):固定元素类型、无 holey 迁移、连续存储。普通 `number[]` 可以保持 packed,但混合 Smi/double 迁移与装箱仍有成本。 +- 读过 `length` 或把洞当「可选槽」是 shape 味道;必须时用并行长度、typed 位掩码,或在**packed** 数组上用 `undefined` 墓碑。 + +Lint:**jp-delete-array-hole** 标记 `delete arr[i]` 式成员删除(holey 风险);实测门禁 [`evidence/jp-delete-array-hole`](../../evidence/zh-CN/jp-delete-array-hole.md)(约 **3.2×**)。jp-delete-shape 覆盖普通成员上的 `delete obj.key`;历史上漏掉计算索引删除——jp-delete-array-hole 补上。 + +微基准:`examples/bench/elements_packed_vs_holey.js`、`examples/bench/typed_array_vs_number_array.js`。 + +### 4.3 Deopt —— 何时要紧;怎么查 + +**Deopt** 因假设被打破(意外 map、elements kind、边界等)把优化代码打回更慢层级。启动时或稀有路径上偶发 deopt 正常。当它落在 profile 到的热函数上并每轮重复时,deopt **要紧**。 + +**静态 jsperf-lint 无法检测 JIT 结果。** IC 状态、deopt 与优化层级是运行时事实。jp-* 规则只标分配/shape *味道*。JIT 验证 = profile / `--trace-deopt` / bench —— 绝不是「linter 说了算」。 + +**运行时 harness(不是 lint 规则):** + +```sh +# 跨 Node 版本优先: +node --trace-deopt examples/bench/deopt_probe.js +bash scripts/jit-check.sh # 写入 examples/bench/out/deopt-sample.txt + +# 可选 / 脆弱:V8 natives(状态码会漂) +USE_NATIVES=1 node --allow-natives-syntax --trace-deopt examples/bench/deopt_probe.js +``` + +本机(Node v22.22.3)上,`deopt_probe` 先把 `hotLoad` 暖成 monomorphic 再喂分叉 map;`--trace-deopt` 报告: + +`[bailout … reason: wrong map … JSFunction hotLoad … Code TURBOFAN]` + +一次性 `-e` 脚本也常打印 “Insufficient type feedback …” —— 当作**启动噪声**,不是味道已热的证明。 + +**检查(Chrome):** Performance 面板 → 可用处的 V8 标记;与改前/改后墙钟对照。不要编造加速百分比。 + +### 4.4 流水线素描(Ignition → Maglev / Turbofan) + +高层上,现代 V8/Node: + +1. **Ignition** —— 字节码解释器;收集类型反馈。 +2. **Maglev** —— 中层优化编译器(当前 Node 22+ / 近版 V8 有;确切启用可取决于标志与负载)。 +3. **Turbofan** —— 热代码的顶层优化编译器。 + +代码升温时可能爬层级,deopt 时回落。不要过度声称「Node N 上总是 Maglev」或钉死确切层级阈值——它们会动。对没有这条流水线的嵌入(§3),完全跳过层级话术。 + +### 4.5 jsperf-lint 能看见与不能看见什么 + +**硬限制:** 静态 jp-* 规则**不能**识别 JIT 优化成败。没有规则观察 monomorphic vs mega IC、Maglev/Turbofan 层级或 deopt。 + +| 规则 | 看见 | **看不见** | +|------|------|------------| +| jp-alloc-hot | `.push({`、非空绑定、清晰的 `new` 容器(默认 scope all) | IC 多态;分配是否导致 deopt(通常不会——导致 GC) | +| jp-console-hot | `console.*`(默认 scope all) | 即使不在热路径上,日志是否值得门控 | +| jp-delete-shape | `delete obj.key` 式成员 | 是否已是 dictionary 模式;deopt vs「只是慢」 | +| jp-delete-array-hole | `delete arr[i]`(holey) | 运行时 elements kind;无 `delete` 的跳索引打洞 | +| jp-string-plus-in-loop | 已移除 | 匹配到的 `+=` shape 上 `push` + `join` 大约没有 2× | +| jp-map-filter-chain | 一行上的链式 `.map` / `.filter` / `.flatMap` | 数组长度;3 行链与 35000 行链成本不同 | + +Lint = 分配/shape 味道。JIT 验证 = `jit-check.sh` / `--trace-deopt` / `examples/bench`。多态 IC **没有** jp-* 指纹。 + +### 4.6 本机实测(Node v22.22.3) + +经 `scripts/run-v8-benches.sh` → `examples/bench/out/`,跑于 2026-10-21 18:49 Asia/Shanghai。Node **v22.22.3**,V8 **12.4.254.21-node.56**,linux x64。预热 3 + 9 轮;下表为中位数。**仅单机数字** —— 不是跨机声明。 + +| Bench | 变体 | N | 中位 ms | Ops/s(约) | +|-------|------|---|----------|-------------| +| shape_mono_vs_poly | mono(同字段序) | 2e5 | **0.219** | ~9.1e8 | +| shape_mono_vs_poly | poly(分叉序) | 2e5 | **1.435** | ~1.4e8 | +| elements_packed_vs_holey | packed fill+sum | 2e5 | **1.227** | ~1.6e8 | +| elements_packed_vs_holey | 经 `delete` 的 holey | 2e5 | **4.589** | ~4.4e7 | +| elements_packed_vs_holey | 经跳索引的 holey | 2e5 | **1.445** | ~1.4e8 | +| typed_array_vs_number_array | 稠密 `number[]` fill+sum | 1e6 | **5.217** | ~1.9e8 | +| typed_array_vs_number_array | `Float64Array` fill+sum | 1e6 | **5.948** | ~1.7e8 | + +**怎么读:** 此处 poly shape 比 mono 慢约 **6.5×**;`delete`-holey 比 packed 慢约 **3.7×**。跳索引 holey 写的槽更少——比较 kind 行为,不要只比原始速度。在本稠密 double 负载上 `number[]` **略快于** `Float64Array`;typed array 仍对固定元素类型、避免 packed→holey / Smi→double 迁移以及二进制/WASM 边界有意义——测你的循环。 + +完整 JSONL:`examples/bench/out/results.jsonl`(脚本写出)。 + +### 4.7 对照:静态味道 vs V8 时间(PARTIAL) + +实验:`examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh`(Node **v22.22.3**)。 + +| 案例 | 静态味道? | 比值(味道/干净) | 同意? | +|------|------------|-------------------|--------| +| Mono vs poly shape | 仅 poly | **12.0×** | **YES**(+ mono→poly 上的 `wrong map` deopt) | +| Packed vs holey `delete a[i]` | 类 jp-delete-array-hole | **5.47×** | **YES**(时间);deopt 嘈杂 | +| `Float64Array` vs holey `number[]` | holey | **1.34×** | **PARTIAL**(弱化) | +| `delete obj.k` vs `= undefined` | jp-delete-shape | **165×** | **YES**(时间);deopt 计数相近 | +| 对照:循环 `new` vs 一次 | jp-alloc-hot | **49.3×** | **PARTIAL** —— 分配/GC 痛,不是 deopt | + +**不要假装 jp-* 规则等于 JIT 意识。** 对 shape / holey / 热 `delete` 同意最强;用 deopt 行数当神谕,或推销 typed array 总是快于 packed double 时最弱。 + +**JSONL 门禁(本机 Node v20.19.2,重跑 `scripts/regen-evidence.sh`):** jp-delete-array-hole 约 **3.2×** → error;jp-delete-shape 约 **474×**(语料命中几乎全是 OK 惯用法)→ off;jp-alloc-hot 约 **19×**(分配/GC)→ off;jp-console-hot 约 **1328×**(丢弃 sink,依赖环境)→ off(`app`)/ hint(`engine`)。见 [`evidence-policy`](evidence-policy.md) 与 `evidence/jp-*.md`。 + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/05_startup_and_tooling.md b/packages/jsperf/docs/guide-src/zh-CN/05_startup_and_tooling.md new file mode 100644 index 0000000..15cb06e --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/05_startup_and_tooling.md @@ -0,0 +1,71 @@ +## 5. 启动与工具链(冷路径) + +本章讲 **工具链 / 冷启动 / 模块图** 成本——不是 §1–§2 / §4 的热循环 shape 与分配味道。 + +主要来源:Marvin Hagemeister 系列 **[Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)**(第 1–13 部分)。下列方法经转述;**不要把他的墙钟数字当成我们的**——在你的机器上重测。 + +| 关注点 | 章节 | 靠什么发现 | +|--------|------|------------| +| 运行时 shape / 分配 / holey / `delete` | §1、§2、§4 | jp-* 规则 + `examples/bench` | +| 模块解析、急切 `require`、barrel、CLI 开销 | §5 | `node --cpu-prof` / speedscope、require 图检查 —— **不是** jp-* 规则 | + +### 5.1 先做 profile(工具链) + +重写库或 CLI 之前: + +1. 抓 CPU profile:`node --cpu-prof your-tool.js …`(或对 npm 脚本 `node --cpu-prof $(which npm) run …` —— 见 [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/))。 +2. 在 [speedscope](https://www.speedscope.app/)(或 Chrome DevTools)打开 `.cpuprofile`。 +3. 问:时间在**你的**热函数里,还是在**加载 / 解析**模块、类型转换、正则、FS? + +Marvin 反复使用的方法([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)):找顶帧 → 打开源码 → 去掉可避免的工作 → 再 profile。与 XPerf「重写前先测」同一循环,瞄准 **构建/lint/CLI** 而非应用渲染循环。 + +### 5.2 热转换、短串正则、内层函数(part 1 方法) + +转述自 [part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)(PostCSS、SVGO 及邻居)的模式: + +- **字符串 ↔ 数字搅动** —— 例如紧循环里 `toFixed` 再 `+` 回数字。优先留在数字域(`Math.round` / 按 `10**p` 缩放)。 +- **对很多短串跑正则** —— 用一条正则匹配许多细碎序列化片段,往往差于对更长串单趟,或先做「针是否存在」的廉价预检。 +- **热外层函数内创建的函数** —— 每次调用重建闭包伤 IC;提升助手或显式传参,而不是捕获。 +- **转译器产物** —— 例如 `for…of` 被改写成每轮新鲜 `_loop` 函数;检查**已发布**输出,不要只看源码。 + +路径在运行时很热时,这些可与 §4(IC)重叠。在工具链里它们往往主导**启动或每文件**成本。 + +### 5.3 模块解析(part 2 方法) + +来自 [part 2](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2/):在 lint/测试/构建轨迹里,解析可以压过解析(parsing)。 + +当作检查清单(转述他的四点): + +1. **少碰 FS** —— 规模一大,每次 `stat`/`exists` 都贵。 +2. **缓存** 已解析路径(watch 模式:小心失效)。 +3. **`fs.statSync(path, { throwIfNoEntry: false })`** —— 避免为「文件不存在」(ENOENT)抛错并抓栈。 +4. **限制向上走 `node_modules`** —— 尤其当路径别名被误当成裸包导入时。 + +也留意**扩展名猜测**(`.js` → `.jsx` → … → `.ts`)以及与 TypeScript/`tsconfig` paths 不同解析器的别名插件。 + +**jsperf-lint 看不见这些。** 没有 jp-* 规则测 FS 或解析。配套:CPU profile、解析器缓存,以及 `examples/bench/require_cost_sketch.js` 里急切 vs 惰性素描。 + +### 5.4 启动:急切图、barrel、「抱着香蕉的大猩猩」 + +来自 [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/)(npm 脚本)与 [part 7](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/)(barrel): + +- **急切 `require` / `import`** 预先加载整个可达图。错误格式化器、审计代码、进度条——快乐路径用不到的——应**惰性**(`if (err)` 分支内 `require`,稀有命令用动态 `import()`)。 +- **肥类 / mixin 堆** 拉来「抱着香蕉和整片丛林的大猩猩」(Joe Armstrong,经 Marvin):一个入口类 mixin 了 audit + reify + rebuild,而你只需要 `run`。 +- **Barrel 文件**(只 `export * from …` 的 `index.js`)放大图:一次导入可拉来一目录树;按文件 spawn 的测试跑手反复付钱。启动或 lint/测试墙钟要紧时,优先直接导入。 +- CLI 的**已发布 bundle** 可缩小加载成本;要测——巨大单文件有解析权衡。 + +### 5.5 与 jp-* 规则的关系 + +| | 运行时章节(§1–§2、§4) | 本章(§5) | +|--|------------------------|-----------| +| 典型收益 | 热循环 IC / GC / elements kind | 冷启动、`npm run`、lint/测试图 | +| Lint | jp-* 规则(行窗规则) | **今天没有** —— 仅文档配套 | +| 证据 | `examples/bench/*`、`--trace-deopt` | `--cpu-prof` + speedscope;require 图大小 | + +不要把干净的 jsperf-lint 自检解读成「启动没问题」。不要在没有本地 profile 的情况下把 Marvin 发表的加速当可迁移结论。 + +### 5.6 可选素描:急切 vs 惰性 require + +`examples/bench/require_cost_sketch.js` 建一个小夹具图,计时顶层急切 `require` vs 稀有分支内惰性 `require`。这是**教学微基准**,不是关于 npm 或你的应用的声明。运行:`node examples/bench/require_cost_sketch.js`。 + +--- diff --git a/packages/jsperf/docs/guide-src/zh-CN/06_hot_calls.md b/packages/jsperf/docs/guide-src/zh-CN/06_hot_calls.md new file mode 100644 index 0000000..eff5d36 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/06_hot_calls.md @@ -0,0 +1,19 @@ +## 6. 构造函数、JSON、DOM 查询与日期格式 + +下列每条 hint 对应一种实测 shape。hint 行陈述该前提。只是看起来像的调用不给那套重写。默认 **`--scope all`** 到处报告这些味道——极端栈把点滴成本也算进范围,即便不在循环里。可选用 **`--scope loop`** 恢复从前的仅循环内匹配。消息不会告诉读者或模型可以忽略非循环位点。 + +`new RegExp(pattern)` 在模式不变时每次调用都编译。正则字面量会被缓存。若模式是循环变量或 `parts[i]`,检查器不会说编译一次。基准编译一个固定模式,Node 26 上约 4.4×。 + +对不变文本的 `JSON.parse`,相对解析一次,Node 26 上约 140×。对不变值的 `JSON.stringify` 是同一 shape。JSONL 行或 `rows[i]` 每轮文本不同。检查器跳过参数就是循环绑定的情况。它不会让你把那些行折叠成一次 parse。 + +对不变选择器的 `querySelector` / `getElementById`,在 Node 26、很小的 linkedom 文档上约 16×。由循环下标拼出的选择器不是那个基准。用不变 locale 与选项的 `new Intl.DateTimeFormat`,Node 26 上约 36×;两边仍会 format。`new Date` 不是本规则。 + +对不变路径的 `fs.readFileSync`,相对读一次,Node 26 上约 114×。在 `--scope loop` 下,只匹配 `{` 后的循环体(只跑一次读的 `for` 可迭代随后被跳过)。默认 `--scope all` 下,路径稳定时无论出现在哪都会报。每轮变化的路径(循环绑定)不报。 + +`Date.now` 约 26×,`performance.now` 约 19×,相对只采样一次,在不 wait 的紧循环里。含 `await` 的循环,或 `for await` 循环,时钟规则不报。不需要每次新鲜时间时采样一次。必须每轮读时钟的限流器没有可搬的东西。 + +**jp-regex-ctor-hot** 与 **jp-json-hot** 在 app 与 engine preset 上保持 hint。两者有 JSONL,2026-10-22 裁决未覆盖。**jp-dom-query-hot**(Node 26、很小 linkedom 约 16×)与 **jp-intl-datetimeformat-hot**(Node 26 约 36×,两边仍 format)有 JSONL 且保持 hint。它们在该裁决之外。`--preset all` 对仍存在的每条规则是 error。 + +`Reflect.get` 约 8×,未给规则。 + +Lint: `jp-regex-ctor-hot`, `jp-json-hot`, `jp-dom-query-hot`, `jp-intl-datetimeformat-hot`, `jp-readfile-sync-hot`, `jp-clock-hot`(默认 hint) diff --git a/packages/jsperf/docs/guide-src/zh-CN/99_tail.md b/packages/jsperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..b26ecf3 --- /dev/null +++ b/packages/jsperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,25 @@ +### Lint 对照表 + +| ID | 主题 | 严重级别 | 证据 / 备注 | +|----|------|----------|-------------| +| jp-alloc-hot | 对象/数组/`new` 分配 | off(`all`:error) | [`jp-alloc-hot`](../../evidence/zh-CN/jp-alloc-hot.md) ~19× / ~6.4× —— GC/分配,非 deopt | +| jp-console-hot | `console.*` | hint(`engine`)/ off(`app`) | [`jp-console-hot`](../../evidence/zh-CN/jp-console-hot.md) ~1328× 丢弃 sink —— 依赖环境 | +| jp-delete-shape | 普通成员上的 `delete` | off(`all`:error) | [`jp-delete-shape`](../../evidence/zh-CN/jp-delete-shape.md) 相对墓碑 ~474×;语料命中几乎全是 OK 惯用法 | +| jp-delete-array-hole | 计算索引上的 `delete`(holey 数组) | error | [`jp-delete-array-hole`](../../evidence/zh-CN/jp-delete-array-hole.md) 相对 packed ~3.2× | +| jp-map-filter-chain | 链式 `.map` / `.filter` / `.flatMap` | hint | 长数组约 4.6×–9.8×;3 行 filter+map 为 1.28× | +| jp-regex-ctor-hot | `new RegExp`,模式非循环绑定 | hint | Node 26 固定模式约 4.4×;优先字面量或一次共享编译 | +| jp-json-hot | `JSON.parse` / `stringify`,文本非循环绑定 | hint | Node 26 不变文本约 140×;稳定时复用 | +| jp-dom-query-hot | DOM 查询,选择器非循环绑定 | hint | Node 26、很小 linkedom 上 JSONL 约 16×;未做语料 triage | +| jp-intl-datetimeformat-hot | `new Intl.DateTimeFormat` | hint | Node 26 约 36×;两边仍 format;未做语料 triage | +| jp-readfile-sync-hot | `readFileSync`,路径非循环绑定 | hint | Node 26,不变路径约 114× | +| jp-clock-hot | `Date.now` / `performance.now` | hint | Node 26 无 `await` 时约 70× / 110×;不需要新鲜时间时采样一次 | +| *(无)* | 模块解析 / 急切图 / barrel | — | §5 —— 用 `--cpu-prof` | + +上表严重级别为默认 `app` preset。匹配作用域默认为 **`--scope all`**(到处);`--scope loop` 恢复仅循环内匹配。策略:[`evidence-policy.md`](evidence-policy.md)。夹具:`jsperf-lint/fixtures/{bad,good}/`。 + +### 参考 + +- [corpus.md](corpus.md) +- `examples/bench/` · `scripts/run-v8-benches.sh` · `scripts/jit-check.sh` · `scripts/run-static-vs-v8.sh` · `scripts/regen-evidence.sh` · `examples/bench/require_cost_sketch.js` +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- Marvin Hagemeister — [Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)(第 1–13 部分;§5 引用方法) diff --git a/packages/jsperf/docs/js-performance-guide.md b/packages/jsperf/docs/js-performance-guide.md index 1f80d69..5801aa2 100644 --- a/packages/jsperf/docs/js-performance-guide.md +++ b/packages/jsperf/docs/js-performance-guide.md @@ -1,20 +1,14 @@ # JavaScript performance guide (v0) -Stable shapes, fewer per-iteration allocations — plus a separate chapter on tooling / cold-start. +Stable shapes, fewer per-iteration allocations — plus a separate chapter on tooling and cold-start. -> **Focus: modern Node and V8 ICs / GC.** QuickJS and 小程序-like embeds get a dedicated chapter (§3) — avoid advice that only makes sense with a DOM or a full browser profiler. **§4 deepens V8** (maps, elements kinds, deopts, Ignition→Maglev/Turbofan); it does **not** apply to §3 runtimes. **§5** covers **startup and tooling** (module resolution, eager graphs, barrels) after Marvin Hagemeister’s ecosystem series — orthogonal to the jp-* lint rules. +> **Focus: modern Node and V8 (inline caches / GC).** QuickJS and 小程序-like embeds get their own chapter (§3) — skip advice that needs a DOM or a full browser profiler. §4 is V8-only (maps, elements kinds, deopts, Ignition → Maglev/Turbofan) and does not apply to §3 runtimes. §5 covers startup and tooling (module resolution, eager graphs, barrels) after Marvin Hagemeister’s ecosystem series; that cost sits outside the jp-* lint rules. > -> Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. §4.6 cites single-machine microbench medians only. §5 cites Marvin’s **methods** (URLs); re-measure his timings locally. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. §4.6 cites single-machine microbench medians only. §5 cites Marvin’s methods (URLs); re-measure his timings locally. > -> Companion: `jsperf-lint/` — the jp-* rules, see the [Lint map](#lint-map) (line windows; not a full parser). Dogfooded on Node `lib/`, Preact `src/`, quickjs `*.js` (tests skipped), and TypeScript `packages/typescript/src` (2026-10-21 Asia/Shanghai). See [corpus.md](corpus.md). Startup/FS cost is **out of band** for the jp-* rules — see §5. +> Companion: `jsperf-lint/` — the jp-* rules, see the [Lint map](#lint-map) (line windows; not a full parser). Run against Node `lib/`, Preact `src/`, quickjs `*.js` (tests skipped), and TypeScript `packages/typescript/src` (2026-10-21 Asia/Shanghai). See [corpus.md](corpus.md). Startup and FS cost are out of band for the jp-* rules — see §5. -## How to read this - -1. Profile first (`node --prof`, `node --cpu-prof` + speedscope, Chrome Performance, clinic). Lint findings are review prompts. -2. Prefer consistent object shapes and batch allocation over micro-optimizing single property access. -3. Embed / 小程序 runtimes: skip DOM-only tips; watch alloc + `delete` the same way (§3). Do **not** apply Turbofan/Maglev advice there. -4. On Node/Chrome, use §4 for elements kinds, IC polymorphism, and deopt checks — then measure. -5. For CLIs, lint/test runners, and package cold-start, use §5 (resolution, lazy `require`, barrels) — not the jp-* rules alone. +Profile first (`node --prof`, `node --cpu-prof` + speedscope, Chrome Performance, clinic). Prefer consistent object shapes and batch allocation over micro-optimizing a single property access. On embeds / 小程序, skip DOM tips and do not apply Turbofan/Maglev advice. On Node/Chrome, use §4 for elements kinds, IC polymorphism, and deopt checks. For CLIs, lint/test runners, and package cold-start, use §5. ## Contents @@ -24,6 +18,7 @@ Stable shapes, fewer per-iteration allocations — plus a separate chapter on to - [3. Embed and 小程序](#3-embed-and-小程序) - [4. V8 optimizations (Node / Chrome)](#4-v8-optimizations-node--chrome) - [5. Startup and tooling (cold paths)](#5-startup-and-tooling-cold-paths) +- [6. Constructors, JSON, DOM queries, and date formats](#6-constructors-json-dom-queries-and-date-formats) - [Lint map](#lint-map) - [References](#references) @@ -31,18 +26,15 @@ Stable shapes, fewer per-iteration allocations — plus a separate chapter on to ## 0. Scope and method -1. **Shapes first** — initialize fields in one order; avoid polymorphic hot paths (§1, §4.1). -2. **Alloc pressure** — `.push({…})`, `new`, non-empty object binders; default scope reports these outside loops too (§1). -3. **`delete`** — prefer tombstones or parallel structures on hot objects (§2); avoid `delete arr[i]` holes (§4.2, jp-delete-array-hole). -4. **Embed** — same alloc/shape rules without assuming `window` / DOM or V8 tiers (§3). -5. **V8 Node/Chrome** — elements kinds, IC state, deopts, pipeline sketch (§4). -6. **Startup / tooling** — profile with `--cpu-prof`; cut FS/resolution; lazy-load rare paths; watch barrels (§5). -7. **Measure** — `--prof` / `--cpu-prof` / `--trace-deopt` / Performance panel / `examples/bench` before rewriting. +1. Shapes first — initialize fields in one order; avoid polymorphic hot paths (§1, §4.1). +2. Alloc pressure — `.push({…})`, `new`, non-empty object binders; default scope reports these outside loops too (§1). +3. `delete` — prefer tombstones or parallel structures on hot objects (§2); avoid `delete arr[i]` holes (§4.2, `jp-delete-array-hole`). +4. Embed — same alloc/shape rules without assuming `window` / DOM or V8 tiers (§3). +5. V8 on Node/Chrome — elements kinds, IC state, deopts, pipeline sketch (§4). +6. Startup / tooling — profile with `--cpu-prof`; cut FS/resolution; lazy-load rare paths; watch barrels (§5). +7. Measure — `--prof` / `--cpu-prof` / `--trace-deopt` / Performance panel / `examples/bench` before rewriting. Sources: [XPerf playbook](../../../docs/playbook.md) · [corpus.md](corpus.md) · [Marvin series](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) - ---- - ## 1. Allocation and shapes ### 1.1 Hidden classes (shapes) @@ -69,7 +61,7 @@ Prefer typed arrays (`examples/p01_typed_array.js`), object pools, or preallocat **Evidence (this machine):** [`evidence/jp-alloc-hot`](../evidence/jp-alloc-hot.md) — `new Uint8Array` in-loop vs once **~19×**; `.push({i})` vs prealloc fill **~6.4×** (Node v20.19.2). Mechanism is alloc/GC (STATIC-VS-V8 PARTIAL for deopt). Default severity is **off** (`--preset all` or `--severity jp-alloc-hot=hint` enables it) — see [evidence-policy](evidence-policy.md). Default **`--scope all`** reports these patterns outside loops too; `--scope loop` keeps the former in-loop-only match. -### 1.3 Corpus dogfood (heuristic) +### 1.3 Corpus hit counts Counts below come from earlier line-window revisions of the linter (r1–r3) and show how the heuristics were narrowed; current corpus results are in [corpus.md](corpus.md). @@ -118,7 +110,7 @@ On this machine (same Node as §1.5), both sides build the result array: The 3-row filter-then-map case is under 2×. Four passes on 3 rows is above 2×, and 20000 calls still finish in under a millisecond. The 35000-row cases are where one pass is clearly faster. The hint names that length. Rewriting a short list is optional. -Lint: **jp-map-filter-chain** — default **hint**. `//` comments and block comments are stripped before the match. Codegen `gen-*.js` files are path-skipped. Evidence: [`jp-map-filter-chain`](../evidence/jp-map-filter-chain.md). +Lint: `jp-map-filter-chain` — default hint. `//` comments and block comments are stripped before the match. Codegen `gen-*.js` files are path-skipped. Evidence: [`jp-map-filter-chain`](../evidence/jp-map-filter-chain.md). --- @@ -132,7 +124,7 @@ Lint: **jp-map-filter-chain** — default **hint**. `//` comments and block comm - A parallel `Set` / bitset / dense side table for “present?” when absence is common. - Rebuild a new object in a cold path instead of deleting in a hot loop. -Lint: **jp-delete-shape** — `delete` on a member expression (file-level heuristic; not loop-only by default). Measured gate: [`evidence/jp-delete-shape`](../evidence/jp-delete-shape.md) (~**474×** vs tombstone on Node v20.19.2); the JSONL agrees, but corpus triage found the hits were almost all OK-idiom (see [evidence-policy](evidence-policy.md)), so the default is **off** (`--preset all` or `--severity jp-delete-shape=error` enables it). +Lint: `jp-delete-shape` (`delete` on a member expression; file-level heuristic, not loop-only by default). Measured gate: [`evidence/jp-delete-shape`](../evidence/jp-delete-shape.md) (~474× vs tombstone on Node v20.19.2); the JSONL agrees, but corpus triage found the hits were almost all OK-idiom (see [evidence-policy](evidence-policy.md)), so the default is off (`--preset all` or `--severity jp-delete-shape=error` enables it). ### 2.2 Intentional `delete` (bootstrap / feature stripping) @@ -140,7 +132,7 @@ Node’s `lib/internal/bootstrap/**` uses `delete` for module cache and feature **jsperf-lint:** skips files under `internal/bootstrap/`, filenames matching `*bootstrap*`, and inspect/debugger/test-runner paths for jp-delete-shape and jp-delete-array-hole. -**Evidence (dogfood):** Node `lib/` jp-delete-shape **27 → 21** after bootstrap skip (r1→r2); held at **21** in r3 while jp-alloc-hot moved. Remaining deletes need case-by-case review (CLI teardown, rare maps). +Corpus hits: Node `lib/` jp-delete-shape **27 → 21** after bootstrap skip (r1→r2); held at **21** in r3 while jp-alloc-hot moved. Remaining deletes need case-by-case review (CLI teardown, rare maps). ### 2.3 Optional future @@ -156,19 +148,19 @@ QuickJS, Hermes-like embeds, and 小程序 JS runtimes still pay for: - Per-iteration object allocation (jp-alloc-hot patterns). - Shape changes via `delete` or divergent property order. -- Logging in tight loops (**jp-console-hot** — `console.log` / `console.debug`). Evidence: [`jp-console-hot`](../evidence/jp-console-hot.md) (~**1300×** vs quiet even with discarded stdout) — default **off** (`app`) / **hint** (`engine`), not a CI error. +- Logging in tight loops (`jp-console-hot` — `console.log` / `console.debug`). Evidence: [`jp-console-hot`](../evidence/jp-console-hot.md) (~1300× vs quiet even with discarded stdout) — default off (`app`) / hint (`engine`), not a CI error. They often **lack** a browser Performance panel, DOM, or large IC budgets. Advice that starts with “open DevTools → …” does not transfer. Prefer: - Alloc/shape discipline from §1–§2. - Batching work (`examples/p02_batch_note.js`) instead of per-item microtasks when the host exposes a batch API. -- Measuring with the **host** profiler (Node `--prof`, QuickJS timing hooks, vendor 小程序 perf panels) — not Chrome-only workflows. +- Measuring with the host profiler (Node `--prof`, QuickJS timing hooks, vendor 小程序 perf panels) — not Chrome-only workflows. -**Do not** apply §4 (Maglev/Turbofan, elements-kind transitions, `--trace-deopt`) to these runtimes. Stable shapes and fewer allocs still help; V8 tier talk does not. +Do not apply §4 (Maglev/Turbofan, elements-kind transitions, `--trace-deopt`) to these runtimes. Stable shapes and fewer allocations still help; V8 tier talk does not. ### 3.2 Shared quickjs tree -cperf dogfoods the **C** engine; jsperf dogfoods the `*.js` beside it (tests and `gen-*` skipped). The current rules report **0** findings there ([corpus.md](corpus.md)); earlier revisions reported 2 jp-alloc-hot. Low pressure, but useful as an embed-adjacent sample. Do not expect Preact-level cleanliness on generator scripts. +cperf covers the C engine; jsperf covers the `*.js` beside it (tests and `gen-*` skipped). The current rules report 0 findings there ([corpus.md](corpus.md)); earlier revisions reported 2 jp-alloc-hot. Low pressure, but useful as an embed-adjacent sample. Do not expect Preact-level cleanliness on generator scripts. ### 3.3 What not to recommend here @@ -183,13 +175,13 @@ Typed arrays and consistent shapes remain portable (`examples/p01_typed_array.js ## 4. V8 optimizations (Node / Chrome) -This chapter deepens §1 for **V8** specifically. It does **not** apply to QuickJS, Hermes-like embeds, or 小程序 runtimes — see §3. Those engines share “stable shapes / fewer allocs” physics; they do **not** get Ignition → Maglev/Turbofan advice, elements-kind transitions, or `--trace-deopt` workflows. +This chapter is V8-only. It does not apply to QuickJS, Hermes-like embeds, or 小程序 runtimes — see §3. Those engines still benefit from stable shapes and fewer allocations; they do not get Ignition → Maglev/Turbofan advice, elements-kind transitions, or `--trace-deopt` workflows. -Dated as a **Node 22+ / modern V8 continuum** (measured on Node v22.22.3, V8 12.4.x). Pipeline tier names and flags drift; treat version pins below as “what we ran,” not forever guarantees. +Measured on Node v22.22.3 (V8 12.4.x). Pipeline tier names and flags drift; treat the pins below as what we ran, not forever guarantees. ### 4.1 Hidden classes / maps and ICs (extends §1.1) -V8 attaches a **map** (hidden class) to each object. Property addition order and type feedback build a transition tree. Property loads/stores go through **inline caches (ICs)**: +V8 attaches a map (hidden class) to each object. Property addition order and type feedback build a transition tree. Property loads and stores go through inline caches (ICs): | IC state | Meaning (practical) | |----------|---------------------| @@ -203,7 +195,7 @@ Microbench: `examples/bench/shape_mono_vs_poly.js` (same-order vs divergent cons ### 4.2 Elements kinds -Arrays are not one representation. V8 tracks **elements kinds**, roughly: +Arrays are not one representation. V8 tracks elements kinds, roughly: | Kind (concept) | Typical contents | |----------------|------------------| @@ -219,13 +211,13 @@ Arrays are not one representation. V8 tracks **elements kinds**, roughly: - For numeric hot loops, prefer **typed arrays** (`Float64Array`, `Uint32Array`, …): fixed element type, no holey transition, contiguous storage. Ordinary `number[]` can stay packed, but mixed Smi/double transitions and boxing still cost. - Reading past `length` or relying on holes as “optional slots” is a shape smell; use a parallel length, a typed bitmask, or `undefined` tombstones on a **packed** array if you must. -Lint: **jp-delete-array-hole** flags `delete arr[i]`-style member deletes (holey risk); measured gate [`evidence/jp-delete-array-hole`](../evidence/jp-delete-array-hole.md) (~**3.2×**). jp-delete-shape covers `delete obj.key` on ordinary members; it historically missed computed index deletes — jp-delete-array-hole closes that gap. +Lint: `jp-delete-array-hole` flags `delete arr[i]`-style member deletes (holey risk); measured gate [`evidence/jp-delete-array-hole`](../evidence/jp-delete-array-hole.md) (~3.2×). jp-delete-shape covers `delete obj.key` on ordinary members; it historically missed computed index deletes — jp-delete-array-hole closes that gap. Microbenches: `examples/bench/elements_packed_vs_holey.js`, `examples/bench/typed_array_vs_number_array.js`. ### 4.3 Deopts — when they matter; how to check -A **deopt** throws optimized code back to a slower tier because assumptions broke (unexpected map, elements kind, bounds, etc.). Occasional deopts at startup or on rare paths are normal. Deopts **matter** when they sit on a profiled hot function and repeat every iteration. +A deopt throws optimized code back to a slower tier because assumptions broke (unexpected map, elements kind, bounds, etc.). Occasional deopts at startup or on rare paths are normal. Deopts matter when they sit on a profiled hot function and repeat every iteration. **Static jsperf-lint cannot detect JIT outcomes.** IC state, deopt, and optimization tier are runtime facts. The jp-* rules only flag allocation/shape *smells*. JIT verification = profile / `--trace-deopt` / bench — never “the linter said so.” @@ -244,7 +236,7 @@ On this machine (Node v22.22.3), `deopt_probe` warmed `hotLoad` monomorphic then `[bailout … reason: wrong map … JSFunction hotLoad … Code TURBOFAN]` -One-shot `-e` scripts often also print “Insufficient type feedback …” — treat that as **startup noise**, not proof your smell is hot. +One-shot `-e` scripts often also print “Insufficient type feedback …” — treat that as startup noise, not proof the smell is hot. **Check (Chrome):** Performance panel → V8 markers where available; correlate with wall-clock before/after. No fake speedup percentages. @@ -252,11 +244,11 @@ One-shot `-e` scripts often also print “Insufficient type feedback …” — High level, modern V8/Node: -1. **Ignition** — bytecode interpreter; gathers type feedback. -2. **Maglev** — mid-tier optimizing compiler (present on current Node 22+ / recent V8 builds; exact enablement can depend on flags and workload). -3. **Turbofan** — top-tier optimizing compiler for hot code. +1. Ignition — bytecode interpreter; gathers type feedback. +2. Maglev — mid-tier optimizing compiler (present on current Node 22+ / recent V8 builds; exact enablement can depend on flags and workload). +3. Turbofan — top-tier optimizing compiler for hot code. -Code may climb tiers as it heats up and fall back on deopt. **Do not** overclaim “always Maglev on Node N” or pin exact tier thresholds — they move. For embeds without this pipeline (§3), skip tier talk entirely. +Code may climb tiers as it heats up and fall back on deopt. Do not claim “always Maglev on Node N” or pin exact tier thresholds — they move. For embeds without this pipeline (§3), skip tier talk entirely. ### 4.5 What jsperf-lint can and cannot see @@ -271,11 +263,11 @@ Code may climb tiers as it heats up and fall back on deopt. **Do not** overclaim | jp-string-plus-in-loop | removed | `push` + `join` was not about 2× on the matched `+=` shapes | | jp-map-filter-chain | chained `.map` / `.filter` / `.flatMap` on one line | Array length; a 3-row chain and a 35000-row chain are different costs | -Lint = allocation/shape smells. JIT verification = `jit-check.sh` / `--trace-deopt` / `examples/bench`. Polymorphic ICs leave **no** jp-* fingerprint. +Lint = allocation/shape smells. JIT verification = `jit-check.sh` / `--trace-deopt` / `examples/bench`. Polymorphic ICs leave no jp-* signal. ### 4.6 Measured on this machine (Node v22.22.3) -Ran 2026-10-21 18:49 Asia/Shanghai via `scripts/run-v8-benches.sh` → `examples/bench/out/`. Node **v22.22.3**, V8 **12.4.254.21-node.56**, linux x64. Warm-up 3 + 9 rounds; medians below. **Single-machine numbers only** — not a cross-machine claim. +Ran 2026-10-21 18:49 Asia/Shanghai via `scripts/run-v8-benches.sh` → `examples/bench/out/`. Node v22.22.3, V8 12.4.254.21-node.56, linux x64. Warm-up 3 + 9 rounds; medians below. Single-machine numbers only — not a cross-machine claim. | Bench | Variant | N | Median ms | Ops/s (approx.) | |-------|---------|---|-----------|-----------------| @@ -293,7 +285,7 @@ Full JSONL: `examples/bench/out/results.jsonl` (written by the script). ### 4.7 Correlation: static smell vs V8 time (PARTIAL) -Experiment: `examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh` (Node **v22.22.3**). +Experiment: `examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh` (Node v22.22.3). | Case | Static smell? | Ratio (smell/clean) | Agree? | |------|---------------|---------------------|--------| @@ -311,9 +303,9 @@ Experiment: `examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs ## 5. Startup and tooling (cold paths) -This chapter is about **tooling / cold-start / module-graph** cost — not the hot-loop shape and allocation smells in §1–§2 / §4. +This chapter is about tooling, cold-start, and module-graph cost — not the hot-loop shape and allocation smells in §1–§2 / §4. -Primary source: Marvin Hagemeister’s series **[Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)** (parts 1–13). Methods below are paraphrased; **do not treat his wall-clock numbers as ours** — re-measure on your machine. +Primary source: Marvin Hagemeister’s series [Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) (parts 1–13). Methods below are paraphrased; do not treat his wall-clock numbers as ours — re-measure on your machine. | Concern | Chapters | What finds it | |---------|----------|---------------| @@ -326,9 +318,9 @@ Before rewriting a library or CLI: 1. Capture a CPU profile: `node --cpu-prof your-tool.js …` (or `node --cpu-prof $(which npm) run …` for npm scripts — see [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/)). 2. Open the `.cpuprofile` in [speedscope](https://www.speedscope.app/) (or Chrome DevTools). -3. Ask: is time in **your** hot function, or in **loading / resolving** modules, casting, regex, FS? +3. Ask: is time in your hot function, or in loading / resolving modules, casting, regex, FS? -Marvin’s recurring method ([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)): find a top frame → open source → remove avoidable work → re-profile. Same loop as XPerf’s “measure before rewrite,” aimed at **build/lint/CLI** rather than app render loops. +Marvin’s recurring method ([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)): find a top frame → open source → remove avoidable work → re-profile. Same loop as XPerf’s “measure before rewrite,” aimed at build/lint/CLI rather than app render loops. ### 5.2 Hot casts, short-string regex, inner functions (part 1 methods) @@ -339,18 +331,18 @@ Paraphrased patterns from [part 1](https://marvinh.dev/blog/speeding-up-javascri - **Functions created inside hot outer functions** — closures recreated every call hurt ICs; lift helpers or pass args explicitly instead of capturing. - **Transpiler artifacts** — e.g. `for…of` rewritten into a fresh `_loop` function per iteration; inspect **published** output, not only source. -These can overlap §4 (ICs) when the path is hot at runtime. In tooling they often dominate **startup or per-file** cost instead. +These can overlap §4 (ICs) when the path is hot at runtime. In tooling they often dominate startup or per-file cost instead. ### 5.3 Module resolution (part 2 methods) From [part 2](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2/): resolution can outweigh parsing in lint/test/build traces. -Adopt as checklist (paraphrase of his four takeaways): +His four takeaways, paraphrased: -1. **Avoid FS** — every `stat`/`exists` is expensive at scale. -2. **Cache** resolved paths (watch mode: invalidate carefully). -3. **`fs.statSync(path, { throwIfNoEntry: false })`** — avoid throwing + capturing stack traces for “file missing” (ENOENT path). -4. **Limit upward `node_modules` walks** — especially when path aliases are mistaken for bare package imports. +1. Avoid FS — every `stat`/`exists` is expensive at scale. +2. Cache resolved paths (watch mode: invalidate carefully). +3. `fs.statSync(path, { throwIfNoEntry: false })` — avoid throwing + capturing stack traces for “file missing” (ENOENT path). +4. Limit upward `node_modules` walks — especially when path aliases are mistaken for bare package imports. Also watch **extension guessing** (`.js` → `.jsx` → … → `.ts`) and alias plugins that do not share the same resolver as TypeScript/`tsconfig` paths. @@ -370,7 +362,7 @@ From [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/) | | Runtime chapters (§1–§2, §4) | This chapter (§5) | |--|----------------------------|-------------------| | Typical win | Hot loop IC / GC / elements kind | Cold start, `npm run`, lint/test graph | -| Lint | the jp-* rules (line-window heuristics) | **None today** — document-only companions | +| Lint | the jp-* rules (line windows) | none today — document-only companions | | Evidence | `examples/bench/*`, `--trace-deopt` | `--cpu-prof` + speedscope; require-graph size | Do **not** interpret a clean jsperf-lint self-lint as “startup is fine.” Do **not** treat Marvin’s published speedups as transferable without a local profile. @@ -383,7 +375,7 @@ Do **not** interpret a clean jsperf-lint self-lint as “startup is fine.” Do ## 6. Constructors, JSON, DOM queries, and date formats -Each hint below is one measured shape. The hint line states that premise. A call that only looks similar is not given that rewrite. Default **`--scope all`** reports these smells everywhere — extreme stacks treat drip-by-drip cost as in-scope even off a loop. Opt in to **`--scope loop`** for the former in-loop-only match. Messages do not tell readers or models that non-loop sites can be ignored. +Each hint below is one measured shape. The hint line states that premise. A call that only looks similar is not given that rewrite. Default `--scope all` reports these smells everywhere — extreme stacks treat drip-by-drip cost as in-scope even off a loop. Opt in to `--scope loop` for the former in-loop-only match. Messages do not tell readers or models that non-loop sites can be ignored. `new RegExp(pattern)` compiles on every call when the pattern stays the same. A regex literal is cached. If the pattern is the loop variable, or `parts[i]`, the checker does not say to compile once. The bench compiled one fixed pattern, about 4.4× on Node 26. @@ -395,12 +387,12 @@ Each hint below is one measured shape. The hint line states that premise. A call `Date.now` was about 26× and `performance.now` about 19× versus one sample, in a tight loop that did not wait. A loop that contains `await`, or a `for await` loop, is not reported for the clock rule. Sample once when a fresh time is not required each call. A rate limiter that must read the clock each pass has nothing to move. -**jp-regex-ctor-hot** and **jp-json-hot** stay **hint** on the app and engine presets. JSONL exists for those two, and the 2026-10-22 verdict does not cover them. **jp-dom-query-hot** (about 16× on Node 26, a tiny linkedom document) and **jp-intl-datetimeformat-hot** (about 36× on Node 26, both sides still format) have JSONL and stay **hint**. They are outside that verdict. `--preset all` is error for every rule that still exists. +`jp-regex-ctor-hot` and `jp-json-hot` stay hint on the app and engine presets. JSONL exists for those two, and the 2026-10-22 verdict does not cover them. `jp-dom-query-hot` (about 16× on Node 26, a tiny linkedom document) and `jp-intl-datetimeformat-hot` (about 36× on Node 26, both sides still format) have JSONL and stay hint. They are outside that verdict. `--preset all` is error for every rule that still exists. `Reflect.get` was about 8× and was not given a rule. -Companion lint: **jp-regex-ctor-hot**, **jp-json-hot**, **jp-dom-query-hot**, **jp-intl-datetimeformat-hot**, **jp-readfile-sync-hot**, **jp-clock-hot**. -### Lint map +Lint: `jp-regex-ctor-hot`, `jp-json-hot`, `jp-dom-query-hot`, `jp-intl-datetimeformat-hot`, `jp-readfile-sync-hot`, `jp-clock-hot` (default hint). +## Lint map | ID | Topic | Severity | Evidence / note | |----|--------|----------|-----------------| @@ -417,9 +409,9 @@ Companion lint: **jp-regex-ctor-hot**, **jp-json-hot**, **jp-dom-query-hot**, ** | jp-clock-hot | `Date.now` / `performance.now` | **hint** | Node 26 about 70× / 110× with no `await`; sample once when fresh time is not required | | *(none)* | module resolution / eager graph / barrels | — | §5 — use `--cpu-prof` | -Severities above are the default `app` preset. Matching scope defaults to **`--scope all`** (everywhere); `--scope loop` restores in-loop-only matching. Policy: [`evidence-policy.md`](evidence-policy.md). Fixtures: `jsperf-lint/fixtures/{bad,good}/`. +Severities above are the default `app` preset. Matching scope defaults to `--scope all` (everywhere); `--scope loop` restores in-loop-only matching. Policy: [`evidence-policy.md`](evidence-policy.md). Fixtures: `jsperf-lint/fixtures/{bad,good}/`. -### References +## References - [corpus.md](corpus.md) - `examples/bench/` · `scripts/run-v8-benches.sh` · `scripts/jit-check.sh` · `scripts/run-static-vs-v8.sh` · `scripts/regen-evidence.sh` · `examples/bench/require_cost_sketch.js` diff --git a/packages/jsperf/docs/zh-CN/corpus.md b/packages/jsperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..0473bd0 --- /dev/null +++ b/packages/jsperf/docs/zh-CN/corpus.md @@ -0,0 +1,68 @@ +# jsperf 语料库目标 + +工作候选列表:引擎 + 热路径库,而不是随机的 npm 包。树不会随仓库一起提供;请将它们克隆到仓库外的任意本地目录(quickjs JS 树与 C 套件共享)。 + +```sh +node jsperf-lint/bin/jsperf-lint.js --format json --no-fail +``` + +检查器:**jsperf-lint**。 + +## 固定目标 + +| 树 | URL | 提交 | 范围 | +|------|-----|--------|-------| +| 节点 | https://github.com/nodejs/node | `796a83a7` | 浅层; `lib/**/*.js` | +| Preact | https://github.com/preactjs/preact | `74df25e` | `src/` | +| quickjs | 与 C 套件共享 — 参见 [cperf 语料库](../../../cperf/docs/zh-CN/corpus.md) | `7d1c13c` | `*.js`(跳过 `gen-*`) | +| TypeScript | https://github.com/microsoft/TypeScript | `538671d3c` | `packages/typescript/src` | + +## 是否 corpus-ready? + +**是的** — 重新设计后的 G3 2026-10-22(亚洲/上海)。默认剩余 **31× jp-string-plus-in-loop**(22 个 TP / 9 个 OK-idiom)。FP+DOC+SKIP **0%**。jp-delete-shape→`off`(之前是 100% OK-idiom)。分类注释未随版本发布(重新设计后)。 + +jp-delete-array-hole 保持 **错误**(G4)。当天 jp-string-plus-in-loop 和 jp-map-filter-chain 默认 **提示**(string-plus 于 2026-10-23 被移除;见下文)。jp-alloc-hot/jp-console-hot/jp-delete-shape 默认 **关闭**。 + +## 克隆 — g3e(2026-10-22 重新设计) + +| 树 | 大小 | 发现 g3c → g3e | +|------|------|--------------------| +| **节点 `lib/`** | ~7M `lib/` | **38 jp-delete-shape → 29 jp-string-plus-in-loop** | +| **Preact `src/`** | 3.3M | **0 → 0** | +| **quickjs JS** | (shared) | **0 → 0** | +| **TypeScript** | 源码 1.9M | **0 → 2** 循环中的 jp-string-plus | + +G3 重新设计:jp-delete-shape→关闭;jp-string-plus-in-loop string-append-in-loop;jp-map-filter-chain map/filter 链;冷格式化器 + gen-file 跳过。 + +目录跳过:`node_modules`、`.git`、`dist`、`coverage`、`tests`、`test`、`benchmarks`、`demo`、`fixtures`。 +文件跳过:`gen-*`、`*generate*`、`diagnosticFormatter.ts`。 +jp-string-plus-in-loop 和 jp-map-filter-chain 也跳过 inspect/debugger/test_runner/reporter/bench_runner/assert/myers_diff/cli_table 路径。 + +## 克隆订单 + +1. **已完成:** 节点 `lib/` + Preact + quickjs JS + TypeScript `packages/typescript/src` +2. 后续: 较旧的 TS 标签,包含大型 `src/compiler`,Solid / lodash-es + +## 小程序 / 嵌入焦点 + +保持指南建议可用于 **嵌入式 JS 和 小程序类** 运行时 (QuickJS)。 + +## 库存 + +```text +/ + node/ + preact/ + TypeScript/ + quickjs/ # shared with the C kit +``` + +## 2026-10-23 — 提示规则不在 2026-10-22 判决范围内 + +2026-10-22 的语料库准备判决不包括以下子弹头。它们仍然保持 **提示** 状态,尚未经过 G2/G3。 + +- `jp-regex-ctor-hot`,`jp-json-hot` — 磁盘上的 JSONL;仍然提示。绑定到循环的模式或文本未被报告。 +- `jp-dom-query-hot`,`jp-intl-datetimeformat-hot` — 磁盘上的 JSONL;仍然提示。绑定到循环的选择器、语言环境或选项值未被报告。 +- `jp-readfile-sync-hot`,`jp-clock-hot` — 磁盘上的 JSONL;仍然提示。`for` 可迭代对象不是循环体。使用 `await` 的循环不是 clock 基准。 +- `jp-map-filter-chain` — 磁盘上的 JSONL;仍然提示。长数组大约提高了 4.6×–9.8×;三行的 filter+map 提高了 1.28×。 +- `jp-string-plus-in-loop` — 在同一天移除。上面 2026-10-22 的残留计数是历史数据。匹配器不再运行,这些树也没有为本注释重新进行 lint。 diff --git a/packages/jsperf/docs/zh-CN/evidence-policy.md b/packages/jsperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..f282d37 --- /dev/null +++ b/packages/jsperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,54 @@ +# jsperf 证据政策 + +## 门槛 + +只有在 `packages/jsperf/evidence/` 下同时存在以下两者时,JP 规则才可以被记录/处理为 **`error`**: + +1. `.md` — 声明、利用、Node/V8 版本、限制 +2. `.jsonl` — ≥1 个前/后测量对,包含字段 `rule`、`case`、`node`、`v8`、`median_ms`、`ops`、`ratio`、`ts`(可选 `role`:`before` = 问题, `after` = 修复) + +否则规则是 **`hint`**(审查提示):适用于内部测试和固定组件,不是“必须修复以提升速度”的信号。 + +仅凭证据是**必要的,但不充分**来判断`错误`。只有当实际时间一致**且**失败模式符合规则的保持契约(形状 / 有孔 / 热 `删除`)时才升级。配置/垃圾回收或依赖环境的 I/O 异味,即使有 JSONL,也可能保持**`提示`** / **`关闭`**。 + +## 当前映射 + +截至2026-10-23。“默认”是 `app` 预设;`--preset all` 会将所有规则提升为错误。 + +| 规则 | 默认严重性 | 证据 | 测量比率 (Node v20.19.2 / V8 11.3.244.8-node.38) | +|------|----------|----------|--------------------------------------------------| +| jp-delete-shape | **关闭** | [jp-delete-shape.md](../../evidence/zh-CN/jp-delete-shape.md) · [jp-delete-shape.jsonl](../../evidence/jp-delete-shape.jsonl) | `delete` / tombstone **~474次**;JSONL 存在,但语料审核发现命中几乎全是正确用法 → **关闭**;使用 `--preset all` / `--severity jp-delete-shape=error` 启用 | +| jp-delete-array-hole | **错误** | [jp-delete-array-hole.md](../../evidence/zh-CN/jp-delete-array-hole.md) · [jp-delete-array-hole.jsonl](../../evidence/jp-delete-array-hole.jsonl) | 有空洞的 `delete a[i]` / 紧凑 **~3.2×** | +| jp-alloc-hot | **关闭**(默认) | [jp-alloc-hot.md](../../evidence/zh-CN/jp-alloc-hot.md) · [jp-alloc-hot.jsonl](../../evidence/jp-alloc-hot.jsonl) | JSONL ~19×/~6.4× 存在;G3 语料库几乎全部为正确用法 → **关闭**;可通过 `--preset all` / `--severity jp-alloc-hot=hint` 启用 | +| jp-console-hot | **提示** (`engine`) / **关闭** (`app`) | [jp-console-hot.md](../../evidence/zh-CN/jp-console-hot.md) · [jp-console-hot.jsonl](../../evidence/jp-console-hot.jsonl) | 循环中的 `console.log` / 安静 **~1328×**(丢弃接收器)— 真实但依赖环境 → 不算错误 | + +| jp-string-plus-in-loop | **已移除** | [墓碑](../../evidence/_removed/jp-string-plus-in-loop.REMOVED.md) | `push` + `join` 在匹配的形状上并非约 2 倍;匹配器已删除 | +| jp-map-filter-chain | **提示** | [jp-map-filter-chain.md](../../evidence/zh-CN/jp-map-filter-chain.md) · [jsonl](../../evidence/jp-map-filter-chain.jsonl) | 35000 行约 4.6× / 9.8×;一个 3 行的 filter+map 是 1.28×;保持提示 | +| jp-regex-ctor-hot | **提示** | 已测量(`jp-regex-ctor-hot.md` + `jp-regex-ctor-hot.jsonl`);保持提示 | `new RegExp`(稳定模式);未进行语料库筛选 | +| jp-json-hot | **提示** | 已测量(`jp-json-hot.md` + `jp-json-hot.jsonl`);保持提示 | `JSON.parse` / `stringify`(稳定文本);未进行语料库分类 | +| jp-dom-query-hot | **提示** | 测量 (`jp-dom-query-hot.md` + `jp-dom-query-hot.jsonl`); 保持提示 | DOM 查询(稳定选择器);在 Node 26 上约 16×,一个小的 linkedom 文档 | +| jp-intl-日期时间格式-hot | **提示** | 测量 (`jp-intl-datetimeformat-hot.md` + `jp-intl-datetimeformat-hot.jsonl`); 保持提示 | `new Intl.DateTimeFormat`; Node 26 大约 36×; 未进行语料库分类 | +| jp-readfile-sync-hot | **提示** | 测量过的 (`jp-readfile-sync-hot.md` + `jp-readfile-sync-hot.jsonl`); 保持提示 | `readFileSync`(稳定路径);未进行语料筛选 | +| jp-clock-hot | **提示** | 测量 (`jp-clock-hot.md` + `jp-clock-hot.jsonl`); 保持提示 | `Date.now` / `performance.now`; 未经过语料库分类 | + +提示文本说明测量的前提(未更改的路径、文本、模式、选择器或格式化程序;没有 `await` 的紧密循环用于时钟)。没有 JSONL 的规则不会命名替代项。仅看起来像基准的调用不会获得基准的重写。该策略是指南和 `--preset` 的**保留/升级**契约。 + +## 再生 + +```sh +cd packages/jsperf +bash scripts/regen-evidence.sh +# or: +# node examples/bench/evidence_jp003_jp004.js +# node examples/bench/evidence_jp001_jp002.js +# node examples/bench/evidence_jp005_jp006.js +``` + +在 Node 主要升级后重新运行;更新匹配的 `.md` 表格。 + +## CLI 预设 + +- 默认 `--preset app`:**jp-delete-array-hole=error**;**jp-map-filter-chain**、**jp-regex-ctor-hot**、**jp-json-hot**、**jp-dom-query-hot**、**jp-intl-datetimeformat-hot**、**jp-readfile-sync-hot** 和 **jp-clock-hot** = 提示;**jp-alloc-hot**、**jp-console-hot**、**jp-delete-shape** = 关闭(关闭规则见 G3 2026-10-22)。**jp-string-plus-in-loop** 已移除。 +- `engine`:与 app 相同,除了 **jp-console-hot** = 提示。 +- `all`:所有规则都是错误。 +- 覆盖:`--severity RULE=error|hint|off`(可重复;优先于预设)。 diff --git a/packages/jsperf/docs/zh-CN/js-performance-guide.md b/packages/jsperf/docs/zh-CN/js-performance-guide.md new file mode 100644 index 0000000..a8616ef --- /dev/null +++ b/packages/jsperf/docs/zh-CN/js-performance-guide.md @@ -0,0 +1,416 @@ +# JavaScript 性能指南(v0) + +稳定的 shape、更少的每轮分配——另有一章讲工具链 / 冷启动。 + +> **焦点:现代 Node 与 V8(inline cache / GC)。** QuickJS 与小程序类嵌入有专章(§3)——跳过需要 DOM 或完整浏览器 profiler 的建议。§4 只讲 V8(map、elements kind、deopt、Ignition → Maglev/Turbofan),不适用于 §3 运行时。§5 讲启动与工具链(模块解析、急切图、barrel),依据 Marvin Hagemeister 的生态系列;这类成本不在 jp-* 规则里。 +> +> 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。§4.6 只引用单机微基准中位数。§5 引用 Marvin 的方法(URL);他的计时请在本地重测。 +> +> 配套:`jsperf-lint/` —— jp-* 规则,见 [Lint 对照表](#lint-对照表)(行窗;非完整解析器)。已在 Node `lib/`、Preact `src/`、quickjs `*.js`(跳过测试)以及 TypeScript `packages/typescript/src` 上跑过(2026-10-21 Asia/Shanghai)。见 [corpus.md](corpus.md)。启动与 FS 成本对 jp-* 规则是带外的——见 §5。 + +先做 profile(`node --prof`、`node --cpu-prof` + speedscope、Chrome Performance、clinic)。优先一致的对象 shape 与批量分配,而不是抠单次属性访问。嵌入 / 小程序跳过 DOM 提示,也不要套用 Turbofan/Maglev。在 Node/Chrome 上用 §4 看 elements kind、IC 多态与 deopt。对 CLI、lint/测试跑手与包冷启动用 §5。 + +## 目录 + +- [0. 范围与方法](#0-范围与方法) +- [1. 分配与 shape](#1-分配与-shape) +- [2. `delete` 与 shape](#2-delete-与-shape) +- [3. 嵌入与小程序](#3-嵌入与小程序) +- [4. V8 优化(Node / Chrome)](#4-v8-优化node--chrome) +- [5. 启动与工具链(冷路径)](#5-启动与工具链冷路径) +- [6. 构造、JSON、DOM 查询与日期格式](#6-构造jsondom-查询与日期格式) +- [Lint 对照表](#lint-对照表) +- [参考](#参考) + +--- + +## 0. 范围与方法 + +1. 先管 shape —— 按同一顺序初始化字段;避免多态热路径(§1、§4.1)。 +2. 分配压力 —— `.push({…})`、`new`、非空对象绑定;默认作用域在循环外也会报(§1)。 +3. `delete` —— 热对象上优先墓碑或并行结构(§2);避免 `delete arr[i]` 打洞(§4.2、jp-delete-array-hole)。 +4. 嵌入 —— 同样的分配/shape 规则,不假设 `window` / DOM 或 V8 分层(§3)。 +5. V8 Node/Chrome —— elements kind、IC 状态、deopt、流水线素描(§4)。 +6. 启动 / 工具链 —— 用 `--cpu-prof` profile;削减 FS/解析;惰性加载稀有路径;留意 barrel(§5)。 +7. 测量 —— 重写前用 `--prof` / `--cpu-prof` / `--trace-deopt` / Performance 面板 / `examples/bench`。 + +来源:[XPerf playbook](../../../../docs/zh-CN/playbook.md) · [corpus.md](corpus.md) · [Marvin 系列](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/) + +--- +## 1. 分配与 shape + +### 1.1 Hidden class(shape) + +引擎(V8 及同类)按属性添加顺序分配 **hidden class** / map。热循环里分叉的 shape 会迫使多态或 megamorphic IC。 + +- 按一致顺序初始化全部字段(`return { x, y, z: 0 }`)。 +- 避免先加不同属性的 `if` 分支。 +- 见 `examples/m01_hidden_class.js`(示意;无计时声明)。 + +### 1.2 循环内分配(jp-alloc-hot) + +每轮分配会在循环很热时制造垃圾与 GC 停顿: + +| 模式 | Lint? | 备注 | +|------|--------|------| +| `.push({…})` / `.push([…])` | 是 | 经典 builder 噪声 | +| 循环内非空 `{…}` / `[…]` 绑定 | 是 | 每轮逃逸的 options bag | +| 循环内 `new Ctor` / `Object.create` | 是(r2+) | 除外 `throw new` / `Error` / `ERR_*` | +| 空 `{}` / `[]` 绑定 | **否**(r2) | 每重试一次的 opts bag 在 Node 上是噪声 | +| `throw new …` | 否 | 错误路径 | + +当 profile 显示有 GC 时间时,优先 typed array(`examples/p01_typed_array.js`)、对象池或预分配结构——不要默认改掉每一个 `.push`。 + +**证据(本机):** [`evidence/jp-alloc-hot`](../../evidence/zh-CN/jp-alloc-hot.md) —— 循环内 `new Uint8Array` 对比一次 **~19×**;`.push({i})` 对比预分配填充 **~6.4×**(Node v20.19.2)。机制是分配/GC(STATIC-VS-V8 对 deopt 为 PARTIAL)。默认严重级别 off(`--preset all` 或 `--severity jp-alloc-hot=hint` 开启)——见 [evidence-policy](evidence-policy.md)。默认 **`--scope all`** 在循环外也报;`--scope loop` 恢复仅循环内匹配。 + +### 1.3 语料命中(lint 计数) + +下表来自 lint 行窗的较早修订(r1–r3),展示启发式如何收窄;当前语料结果见 [corpus.md](corpus.md)。 + +| 语料 | r1 → r2 → r3 | 备注 | +|------|--------------|------| +| Node `lib/` | 60 → 179 → **115**(jp-alloc-hot **33 → 158 → 94**;jp-delete-shape 27 → 21 → 21) | r2 有意纳入 `new` / 非空绑定抬高了 jp-alloc-hot;r3 注释/`for` 误报削减。残留:`inspect.js` `new SafeMap()`(~16)、stream `pull.js` / `share.js` / `broadcast.js` 的 `IterResult` / `Uint8Array`、`abort_controller.js` 真实 `for` 里的 `??= new SafeSet()` | +| Preact `src/` | **0 → 0 → 0** | 干净基线——对框架核心规则可能偏窄(很少 `.push({` / 循环 `new`) | +| quickjs `*.js` | 5 → 4 → **2** jp-alloc-hot | 跳过 tests/;剩 codegen 味脚本 | +| TypeScript `packages/typescript/src` | — → 47 → **15** jp-alloc-hot | Go 迁移后 tip(~50 `.ts`);注释/循环修复有帮助。经典 `src/compiler` 不在本树 | + +**怎么读 Node 残留:** 真实循环里的 once-init `??= new` 往往是**真**分配(相对 `.push({` 的严重档仍开放)。`inspect.js` 的 SafeMap 构造可能邻接循环却非每元素热路径——评审,不要批量重写。 + +### 1.4 真正管用的反向优化 + +- 丢掉空 `{}`/`[]` 绑定(r2)——消灭 Node options-bag 噪声;保留 `.push({`。 +- 多行块注释 + 无花括号 `for` 修复(r3)——JSDoc 里的 `for` 与 `for (…)\n stmt;` 不再打开假循环(Console 原型安装等)。 +- 夹具:`good/empty_opts_in_loop.js`、`good/comment_for_not_loop.js`、`good/throw_new_in_loop.js`、`good/jp-alloc-hot_reuse_buffer.js`、`bad/jp-alloc-hot_new_in_loop.js`、`bad/jp-alloc-hot_push_object.js`。 + +### 1.5 循环里字符串 `+=`(已移除) + +`jp-string-plus-in-loop` 曾匹配字符串字面量或 `String.fromCharCode` 的 `+=`,并建议改成 `push` 再 `join`。在匹配器命中的 shape 上,该重写大约没有 2× 收益(Node v24.18.0,V8 13.6.233.17-node.50)。 + +| Shape | `+=` ms | `join` ms | `+=` / `join` | +| --- | ---: | ---: | ---: | +| `"\n"` × 100,重复 200 | 0.194 | 0.126 | 1.53 | +| `"\n"` × 22000,重复 20 | 1.648 | 2.756 | 0.60 | +| `"\n"` × 44000,重复 10 | 1.459 | 3.176 | 0.46 | +| `String.fromCharCode` × 22000,重复 10 | 0.893 | 2.647 | 0.34 | +| 64 字符字面量 × 700,重复 40 | 0.070 | 0.226 | 0.31 | +| `obj.s += "\n"` × 22000,重复 10 | 0.759 | 1.816 | 0.42 | + +比值接近 2 表示 `join` 快一倍。100 换行那例是 1.53×,未达门槛。22000 与 44000 字符时 `join` 更慢。V8 已对循环 `+=` 建串。`push` 再 `join` 还要分配指针数组。匹配器已删。墓碑:[`jp-string-plus-in-loop`](../../evidence/_removed/jp-string-plus-in-loop.REMOVED.md)。 + +### 1.6 链式 `.map` / `.filter`(jp-map-filter-chain) + +`.filter().map()` 每次调用分配中间数组。成本随长度与趟数增长。3 行列表与 35000 行表不是同一成本。 + +本机(与 §1.5 相同 Node)两边都构建结果数组: + +| Shape | 链式 ms | 单趟 ms | 比值 | +| --- | ---: | ---: | ---: | +| 3 行,filter 再 map,× 20000 | 0.289 | 0.226 | 1.28× | +| 3 行,四次 filter 再 map,× 20000 | 0.761 | 0.220 | 3.46× | +| 35000 行,filter 再 map,× 20 | 8.728 | 1.898 | 4.60× | +| 35000 行,四次 filter 再 map,× 20 | 12.850 | 1.306 | 9.84× | + +3 行 filter-then-map 低于 2×。3 行四趟高于 2×,但 20000 次仍在一毫秒内。35000 行才是单趟明显更快的地方。hint 点名长度。短列表重写可选。 + +Lint:**jp-map-filter-chain** —— 默认 hint。匹配前剥掉 `//` 与块注释。codegen `gen-*.js` 路径跳过。证据:[`jp-map-filter-chain`](../../evidence/zh-CN/jp-map-filter-chain.md)。 + +--- +## 2. `delete` 与 shape + +### 2.1 为什么热对象上的 `delete` 伤性能 + +`delete obj.key` 改变对象的 hidden class。引擎可能掉下 monomorphic IC;空洞在某些路径上会逼进 dictionary 模式。优先: + +- 字段可选且希望 shape 稳定时,用 `obj.key = undefined` / `null`。 +- 「是否存在」很常见时,用并行 `Set` / bitset / 稠密旁表。 +- 在冷路径重建新对象,而不是在热循环里 delete。 + +Lint:**jp-delete-shape** —— 成员表达式上的 `delete`(文件级启发式;默认不限循环)。实测门禁:[`evidence/jp-delete-shape`](../../evidence/zh-CN/jp-delete-shape.md)(相对墓碑约 **474×**,Node v20.19.2);JSONL 同意,但语料 triage 发现命中几乎全是 OK 惯用法(见 [evidence-policy](evidence-policy.md)),故默认 off(`--preset all` 或 `--severity jp-delete-shape=error` 开启)。 + +### 2.2 有意的 `delete`(bootstrap / 功能剥离) + +Node 的 `lib/internal/bootstrap/**` 用 `delete` 做模块缓存与功能剥离。那是启动 / 一次性,不是每请求热路径。 + +**jsperf-lint:** 对 jp-delete-shape 与 jp-delete-array-hole 跳过 `internal/bootstrap/` 下文件、匹配 `*bootstrap*` 的文件名,以及 inspect/debugger/test-runner 路径。 + +证据(语料命中): Node `lib/` jp-delete-shape 在 bootstrap 跳过后 **27 → 21**(r1→r2);r3 保持 **21**,同时 jp-alloc-hot 在动。剩余 delete 需逐案评审(CLI 拆卸、稀有 map)。 + +### 2.3 可选的未来 + +`--hot-only`(要求 jp-delete-shape 有循环上下文)尚未交付。在此之前,把循环外的 jp-delete-shape 当作 shape 味道,而不是自动 bug。 + +--- +## 3. 嵌入与小程序 + +### 3.1 同样的物理,更小的运行时 + +QuickJS、Hermes 类嵌入与小程序 JS 运行时仍要为这些付钱: + +- 每轮对象分配(jp-alloc-hot 模式)。 +- 经 `delete` 或分叉属性顺序导致的 shape 变化。 +- 紧循环里的日志(**jp-console-hot** —— `console.log` / `console.debug`)。证据:[`jp-console-hot`](../../evidence/zh-CN/jp-console-hot.md)(即便丢弃 stdout 仍约 **1300×** 对比安静)——默认 off(`app`)/ hint(`engine`),不是 CI error。 + +它们往往**没有**浏览器 Performance 面板、DOM 或大 IC 预算。以「打开 DevTools → …」开头的建议迁不过来。优先: + +- §1–§2 的分配/shape 纪律。 +- 宿主暴露批处理 API 时批量工作(`examples/p02_batch_note.js`),而不是每帧 microtask。 +- 用**宿主** profiler 测量(Node `--prof`、QuickJS 计时钩子、厂商小程序 perf 面板)——不是仅 Chrome 工作流。 + +不要把 §4(Maglev/Turbofan、elements-kind 迁移、`--trace-deopt`)套到这些运行时。稳定 shape 与更少分配仍然有用;V8 分层话术没有。 + +### 3.2 共享的 quickjs 树 + +cperf 在 **C** 引擎上跑;jsperf 在 旁边的 `*.js`(跳过测试与 `gen-*`)。当前规则在那里报 **0**([corpus.md](corpus.md));较早修订报过 2 个 jp-alloc-hot。压力低,但作为邻接嵌入的样本有用。不要指望生成脚本有 Preact 级干净。 + +### 3.3 这里不该推荐什么 + +- 把 DOM 批处理 / `requestAnimationFrame` 当主修复。 +- `innerHTML` vs `createElement` 争论。 +- 仅浏览器 API(嵌入可能不提供的 `WeakRef` + FinalizationRegistry 模式)。 +- §4 的 Turbofan / Maglev / elements-kind 调优。 + +Typed array 与一致 shape 仍然可移植(`examples/p01_typed_array.js`、`m01_hidden_class.js`)。 + +--- +## 4. V8 优化(Node / Chrome) + +本章针对 **V8** 深化 §1。不适用于 QuickJS、Hermes 类嵌入或小程序运行时——见 §3。那些引擎共享「稳定 shape / 更少分配」的物理;它们**得不到** Ignition → Maglev/Turbofan 建议、elements-kind 迁移或 `--trace-deopt` 工作流。 + +按 **Node 22+ / 现代 V8 连续体** 标注日期(测于 Node v22.22.3,V8 12.4.x)。流水线层级名与标志会漂;把下面的版本钉当作「我们跑过什么」,不是永远保证。 + +### 4.1 Hidden class / map 与 IC(扩展 §1.1) + +V8 给每个对象挂一个 **map**(hidden class)。属性添加顺序与类型反馈建过渡树。属性读写走 **内联缓存(IC)**: + +| IC 状态 | 含义(实践) | +|---------|--------------| +| Monomorphic | 只见过一个 map —— 热读写的最佳情况 | +| Polymorphic | 少数 map(小固定集)—— 仍可优化,更慢 | +| Megamorphic | 很多 map —— IC 放弃;走通用慢路径 | + +**要做:** 用一种字段顺序构造(`{ x, y, z: 0 }`);让热调用点落在同一 shape。**不要:** 分支成有的对象先 `x` 后 `y`、有的反过来;不要在热对象上 `delete` 字段(§2)。 + +微基准:`examples/bench/shape_mono_vs_poly.js`(同序 vs 分叉构造;数字见 §4.6)。 + +### 4.2 Elements kind + +数组不是单一表示。V8 跟踪 **elements kind**,大致: + +| Kind(概念) | 典型内容 | +|--------------|----------| +| `PACKED_SMI_ELEMENTS` | 稠密小整数,无洞 | +| `PACKED_DOUBLE_ELEMENTS` | 稠密浮点(出现非 Smi 数之后) | +| `PACKED_ELEMENTS` | 稠密对象/值混合 | +| `HOLEY_*` | 同族**带洞**(缺索引) | + +**实践要/不要:** + +- 优先稠密 `push` / 预分配(`new Array(n)` 再填 0..n-1),而不是稀疏写。 +- **不要打洞:** `delete arr[i]`,或跳索引(`arr[0]=1; arr[2]=2`),会逼进 holey kind 与更弱的优化代码。 +- 数值热循环优先 **typed array**(`Float64Array`、`Uint32Array` …):固定元素类型、无 holey 迁移、连续存储。普通 `number[]` 可以保持 packed,但混合 Smi/double 迁移与装箱仍有成本。 +- 读过 `length` 或把洞当「可选槽」是 shape 味道;必须时用并行长度、typed 位掩码,或在**packed** 数组上用 `undefined` 墓碑。 + +Lint:**jp-delete-array-hole** 标记 `delete arr[i]` 式成员删除(holey 风险);实测门禁 [`evidence/jp-delete-array-hole`](../../evidence/zh-CN/jp-delete-array-hole.md)(约 **3.2×**)。jp-delete-shape 覆盖普通成员上的 `delete obj.key`;历史上漏掉计算索引删除——jp-delete-array-hole 补上。 + +微基准:`examples/bench/elements_packed_vs_holey.js`、`examples/bench/typed_array_vs_number_array.js`。 + +### 4.3 Deopt —— 何时要紧;怎么查 + +**Deopt** 因假设被打破(意外 map、elements kind、边界等)把优化代码打回更慢层级。启动时或稀有路径上偶发 deopt 正常。当它落在 profile 到的热函数上并每轮重复时,deopt **要紧**。 + +**静态 jsperf-lint 无法检测 JIT 结果。** IC 状态、deopt 与优化层级是运行时事实。jp-* 规则只标分配/shape *味道*。JIT 验证 = profile / `--trace-deopt` / bench —— 绝不是「linter 说了算」。 + +**运行时 harness(不是 lint 规则):** + +```sh +# 跨 Node 版本优先: +node --trace-deopt examples/bench/deopt_probe.js +bash scripts/jit-check.sh # 写入 examples/bench/out/deopt-sample.txt + +# 可选 / 脆弱:V8 natives(状态码会漂) +USE_NATIVES=1 node --allow-natives-syntax --trace-deopt examples/bench/deopt_probe.js +``` + +本机(Node v22.22.3)上,`deopt_probe` 先把 `hotLoad` 暖成 monomorphic 再喂分叉 map;`--trace-deopt` 报告: + +`[bailout … reason: wrong map … JSFunction hotLoad … Code TURBOFAN]` + +一次性 `-e` 脚本也常打印 “Insufficient type feedback …” —— 当作**启动噪声**,不是味道已热的证明。 + +**检查(Chrome):** Performance 面板 → 可用处的 V8 标记;与改前/改后墙钟对照。不要编造加速百分比。 + +### 4.4 流水线素描(Ignition → Maglev / Turbofan) + +高层上,现代 V8/Node: + +1. **Ignition** —— 字节码解释器;收集类型反馈。 +2. **Maglev** —— 中层优化编译器(当前 Node 22+ / 近版 V8 有;确切启用可取决于标志与负载)。 +3. **Turbofan** —— 热代码的顶层优化编译器。 + +代码升温时可能爬层级,deopt 时回落。不要过度声称「Node N 上总是 Maglev」或钉死确切层级阈值——它们会动。对没有这条流水线的嵌入(§3),完全跳过层级话术。 + +### 4.5 jsperf-lint 能看见与不能看见什么 + +**硬限制:** 静态 jp-* 规则**不能**识别 JIT 优化成败。没有规则观察 monomorphic vs mega IC、Maglev/Turbofan 层级或 deopt。 + +| 规则 | 看见 | **看不见** | +|------|------|------------| +| jp-alloc-hot | `.push({`、非空绑定、清晰的 `new` 容器(默认 scope all) | IC 多态;分配是否导致 deopt(通常不会——导致 GC) | +| jp-console-hot | `console.*`(默认 scope all) | 即使不在热路径上,日志是否值得门控 | +| jp-delete-shape | `delete obj.key` 式成员 | 是否已是 dictionary 模式;deopt vs「只是慢」 | +| jp-delete-array-hole | `delete arr[i]`(holey) | 运行时 elements kind;无 `delete` 的跳索引打洞 | +| jp-string-plus-in-loop | 已移除 | 匹配到的 `+=` shape 上 `push` + `join` 大约没有 2× | +| jp-map-filter-chain | 一行上的链式 `.map` / `.filter` / `.flatMap` | 数组长度;3 行链与 35000 行链成本不同 | + +Lint = 分配/shape 味道。JIT 验证 = `jit-check.sh` / `--trace-deopt` / `examples/bench`。多态 IC **没有** jp-* 指纹。 + +### 4.6 本机实测(Node v22.22.3) + +经 `scripts/run-v8-benches.sh` → `examples/bench/out/`,跑于 2026-10-21 18:49 Asia/Shanghai。Node **v22.22.3**,V8 **12.4.254.21-node.56**,linux x64。预热 3 + 9 轮;下表为中位数。**仅单机数字** —— 不是跨机声明。 + +| Bench | 变体 | N | 中位 ms | Ops/s(约) | +|-------|------|---|----------|-------------| +| shape_mono_vs_poly | mono(同字段序) | 2e5 | **0.219** | ~9.1e8 | +| shape_mono_vs_poly | poly(分叉序) | 2e5 | **1.435** | ~1.4e8 | +| elements_packed_vs_holey | packed fill+sum | 2e5 | **1.227** | ~1.6e8 | +| elements_packed_vs_holey | 经 `delete` 的 holey | 2e5 | **4.589** | ~4.4e7 | +| elements_packed_vs_holey | 经跳索引的 holey | 2e5 | **1.445** | ~1.4e8 | +| typed_array_vs_number_array | 稠密 `number[]` fill+sum | 1e6 | **5.217** | ~1.9e8 | +| typed_array_vs_number_array | `Float64Array` fill+sum | 1e6 | **5.948** | ~1.7e8 | + +**怎么读:** 此处 poly shape 比 mono 慢约 **6.5×**;`delete`-holey 比 packed 慢约 **3.7×**。跳索引 holey 写的槽更少——比较 kind 行为,不要只比原始速度。在本稠密 double 负载上 `number[]` **略快于** `Float64Array`;typed array 仍对固定元素类型、避免 packed→holey / Smi→double 迁移以及二进制/WASM 边界有意义——测你的循环。 + +完整 JSONL:`examples/bench/out/results.jsonl`(脚本写出)。 + +### 4.7 对照:静态味道 vs V8 时间(PARTIAL) + +实验:`examples/bench/static_vs_v8_correlate.js` · `scripts/run-static-vs-v8.sh`(Node **v22.22.3**)。 + +| 案例 | 静态味道? | 比值(味道/干净) | 同意? | +|------|------------|-------------------|--------| +| Mono vs poly shape | 仅 poly | **12.0×** | **YES**(+ mono→poly 上的 `wrong map` deopt) | +| Packed vs holey `delete a[i]` | 类 jp-delete-array-hole | **5.47×** | **YES**(时间);deopt 嘈杂 | +| `Float64Array` vs holey `number[]` | holey | **1.34×** | **PARTIAL**(弱化) | +| `delete obj.k` vs `= undefined` | jp-delete-shape | **165×** | **YES**(时间);deopt 计数相近 | +| 对照:循环 `new` vs 一次 | jp-alloc-hot | **49.3×** | **PARTIAL** —— 分配/GC 痛,不是 deopt | + +**不要假装 jp-* 规则等于 JIT 意识。** 对 shape / holey / 热 `delete` 同意最强;用 deopt 行数当神谕,或推销 typed array 总是快于 packed double 时最弱。 + +**JSONL 门禁(本机 Node v20.19.2,重跑 `scripts/regen-evidence.sh`):** jp-delete-array-hole 约 **3.2×** → error;jp-delete-shape 约 **474×**(语料命中几乎全是 OK 惯用法)→ off;jp-alloc-hot 约 **19×**(分配/GC)→ off;jp-console-hot 约 **1328×**(丢弃 sink,依赖环境)→ off(`app`)/ hint(`engine`)。见 [`evidence-policy`](evidence-policy.md) 与 `evidence/jp-*.md`。 + +--- +## 5. 启动与工具链(冷路径) + +本章讲 **工具链 / 冷启动 / 模块图** 成本——不是 §1–§2 / §4 的热循环 shape 与分配味道。 + +主要来源:Marvin Hagemeister 系列 **[Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)**(第 1–13 部分)。下列方法经转述;**不要把他的墙钟数字当成我们的**——在你的机器上重测。 + +| 关注点 | 章节 | 靠什么发现 | +|--------|------|------------| +| 运行时 shape / 分配 / holey / `delete` | §1、§2、§4 | jp-* 规则 + `examples/bench` | +| 模块解析、急切 `require`、barrel、CLI 开销 | §5 | `node --cpu-prof` / speedscope、require 图检查 —— **不是** jp-* 规则 | + +### 5.1 先做 profile(工具链) + +重写库或 CLI 之前: + +1. 抓 CPU profile:`node --cpu-prof your-tool.js …`(或对 npm 脚本 `node --cpu-prof $(which npm) run …` —— 见 [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/))。 +2. 在 [speedscope](https://www.speedscope.app/)(或 Chrome DevTools)打开 `.cpuprofile`。 +3. 问:时间在**你的**热函数里,还是在**加载 / 解析**模块、类型转换、正则、FS? + +Marvin 反复使用的方法([part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)):找顶帧 → 打开源码 → 去掉可避免的工作 → 再 profile。与 XPerf「重写前先测」同一循环,瞄准 **构建/lint/CLI** 而非应用渲染循环。 + +### 5.2 热转换、短串正则、内层函数(part 1 方法) + +转述自 [part 1](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)(PostCSS、SVGO 及邻居)的模式: + +- **字符串 ↔ 数字搅动** —— 例如紧循环里 `toFixed` 再 `+` 回数字。优先留在数字域(`Math.round` / 按 `10**p` 缩放)。 +- **对很多短串跑正则** —— 用一条正则匹配许多细碎序列化片段,往往差于对更长串单趟,或先做「针是否存在」的廉价预检。 +- **热外层函数内创建的函数** —— 每次调用重建闭包伤 IC;提升助手或显式传参,而不是捕获。 +- **转译器产物** —— 例如 `for…of` 被改写成每轮新鲜 `_loop` 函数;检查**已发布**输出,不要只看源码。 + +路径在运行时很热时,这些可与 §4(IC)重叠。在工具链里它们往往主导**启动或每文件**成本。 + +### 5.3 模块解析(part 2 方法) + +来自 [part 2](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2/):在 lint/测试/构建轨迹里,解析可以压过解析(parsing)。 + +当作检查清单(转述他的四点): + +1. **少碰 FS** —— 规模一大,每次 `stat`/`exists` 都贵。 +2. **缓存** 已解析路径(watch 模式:小心失效)。 +3. **`fs.statSync(path, { throwIfNoEntry: false })`** —— 避免为「文件不存在」(ENOENT)抛错并抓栈。 +4. **限制向上走 `node_modules`** —— 尤其当路径别名被误当成裸包导入时。 + +也留意**扩展名猜测**(`.js` → `.jsx` → … → `.ts`)以及与 TypeScript/`tsconfig` paths 不同解析器的别名插件。 + +**jsperf-lint 看不见这些。** 没有 jp-* 规则测 FS 或解析。配套:CPU profile、解析器缓存,以及 `examples/bench/require_cost_sketch.js` 里急切 vs 惰性素描。 + +### 5.4 启动:急切图、barrel、「抱着香蕉的大猩猩」 + +来自 [part 4](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-4/)(npm 脚本)与 [part 7](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/)(barrel): + +- **急切 `require` / `import`** 预先加载整个可达图。错误格式化器、审计代码、进度条——快乐路径用不到的——应**惰性**(`if (err)` 分支内 `require`,稀有命令用动态 `import()`)。 +- **肥类 / mixin 堆** 拉来「抱着香蕉和整片丛林的大猩猩」(Joe Armstrong,经 Marvin):一个入口类 mixin 了 audit + reify + rebuild,而你只需要 `run`。 +- **Barrel 文件**(只 `export * from …` 的 `index.js`)放大图:一次导入可拉来一目录树;按文件 spawn 的测试跑手反复付钱。启动或 lint/测试墙钟要紧时,优先直接导入。 +- CLI 的**已发布 bundle** 可缩小加载成本;要测——巨大单文件有解析权衡。 + +### 5.5 与 jp-* 规则的关系 + +| | 运行时章节(§1–§2、§4) | 本章(§5) | +|--|------------------------|-----------| +| 典型收益 | 热循环 IC / GC / elements kind | 冷启动、`npm run`、lint/测试图 | +| Lint | jp-* 规则(行窗规则) | **今天没有** —— 仅文档配套 | +| 证据 | `examples/bench/*`、`--trace-deopt` | `--cpu-prof` + speedscope;require 图大小 | + +不要把干净的 jsperf-lint 自检解读成「启动没问题」。不要在没有本地 profile 的情况下把 Marvin 发表的加速当可迁移结论。 + +### 5.6 可选素描:急切 vs 惰性 require + +`examples/bench/require_cost_sketch.js` 建一个小夹具图,计时顶层急切 `require` vs 稀有分支内惰性 `require`。这是**教学微基准**,不是关于 npm 或你的应用的声明。运行:`node examples/bench/require_cost_sketch.js`。 + +--- +## 6. 构造函数、JSON、DOM 查询与日期格式 + +下列每条 hint 对应一种实测 shape。hint 行陈述该前提。只是看起来像的调用不给那套重写。默认 **`--scope all`** 到处报告这些味道——极端栈把点滴成本也算进范围,即便不在循环里。可选用 **`--scope loop`** 恢复从前的仅循环内匹配。消息不会告诉读者或模型可以忽略非循环位点。 + +`new RegExp(pattern)` 在模式不变时每次调用都编译。正则字面量会被缓存。若模式是循环变量或 `parts[i]`,检查器不会说编译一次。基准编译一个固定模式,Node 26 上约 4.4×。 + +对不变文本的 `JSON.parse`,相对解析一次,Node 26 上约 140×。对不变值的 `JSON.stringify` 是同一 shape。JSONL 行或 `rows[i]` 每轮文本不同。检查器跳过参数就是循环绑定的情况。它不会让你把那些行折叠成一次 parse。 + +对不变选择器的 `querySelector` / `getElementById`,在 Node 26、很小的 linkedom 文档上约 16×。由循环下标拼出的选择器不是那个基准。用不变 locale 与选项的 `new Intl.DateTimeFormat`,Node 26 上约 36×;两边仍会 format。`new Date` 不是本规则。 + +对不变路径的 `fs.readFileSync`,相对读一次,Node 26 上约 114×。在 `--scope loop` 下,只匹配 `{` 后的循环体(只跑一次读的 `for` 可迭代随后被跳过)。默认 `--scope all` 下,路径稳定时无论出现在哪都会报。每轮变化的路径(循环绑定)不报。 + +`Date.now` 约 26×,`performance.now` 约 19×,相对只采样一次,在不 wait 的紧循环里。含 `await` 的循环,或 `for await` 循环,时钟规则不报。不需要每次新鲜时间时采样一次。必须每轮读时钟的限流器没有可搬的东西。 + +**jp-regex-ctor-hot** 与 **jp-json-hot** 在 app 与 engine preset 上保持 hint。两者有 JSONL,2026-10-22 裁决未覆盖。**jp-dom-query-hot**(Node 26、很小 linkedom 约 16×)与 **jp-intl-datetimeformat-hot**(Node 26 约 36×,两边仍 format)有 JSONL 且保持 hint。它们在该裁决之外。`--preset all` 对仍存在的每条规则是 error。 + +`Reflect.get` 约 8×,未给规则。 + +Lint: `jp-regex-ctor-hot`, `jp-json-hot`, `jp-dom-query-hot`, `jp-intl-datetimeformat-hot`, `jp-readfile-sync-hot`, `jp-clock-hot`(默认 hint) +### Lint 对照表 + +| ID | 主题 | 严重级别 | 证据 / 备注 | +|----|------|----------|-------------| +| jp-alloc-hot | 对象/数组/`new` 分配 | off(`all`:error) | [`jp-alloc-hot`](../../evidence/zh-CN/jp-alloc-hot.md) ~19× / ~6.4× —— GC/分配,非 deopt | +| jp-console-hot | `console.*` | hint(`engine`)/ off(`app`) | [`jp-console-hot`](../../evidence/zh-CN/jp-console-hot.md) ~1328× 丢弃 sink —— 依赖环境 | +| jp-delete-shape | 普通成员上的 `delete` | off(`all`:error) | [`jp-delete-shape`](../../evidence/zh-CN/jp-delete-shape.md) 相对墓碑 ~474×;语料命中几乎全是 OK 惯用法 | +| jp-delete-array-hole | 计算索引上的 `delete`(holey 数组) | error | [`jp-delete-array-hole`](../../evidence/zh-CN/jp-delete-array-hole.md) 相对 packed ~3.2× | +| jp-map-filter-chain | 链式 `.map` / `.filter` / `.flatMap` | hint | 长数组约 4.6×–9.8×;3 行 filter+map 为 1.28× | +| jp-regex-ctor-hot | `new RegExp`,模式非循环绑定 | hint | Node 26 固定模式约 4.4×;优先字面量或一次共享编译 | +| jp-json-hot | `JSON.parse` / `stringify`,文本非循环绑定 | hint | Node 26 不变文本约 140×;稳定时复用 | +| jp-dom-query-hot | DOM 查询,选择器非循环绑定 | hint | Node 26、很小 linkedom 上 JSONL 约 16×;未做语料 triage | +| jp-intl-datetimeformat-hot | `new Intl.DateTimeFormat` | hint | Node 26 约 36×;两边仍 format;未做语料 triage | +| jp-readfile-sync-hot | `readFileSync`,路径非循环绑定 | hint | Node 26,不变路径约 114× | +| jp-clock-hot | `Date.now` / `performance.now` | hint | Node 26 无 `await` 时约 70× / 110×;不需要新鲜时间时采样一次 | +| *(无)* | 模块解析 / 急切图 / barrel | — | §5 —— 用 `--cpu-prof` | + +上表严重级别为默认 `app` preset。匹配作用域默认为 **`--scope all`**(到处);`--scope loop` 恢复仅循环内匹配。策略:[`evidence-policy.md`](evidence-policy.md)。夹具:`jsperf-lint/fixtures/{bad,good}/`。 + +### 参考 + +- [corpus.md](corpus.md) +- `examples/bench/` · `scripts/run-v8-benches.sh` · `scripts/jit-check.sh` · `scripts/run-static-vs-v8.sh` · `scripts/regen-evidence.sh` · `examples/bench/require_cost_sketch.js` +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- Marvin Hagemeister — [Speeding up the JavaScript ecosystem](https://marvinh.dev/blog/speeding-up-javascript-ecosystem/)(第 1–13 部分;§5 引用方法) diff --git a/packages/jsperf/docs/zh-CN/rule-ids.md b/packages/jsperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..092af89 --- /dev/null +++ b/packages/jsperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,43 @@ +# jsperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `jp-alloc-hot` | +| `jp-console-hot` | +| `jp-delete-shape` | +| `jp-delete-array-hole` | +| `jp-map-filter-chain` | +| `jp-regex-ctor-hot` | +| `jp-json-hot` | +| `jp-dom-query-hot` | +| `jp-intl-datetimeformat-hot` | +| `jp-readfile-sync-hot` | +| `jp-clock-hot` | +| `jp-query-all-star` | +| `jp-execjs-in-timer` | +| `jp-clone-per-event` | + +Matching scope defaults to **`--scope all`** (report everywhere). Opt in to **`--scope loop`** for the former in-loop-only match. Former `*-in-loop` slugs were renamed to `*-hot` (see Renamed below). + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `jp-alloc-in-loop` | `jp-alloc-hot` | +| `jp-clock-in-loop` | `jp-clock-hot` | +| `jp-console-in-loop` | `jp-console-hot` | +| `jp-dom-query-in-loop` | `jp-dom-query-hot` | +| `jp-intl-datetimeformat-in-loop` | `jp-intl-datetimeformat-hot` | +| `jp-json-in-loop` | `jp-json-hot` | +| `jp-readfile-sync-in-loop` | `jp-readfile-sync-hot` | +| `jp-regex-ctor-in-loop` | `jp-regex-ctor-hot` | + +## Removed (tombstone; never reuse) + +- `jp-string-plus-in-loop` — `push` + `join` was not ~2× on the matched shapes. See `evidence/_removed/`. + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/jsperf/evidence/zh-CN/jp-alloc-hot.md b/packages/jsperf/evidence/zh-CN/jp-alloc-hot.md new file mode 100644 index 0000000..f3f3953 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-alloc-hot.md @@ -0,0 +1,43 @@ +# jp-alloc-hot — 分配类模式 + +## 主张 + +热的每次迭代分配(清空容器/缓冲区的 `new`,`.push({…})`)在 V8/Node 上比重用/预分配更慢,主要是由于 **分配/垃圾回收成本**,而不是由于去优化/IC 失败。 + +## 如何测量 + +- 测试工具:`packages/jsperf/examples/bench/evidence_jp001_jp002.js` +- 配对 A(对应 `static_vs_v8_correlate.js` 的案例 5):N=200000;预热=3;取 9 次的中位数 + - **之前:** 循环内使用 `new Uint8Array(16)` + - **之后:** 循环外创建一个 `Uint8Array`;在循环中修改它 +- 配对 B:N=200000 + - **之前:** `.push({ i })` 添加元素 + - **之后:** `new Array(N)` 填充 + 求和(语义不同 — 仅用于速度参考) +- 原始数据行:[`jp-alloc-hot.jsonl`](../jp-alloc-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海(本地时间;JSONL 中的 ISO 时间戳为 UTC) | +| Node | **v20.19.2** | +| V8 | **11.3.244.8-node.38** | +| 循环前的中位数 (`new_in_loop`) | ~4.66 毫秒 | +| (`new_once_outside`) 之后的中位数 | ~0.24 毫秒 | +| **ratio new_in_loop / once** | **~19×** | +| `push_object_literal` 之前的中位数 | ~10.9 毫秒 | +| (`prealloc_fill_sum`) 后的中位数 | ~1.69 毫秒 | +| **推送/预分配比率** | **~6.4×** | + +早期在 Node v22.22.3 / V8 12.4 上的相关性显示 Uint8Array 对 (`scripts/run-static-vs-v8.sh`) 约为 ~**49×**。方向稳定;幅度受 Node/机器影响。 + +## 与 lint 的对应 + +`jsperf-lint` jp-alloc-hot 标记 `.push({` / 非空绑定器 / 清除 `new` / `Object.create`。默认 `--scope all` 也会报告循环外的这些模式;`--scope loop` 则只匹配循环内的前者。默认严重性在 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 下为 **`off`**(使用 `--preset all` 或 `--severity jp-alloc-hot=hint` 可启用):实测时间一致,但机制是 GC/分配 — **不是** JIT/去优化门控 — 并且遗留的 Node `lib/` 命中需要人工审查(SafeMap、一次初始化、流)。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- Pair B 改变结果形状(对象 vs 整数)——仅作为“构建者税”示例使用。 +- **不要**将 jp-alloc-hot 与 deopt/IC 失败等同(STATIC-VS-V8 案例 5)。 +- Marvin §05(启动/工具)不在此运行时检查范围内。 diff --git a/packages/jsperf/evidence/zh-CN/jp-clock-hot.md b/packages/jsperf/evidence/zh-CN/jp-clock-hot.md new file mode 100644 index 0000000..a71c7e3 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-clock-hot.md @@ -0,0 +1,25 @@ +# jp-clock-hot — 测量过的,仍有暗示 + +## 状态 +**提示**。实时时钟 JSONL:[jp-clock-hot.jsonl](../jp-clock-hot.jsonl)。未进行语料库分类。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 +`Date.now` 和 `performance.now`,每个都与循环外的一个样本进行比较。`new Date` 不适用此规则。 + +节点 **v26.12.1**,V8 **14.6.202.34-node.40**,linux x64,负载 0.61。预热 3,9 的中位数。测试工具:`examples/bench/evidence_jp_clock.js`。之前的 Node 24 对大约是 26× / 19×。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `date_now_once` | 0.043 | 100000 | 1 | +| 之前 | `date_now_each` | 2.982 | 100000 | 69.712 | +| 之后 | `perf_now_once` | 0.027 | 100000 | 1 | +| 之前 | `perf_now_each` | 2.980 | 100000 | 110.209 | + +`date_now_once` 在两个样本较慢(0.297 毫秒、0.125 毫秒)之后,其余样本稳定在 0.029–0.051 毫秒。中位数位于该集群中。相对于最慢的样本,两边仍然约有 10 倍的差距。`performance.now` 的样本非常集中。采样一次后不会在循环内部读取时钟。需要每次循环获取新时间戳的代码无法提升。默认保持 **hint**。这个比率并不是通用的加速。 + +## 匹配器 + +上面的比率是一个不等待的紧循环。包含 `await` 的循环和 `for await` 循环不会被报告。默认的 `--scope all` 也会报告循环外的时钟样本,当每次调用不需要新的时间时;`--scope loop` 则保持前者仅在循环内匹配。速率限制或必须在每次迭代中读取时钟的工作线程没有任何可移动的内容。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或另一个 ABI 都可能抹去差异。在进行任何默认更改之前,仍然需要语料库分流。 diff --git a/packages/jsperf/evidence/zh-CN/jp-console-hot.md b/packages/jsperf/evidence/zh-CN/jp-console-hot.md new file mode 100644 index 0000000..94db44a --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-console-hot.md @@ -0,0 +1,40 @@ +# jp-console-hot — `console.*` + +## 主张 + +在热循环中调用 `console.log`(以及相关方法)比在安静循环中慢得多,即使标准输出被丢弃——格式化和控制台管道是主要因素。真实的 TTY / 管道通常更慢,并且**依赖环境**。 + +## 如何测量 + +- 测试工具:`packages/jsperf/examples/bench/evidence_jp001_jp002.js` +- N_CONSOLE=20000;热身=3;9 次的中位数 +- **之前:** 每次迭代 `console.log(i)` (stdout `write` 重定向到丢弃的 `Writable`) +- **之后(安静):** 相同的算术操作,没有控制台输出 +- **之后(一次):** 安静循环 + 循环后一次 `console.log("done", s)` +- 原始数据行:[`jp-console-hot.jsonl`](../jp-console-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 | +| Node | **v20.19.2** | +| V8 | **11.3.244.8-node.38** | +| 循环中 `console_log_in_loop` 之前的中位数 | ~27.2 毫秒 | +| (`quiet_loop`) 之后的中位数 | ~0.021 毫秒 | +| **比率 控制台/安静** | **~1328×** | +| 中位数 `console_once_after` | ~0.11 毫秒 (~安静5倍;与循环内相比仍然很小) | + +## 与 lint 的对应 + +jp-console-hot 标志 `console.*`(默认 `--scope all` 包含循环外的网站;`--scope loop` 保持原来的仅循环内匹配)。严重性在 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 下保持 **`hint`** (`engine`) / **`off`** (`app`): + +- 即使没有 TTY,这台机器的成本也是**真实的**。 +- 仍然不是一个 `错误` 门:有意的调试循环、测试辅助工具和 CLI 进度打印是常见的;比例随着接收端(丢弃 vs 管道 vs TTY)和日志库而变化。 +- 将其视为对极端堆栈的审查提示(即使在非热点路径上,成本也会累积),而不是应用预设下“必须修复以提高速度”的 CI 失败。 + +## 限制 + +- 丢弃接收器低估了 TTY 的成本;没有夸大“日志记录是免费的吗?”——它不是免费的。 +- 不测量 `pino` / 结构化记录器(通常更便宜,但在紧密循环中仍然不是免费的)。 +- 不是形状/JIT 的异味;与 jp-alloc-hot/jp-delete-shape/jp-delete-array-hole 无关。 diff --git a/packages/jsperf/evidence/zh-CN/jp-delete-array-hole.md b/packages/jsperf/evidence/zh-CN/jp-delete-array-hole.md new file mode 100644 index 0000000..b8d66f7 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-delete-array-hole.md @@ -0,0 +1,36 @@ +# jp-delete-array-hole — `delete arr[i]`(稀疏数组) + +## 主张 + +在一个紧凑数组上使用 `delete a[i]` 会创建一个**有孔的**元素类型;随后进行类似密集数组的求和操作,比在 V8/Node 上保持紧凑数组要慢。 + +## 如何测量 + +- 测试工具:`packages/jsperf/examples/bench/evidence_jp003_jp004.js`(与 `static_vs_v8_correlate.js` 的案例 2 使用相同的内核) +- 工作量:N=200000;预热=3;9 轮的中位数 +- **之前:** 填充 packed,`delete a[i]` 对偶数 i,求和已定义的槽 +- **之后:** 填充 packed,求和所有槽(不删除) +- 原始数据行:[ `jp-delete-array-hole.jsonl`](../jp-delete-array-hole.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 | +| Node | **v20.19.2** | +| V8 | **11.3.244.8-node.38** | +| 中位数之前 (`holey_delete_arr_i`) | 约 5.14 毫秒 | +| (`packed_fill_sum`) 后的中位数 | ~1.63 毫秒 | +| **前/后比例** | **~3.2×** | + +节点 v22.22.3 的相关性之前在同一方向上大约为 ~5.5×。在引用单一系数之前,最好在你的节点上重新测量。 + +## 与 lint 的对应 + +jp-delete-array-hole 标志使用计算索引(`arr[i]`)的 `delete`。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md),严重性为 **错误**。 + +## 限制 + +- 填充+求和微基准;实际应用可能更喜欢使用 `splice` / filter / dense tombstones 来实现语义,而不仅仅是速度。 +- 多孔数组也可能因为跳过的索引而出现,而不使用 `delete`——lint 不会检测到这些。 +- 这种情况下的去优化追踪十分嘈杂;门槛是实时时钟的比例。 diff --git a/packages/jsperf/evidence/zh-CN/jp-delete-shape.md b/packages/jsperf/evidence/zh-CN/jp-delete-shape.md new file mode 100644 index 0000000..9d71d36 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-delete-shape.md @@ -0,0 +1,36 @@ +# jp-delete-shape — 对普通成员使用 `delete` + +## 主张 + +在 V8/Node 上,热 `delete obj.key` 比通过墓碑方式保留结构 (`obj.key = undefined`) 要慢,因为 `delete` 会改变对象映射 / 可能强制对象进入字典模式路径。 + +## 如何测量 + +- 测试工具:`packages/jsperf/examples/bench/evidence_jp003_jp004.js`(与 `static_vs_v8_correlate.js` 的第4种情况使用相同的内核) +- 工作量:N=200000 次迭代;预热=3;取 9 个 `process.hrtime.bigint` 样本的中位数 +- **之前:** 构造 `{ k, keep }`,`delete o.k`,访问字段 +- **之后:** 相同构造,`o.k = undefined`,访问字段 +- 原始行:[`jp-delete-shape.jsonl`](../jp-delete-shape.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海(本地时间;JSONL 中的 ISO 时间戳为 UTC) | +| Node | **v20.19.2** | +| V8 | **11.3.244.8-node.38** | +| 删除对象前的中位数 (`delete_obj_k`) | ~43.2 毫秒 | +| (`tombstone_undefined`) 之后的中位数 | ~0.091 毫秒 | +| **前/后比率** | **~474×** | + +在 Node v22.22.3 / V8 12.4 上的早期相关性显示在相同方向上约为 ~165×(`scripts/run-static-vs-v8.sh`)。这台机器上的之前门槛约为 ~360×。方向稳定;幅度对机器/Node 敏感——升级后重新运行测试框架。 + +## 与 lint 的对应 + +`jsperf-lint` 的 jp-delete-shape 会在 `delete obj.key` 风格的成员上触发(不是 `delete arr[i]` —— 那是 jp-delete-array-hole)。在 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 下默认严重性是 **`off`**:这个 JSONL 存在并且一致,但语料筛查发现几乎所有命中都是可接受的惯用法。使用 `--preset all` 或 `--severity jp-delete-shape=error` 可以启用它。 + +## 限制 + +- 微基准测试,而不是应用程序分析。 +- 一次性脚本:`--trace-deopt` 行计数是一个弱预测(实际耗时才是关键)。 +- 冷启动 / 引导 `delete`(Node `internal/bootstrap/**`)被代码检查工具跳过,不属于此声明的范围。 diff --git a/packages/jsperf/evidence/zh-CN/jp-dom-query-hot.md b/packages/jsperf/evidence/zh-CN/jp-dom-query-hot.md new file mode 100644 index 0000000..3989135 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-dom-query-hot.md @@ -0,0 +1,21 @@ +# jp-dom-query-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[jp-dom-query-hot.jsonl](../jp-dom-query-hot.jsonl)。未进行语料库筛选。2026-10-22 的语料库裁决不涵盖此段。使用 linkedom 在单元素文档上计时,而不是浏览器布局树。 + +## 测量 +双方都读取 `textContent`。在每次迭代之前都会调用 `querySelector(".item")`。节点 **v26.12.1**,linkedom,linux x64,负载 0.32。测试程序:`examples/bench/evidence_jp_dom.js`。先前的 Node 24 配对大约是 8.6×。一次样本为 1.459 毫秒,每个样本为 17.257 毫秒;中位数位于更紧凑的群集。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 缓存元素 | 0.924 | 20000 | 1 | +| 之前 | 每次迭代查询 | 14.935 | 20000 | 16.169 | + +较大的文档可以移动此数字。默认保持提示。 + +## 匹配器 + +该基准重复了一个未更改的选择器。命名循环绑定(`ids[i]`)的选择器不会被报告。默认的 `--scope all` 也会报告循环外的稳定选择器 DOM 查询;`--scope loop` 则仅保留前者在循环内的匹配。提示建议在选择器稳定时缓存节点。 + +## 解除阻塞 +不要仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。在进行任何默认更改之前,仍然需要语料分流。 diff --git a/packages/jsperf/evidence/zh-CN/jp-intl-datetimeformat-hot.md b/packages/jsperf/evidence/zh-CN/jp-intl-datetimeformat-hot.md new file mode 100644 index 0000000..9978bd7 --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-intl-datetimeformat-hot.md @@ -0,0 +1,21 @@ +# jp-intl-datetimeformat-hot — 测量过,仍然提示 + +## 状态 +**提示**。实时时钟 JSONL:[jp-intl-datetimeformat-hot.jsonl](../jp-intl-datetimeformat-hot.jsonl)。未进行语料库分流。2026-10-22 的语料库裁定不涵盖此条目。是 Swift 和 Objective-C 日期格式化规则的同类。双方仍然只格式化一个日期。 + +## 测量 +节点 **v26.12.1** 在 linux x64 上,负载 0.74。测试工具:`examples/bench/evidence_jp_intl.js`。2000 个日期,地区设置为 `en-US`。之前的 Node 24 配对大约是 53×。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | 一个 Intl.DateTimeFormat | 1.999 | 2000 | 1 | +| 之前 | 构建每次迭代 | 72.002 | 2000 | 36.013 | + +`new Date` 不适用此规则。默认仍保持提示。 + +## 匹配器 + +该基准构建了一个未更改的格式化器。指定循环绑定名称的区域设置或选项参数不会被报告。默认的 `--scope all` 也会报告循环外的稳定格式化器;`--scope loop` 保持前者仅在循环内匹配。该提示建议在区域设置和选项稳定时重用一个格式化器。 + +## 解除阻塞 +不要仅从此文件将默认设置为 `error`。其他输入或冷路径可能会抹去差异。在进行任何默认更改之前,仍然需要进行语料分流。 diff --git a/packages/jsperf/evidence/zh-CN/jp-json-hot.md b/packages/jsperf/evidence/zh-CN/jp-json-hot.md new file mode 100644 index 0000000..e4a332e --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-json-hot.md @@ -0,0 +1,24 @@ +# jp-json-hot — 稳定,仍有暗示 + +## 状态 + +**提示**。实时时钟 JSONL:[jp-json-hot.jsonl](../jp-json-hot.jsonl)。未经过语料库筛选。2026-10-22 的语料库裁定不涵盖此标签。 + +## 测量 + +解析固定 JSON 文本 `{"n":1,"s":"hello"}` 20000 次。Node **v26.12.1** 在 linux x64 上,负载 0.05。测试工具:`examples/bench/evidence_jp_json.js`。之前的 Node 24 配对大约是 297×。这次重跑没有使用 `JSON.stringify`。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 之后 | `parse_once` | 0.023325 | 20000 | 1 | +| 之前 | `parse_each_iter` | 3.263081 | 20000 | 139.896 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 匹配器 + +该基准解析了一个未改变的文本。在 `for (const line of lines)` 中的 `JSON.parse(rows[i])` 和 `JSON.parse(line)` 每次迭代都是不同的文本。检查器不会报告这些。它没有说明要将一个 JSONL 文件折叠为一次解析。默认的 `--scope all` 也会报告循环外的稳定文本 JSON 调用;`--scope loop` 则只保留之前的循环内匹配。该提示建议在文本稳定时重用结果。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。输入的变化、冷路径或其他 ABI 都可能抹去差异。在进行任何默认值更改之前,仍然需要进行语料库分流。 diff --git a/packages/jsperf/evidence/zh-CN/jp-map-filter-chain.md b/packages/jsperf/evidence/zh-CN/jp-map-filter-chain.md new file mode 100644 index 0000000..850694f --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-map-filter-chain.md @@ -0,0 +1,34 @@ +# jp-map-filter-chain — 已测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[jp-map-filter-chain.jsonl](../jp-map-filter-chain.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 + +`examples/bench/evidence_jp005_jp006.js`。三次热身运行后九轮;中位数。双方构建结果数组并求和。Node v24.18.0,V8 13.6.233.17-node.50。 + +`ratio` 是链时间 / 一次通行时间。成本随着长度和通行次数的增加而增加。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之前 | 3 行,先过滤再映射,× 20000 | 0.289 | 60000 | 1.276 | +| 之后 | 一次传递 | 0.226 | 60000 | 1 | +| 之前 | 3 行,四个过滤器然后映射,× 20000 | 0.761 | 60000 | 3.460 | +| 之后 | 一次传递 | 0.220 | 60000 | 1 | +| 之前 | 100 行,先过滤再映射,× 2000 | 0.504 | 200000 | 1.228 | +| 之后 | 一次传递 | 0.410 | 200000 | 1 | +| 之前 | 100 行,四个筛选器然后映射,× 2000 | 1.026 | 200000 | 2.487 | +| 之后 | 一次传递 | 0.413 | 200000 | 1 | +| 之前 | 35000 行,先过滤然后映射,× 20 | 8.728 | 700000 | 4.598 | +| 之后 | 一次通过 | 1.898 | 700000 | 1 | +| 之前 | 35000 行,四个过滤器然后映射,× 20 | 12.850 | 700000 | 9.841 | +| 之后 | 一次通过 | 1.306 | 700000 | 1 | + +对 3 行数据进行 `filter` 然后 `map` 的速度是 1.28×,低于大约 2× 的标杆。对 3 行数据进行四次遍历是 3.46×,而 20000 次调用仍然在 0.76 毫秒内完成。在 35000 行数据上,一次遍历大约是 4.6×(一次 filter)和大约 9.8×(四次 filter)。这些是不同的成本。提示是这么说的,它并没有说每个链条都必须重写。 + +匹配器在查找之前会去掉 `//` 注释和块注释。它不知道数组的长度,也不需要循环。生成的 `gen-*` 文件仍然会被跳过。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。一个短数组、一条冷路径或另一个引擎都可能抹去差异。默认更改之前仍然需要语料分流。 diff --git a/packages/jsperf/evidence/zh-CN/jp-readfile-sync-hot.md b/packages/jsperf/evidence/zh-CN/jp-readfile-sync-hot.md new file mode 100644 index 0000000..563067e --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-readfile-sync-hot.md @@ -0,0 +1,21 @@ +# jp-readfile-sync-hot — 已测量,仍然提示 + +## 状态 +**提示**. 实时时钟 JSONL: [jp-readfile-sync-hot.jsonl](../jp-readfile-sync-hot.jsonl)。未进行语料库分流。2026-10-22 的语料库裁定不涵盖此标识。 + +## 测量 +`fs.readFileSync` 对一个一字节文件进行 2000 次读取,与一次读取的比较。Node **v26.12.1** 在 Linux x64 上,负载 0.26。测试工具:`examples/bench/evidence_jp_readfile.js`。之前的 Node 24 对比约为 480×。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `read_once` | 0.041 | 2000 | 1 | +| 之前 | `read_each_iter` | 4.647 | 2000 | 114.417 | + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 匹配器 + +默认情况下,`--scope all` 会在路径看起来稳定的地方报告 `readFileSync`。在 `--scope loop` 下,只有 `{` 之后的循环体会匹配,因此 `for (const line of readFileSync(path).split("\n"))`(作为可迭代对象只读一次)不会被报告。作为循环绑定的路径(`files[i]`)也不会被报告。这个提示建议在路径稳定时只读取一次。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在任何默认更改之前,仍然需要进行语料库分类处理。 diff --git a/packages/jsperf/evidence/zh-CN/jp-regex-ctor-hot.md b/packages/jsperf/evidence/zh-CN/jp-regex-ctor-hot.md new file mode 100644 index 0000000..36e641f --- /dev/null +++ b/packages/jsperf/evidence/zh-CN/jp-regex-ctor-hot.md @@ -0,0 +1,24 @@ +# jp-regex-ctor-hot — 测量过的,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[jp-regex-ctor-hot.jsonl](../jp-regex-ctor-hot.jsonl)。未进行语料库分类。2026-10-22 的语料库裁定不涵盖此标识符。 + +## 测量 + +使用 `new RegExp("a+")` 对固定字符串 `aaa-bbb` 进行 20000 次测试。Node **v26.12.1** 在 linux x64 上,负载 0.25。测试工具:`examples/bench/evidence_jp_regex.js`。之前的 Node 24 配对大约是 3.9×。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 之后 | `regexp_once` | 0.273718 | 20000 | 1 | +| 之前 | `regexp_each_iter` | 1.213845 | 20000 | 4.435 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 匹配器 + +该基准编译了一个固定模式。如果参数命名了一个 `for` 绑定(`word`、`parts[i]`、一个解构的 `{word}`),检查器不会报告该调用。该模式每次迭代都会变化,因此只编译一次不起作用。默认 `--scope all` 也会报告循环外的稳定模式 `new RegExp`;`--scope loop` 保持以前仅在循环内匹配。提示建议在模式稳定时使用文字或共享编译的单个模式。 + +## 解阻条件 + +不要仅从这个文件将默认值设置为 `error`。输入的变化、冷路径或其他 ABI 都可能抹去差异。在进行任何默认值更改之前,仍然需要进行语料库分流。 diff --git a/packages/jsperf/zh-CN/CONTRIBUTING.md b/packages/jsperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/jsperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/jsperf/zh-CN/README.md b/packages/jsperf/zh-CN/README.md new file mode 100644 index 0000000..ab074bd --- /dev/null +++ b/packages/jsperf/zh-CN/README.md @@ -0,0 +1,49 @@ +# jsperf + +面向 V8/Node 和嵌入环境(QuickJS / 类小程序)的 JavaScript/TypeScript 性能笔记:隐藏类、元素类型、垃圾回收压力、类型化数组、异步批处理,以及避免多态热路径。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。该 lint 是 **纯 JS**(Node ≥18,无 npm 依赖)并带有严重性预设。Guide v0 包括 **§4 V8 优化**(单机微基准,Node 22.22.3)和 **§5 启动/工具链**(Marvin 系列方法)。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/js-performance-guide.md) — 来自 `docs/guide-src/`(包括 `04_v8_optimizations`,`05_startup_and_tooling`) +- [`jsperf-lint`](../jsperf-lint/README.md) — jp-* 规则 +- [证据政策](../docs/zh-CN/evidence-policy.md) · [规则 ID](../docs/zh-CN/rule-ids.md) +- [语料库固定项](../docs/zh-CN/corpus.md) +- V8 测试:`examples/bench/` · `scripts/run-v8-benches.sh` + +## 运行 lint + +```sh +node jsperf-lint/bin/jsperf-lint.js path/to/sources --preset app # default +node jsperf-lint/bin/jsperf-lint.js path/to/sources --preset all +node jsperf-lint/bin/jsperf-lint.js path/to/sources --scope all|loop # default all +node jsperf-lint/bin/jsperf-lint.js path/to/sources --severity jp-alloc-hot=error +npm run self-lint # dogfood own sources; must exit 0 +npm test # fixtures + preset smoke +``` + +默认 **应用**:jp-delete-array-hole = 错误;jp-map-filter-chain、jp-regex-ctor-hot、jp-json-hot、jp-dom-query-hot、jp-intl-datetimeformat-hot、jp-readfile-sync-hot 和 jp-clock-hot = 提示;jp-alloc-hot、jp-console-hot 和 jp-delete-shape = 关闭。默认 **`--scope all`** 也会报告循环外的这些异味(如需仅匹配循环内,请选择 `--scope loop`)。jp-string-plus-in-loop 已移除。仅对错误等级的发现退出 `1`。 + +证据 JSONL 门:[`evidence/`](../evidence/)(jp-alloc-hot、jp-console-hot、jp-delete-shape、jp-delete-array-hole)。政策:[`docs/evidence-policy.md`](../docs/zh-CN/evidence-policy.md)。重新生成:`bash scripts/regen-evidence.sh`。 + +## 布局 + +| 路径 | 内容 | +|------|----------| +| `docs/` | 指南 (`guide-src/` 是源文件), 语料库固定, 证据政策, 规则 ID | +| `evidence/` | 每条规则的证据笔记和 JSONL | +| `examples/` | 说明性片段; `examples/bench/` 包含 V8 微基准 | +| `jsperf-lint/` | linter 源码, 测试夹具, README | +| `scripts/` | 烟雾测试, 基准测试和证据运行器 | + +## 示例 / 长凳 + +```sh +node examples/m01_hidden_class.js +node examples/p01_typed_array.js +# Prefer Node 22+ for §4 numbers: +NODE_BIN=$(command -v node) bash scripts/run-v8-benches.sh +NODE_BIN=$(command -v node) bash scripts/jit-check.sh # --trace-deopt sample +NODE_BIN=$(command -v node) bash scripts/run-static-vs-v8.sh # smell vs wall-clock +``` diff --git a/packages/jvmperf/docs/en/performance-guide.md b/packages/jvmperf/docs/en/performance-guide.md index 434ecad..f5c7b3d 100644 --- a/packages/jvmperf/docs/en/performance-guide.md +++ b/packages/jvmperf/docs/en/performance-guide.md @@ -4,8 +4,8 @@ > Last reviewed: 2026-10-21. > > - Every code sample comes from a standalone file under `examples/`. The Java samples were compiled with `javac --release 25` and run with self-checks; Kotlin samples were compiled with `kotlinc 2.4.21 -jvm-target 25` and run with self-checks. Android samples were compiled against `android-34/android.jar`. Logs are in `research/*-check.log`. Files under `research/` that this guide names (`*.txt`, `*.log`, `bench-results/`, `benches/`) are local run output and are not committed. -> - Values marked **Measured locally** were measured with JOL 0.17 on x86_64 Linux with 8 cores, JDK 25.0.4.1, G1, and compressed pointers enabled. Raw output is in `research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`, `research/ea-probe.txt`, and `research/jvm-flags-jdk25.txt`. Results vary across JDKs, pointer settings, and ART. -> - This guide does not invent speedup numbers. Use the JMH template in Chapter 14 to measure on your machine. +> - Values marked Measured locally were measured with JOL 0.17 on x86_64 Linux with 8 cores, JDK 25.0.4.1, G1, and compressed pointers enabled. Raw output is in `research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`, `research/ea-probe.txt`, and `research/jvm-flags-jdk25.txt`. Results vary across JDKs, pointer settings, and ART. +> - Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. This guide does not invent speedup numbers; use the JMH template in Chapter 14 to measure on your machine. > - Each section links to its sources and explains when its recommendations do not apply. ## Contents @@ -39,9 +39,9 @@ | JDK 22 | Foreign Function & Memory API (`MemorySegment`/`Arena`) finalized (JEP 454). | | JDK 23 | Generational mode became the default for ZGC (JEP 474). | | JDK 24 | Compact object headers preview (JEP 450); non-generational ZGC removed (JEP 490); `synchronized` no longer pins virtual threads (JEP 491); AOT class-loading cache (JEP 483). | -| **JDK 25 (LTS)** | Compact object headers finalized but **disabled by default** (JEP 519, `-XX:+UseCompactObjectHeaders`); generational Shenandoah finalized (JEP 521); Vector API incubator, 10th round (JEP 508); Stable Values preview (JEP 502); simplified AOT commands and method profiles (JEP 514/515); JFR CPU-time sampling experimental (JEP 509). | +| JDK 25 (LTS) | Compact object headers finalized but disabled by default (JEP 519, `-XX:+UseCompactObjectHeaders`); generational Shenandoah finalized (JEP 521); Vector API incubator, 10th round (JEP 508); Stable Values preview (JEP 502); simplified AOT commands and method profiles (JEP 514/515); JFR CPU-time sampling experimental (JEP 509). | | JDK 26 (2026-03) | G1 reduces synchronization to improve throughput (JEP 522); AOT object caching available with any GC (JEP 516); Stable Values renamed Lazy Constants (JEP 526, preview); Vector API incubator, 11th round. | -| JDK 27 (2026-09, non-LTS) | **Compact object headers enabled by default** (JEP 534); G1 is the default GC in all environments (JEP 523); Vector API incubator, 12th round (JEP 537, no API change). | +| JDK 27 (2026-09, non-LTS) | Compact object headers enabled by default (JEP 534); G1 is the default GC in all environments (JEP 523); Vector API incubator, 12th round (JEP 537, no API change). | | JDK 28 (planned 2027-03) | Valhalla value classes (JEP 401) are expected to enter the mainline as a preview, according to InfoQ; the Vector API is expected to move from incubator to preview later. | | Kotlin 2.0 | K2 compiler; lambdas use `invokedynamic` by default (`-Xlambdas=indy`). | | Kotlin 2.4 (2026-06, current 2.4.21) | Context parameters and other features stabilized; Java 26 target supported; `-language-version 1.9` can no longer select K1. | @@ -53,10 +53,10 @@ Sources: [JDK 25](https://openjdk.org/projects/jdk/25/) · [JDK 26](https://open ## 1. Method: estimate, then measure -1. **Estimate first.** JVM memory for an object is roughly its header, fields, and alignment padding, plus every object it references. Chapter 2 gives formulas and local measurements so you can estimate the cost of a million objects before changing code. -2. **Measure next.** Use JOL (`ClassLayout`/`GraphLayout`) for object sizes; JMH `-prof gc` and `gc.alloc.rate.norm`, or `ThreadMXBean.getThreadAllocatedBytes`, for allocation rates; async-profiler or JFR for CPU hotspots; and JMH for throughput and latency. Do not wrap a loop in `System.nanoTime()` and call it a benchmark: JIT warmup, dead-code elimination, and constant folding can invalidate the result. -3. **Optimize hotspots only.** Bit-packing a configuration object created a few dozen times is usually wasted effort. Replacing a `List` with ten million entries by an `int[]` can make a large difference (see the measurements in Chapter 3). -4. **Measure again after the change.** Keep the benchmark and its correctness checks. +1. Estimate first. JVM memory for an object is roughly its header, fields, and alignment padding, plus every object it references. Chapter 2 gives formulas and local measurements so you can estimate the cost of a million objects before changing code. +2. Measure next. Use JOL (`ClassLayout`/`GraphLayout`) for object sizes; JMH `-prof gc` and `gc.alloc.rate.norm`, or `ThreadMXBean.getThreadAllocatedBytes`, for allocation rates; async-profiler or JFR for CPU hotspots; and JMH for throughput and latency. Do not wrap a loop in `System.nanoTime()` and call it a benchmark: JIT warmup, dead-code elimination, and constant folding can invalidate the result. +3. Optimize hotspots only. Bit-packing a configuration object created a few dozen times is usually wasted effort. Replacing a `List` with ten million entries by an `int[]` can make a large difference (see the measurements in Chapter 3). +4. Measure again after the change. Keep the benchmark and its correctness checks. Chapter 14 explains how to use the measurement tools. @@ -267,7 +267,7 @@ public class J01_Boxing { Key points: -- `Integer.valueOf` caches values from −128 through 127 (the upper bound can be raised with `-XX:AutoBoxCacheMax`; it was 128 by default on the measurement machine). **Never compare wrapper objects with `==`.** +- `Integer.valueOf` caches values from −128 through 127 (the upper bound can be raised with `-XX:AutoBoxCacheMax`; it was 128 by default on the measurement machine). Never compare wrapper objects with `==`. - For streams, use `mapToInt(...).sum()` instead of `map(...).reduce(0, Integer::sum)`, which boxes each intermediate value. - C2 has `EliminateAutoBox` enabled by default on the measurement machine. It can remove some local boxing and unboxing, but cannot remove objects stored in a collection. @@ -325,11 +325,11 @@ Sources: [`Integer.valueOf`](https://docs.oracle.com/en/java/javase/25/docs/api/ ## 4. Case study: memory used by a hybrid car -A popular C/C++ example models whether a car has an electric motor or a gasoline engine. Two `int` fields use 8 bytes; one `int` bit field uses 4; a `uint8_t` uses 1. The JVM behaves differently because **an object's size is determined by its header and 8-byte alignment**. +A popular C/C++ example models whether a car has an electric motor or a gasoline engine. Two `int` fields use 8 bytes; one `int` bit field uses 4; a `uint8_t` uses 1. The JVM behaves differently because an object's size is determined by its header and 8-byte alignment. ### 4.1 A single object: packing still leaves 16 bytes -**Measured locally with JOL 0.17 on JDK 25.0.4.1** (`research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`): +Measured locally with JOL 0.17 on JDK 25.0.4.1 (`research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`): | Representation | Size in C | Default JVM | `+UseCompactObjectHeaders` | |---|---:|---:|---:| @@ -342,7 +342,7 @@ A popular C/C++ example models whether a car has an electric motor or a gasoline | Kotlin `data class HybridInts(Int, Int)` | — | 24 | 16 | | Kotlin `class Car(val fuels: FuelFlags)` (`FuelFlags` is a value class wrapping `Byte`) | — | 16 | 16 | -The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byte object. Changing `int` to `byte` only creates more padding. Two `int` fields need 20 bytes before alignment, so the object occupies 24 bytes. Compact headers reduce it to 16 bytes; **that flag can save more than manually packing fields**. `EnumSet` is convenient, but it is a separate object containing a `long` and adds another object when stored as a field. +The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byte object. Changing `int` to `byte` only creates more padding. Two `int` fields need 20 bytes before alignment, so the object occupies 24 bytes. Compact headers reduce it to 16 bytes; that flag can save more than manually packing fields. `EnumSet` is convenient, but it is a separate object containing a `long` and adds another object when stored as a field. ### 4.2 One thousand cars: avoid one object per car @@ -358,7 +358,7 @@ The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byt On the JVM: 1. Packing flags inside one object rarely saves memory unless it crosses an 8-byte alignment boundary. -2. The large saving comes from replacing an **array of objects** with **primitive arrays** (Structure of Arrays; see Chapter 5). The example drops from 20 KB to 1 KB and removes 1,000 pointer chases. +2. The large saving comes from replacing an array of objects with primitive arrays (Structure of Arrays; see Chapter 5). The example drops from 20 KB to 1 KB and removes 1,000 pointer chases. 3. Bit encodings (`byte` operations, `EnumSet`, `BitSet`) matter more at the array level. Prefer readable fields for individual objects. Java version: @@ -1066,7 +1066,7 @@ fun main() { ### 7.9 Coroutines -Each `suspend` function compiles to a state machine and needs a continuation object when it actually suspends. Each `launch` or `async` also creates an object. Split CPU-heavy work into chunks; **do not launch one coroutine per element**. +Each `suspend` function compiles to a state machine and needs a continuation object when it actually suspends. Each `launch` or `async` also creates an object. Split CPU-heavy work into chunks; do not launch one coroutine per element. `examples/kotlin/K10_Coroutines.kt` @@ -1206,7 +1206,7 @@ Avoid it for ordinary business objects. Off-heap data needs manual serialization ### 8.2 Direct `ByteBuffer` -This older API is limited by `-XX:MaxDirectMemorySize` (zero by default means the maximum heap size) and is indirectly freed by a `Cleaner` during GC. Allocation and release cost more than heap memory, so **reuse direct buffers** (see Chapter 10) instead of creating one per request. A `ByteBuffer` is limited to 2 GB because it uses `int` indexes; `MemorySegment` has no such limit. +This older API is limited by `-XX:MaxDirectMemorySize` (zero by default means the maximum heap size) and is indirectly freed by a `Cleaner` during GC. Allocation and release cost more than heap memory, so reuse direct buffers (see Chapter 10) instead of creating one per request. A `ByteBuffer` is limited to 2 GB because it uses `int` indexes; `MemorySegment` has no such limit. ### 8.3 I/O @@ -1261,7 +1261,7 @@ Sources: [JEP 454: FFM API](https://openjdk.org/jeps/454) · [`Arena`](https://d ### 9.1 Escape analysis and scalar replacement -C2 checks whether an object escapes a method (for example, by being stored in a field or array, returned, or passed to a method that is not inlined). A non-escaping object can be **scalar-replaced**: its fields become local variables and the object is never allocated on the heap. +C2 checks whether an object escapes a method (for example, by being stored in a field or array, returned, or passed to a method that is not inlined). A non-escaping object can be scalar-replaced: its fields become local variables and the object is never allocated on the heap. **Measured locally** with `research/EaProbe.java`, which uses `ThreadMXBean.getThreadAllocatedBytes` to measure 1,000 iterations of `new Vec(...).plus(new Vec(1,1))`: @@ -1301,7 +1301,7 @@ public class J08_EscapeAnalysis { } ``` -For escape-analysis-friendly code, keep methods small enough to inline, do not store short-lived objects in fields or collections, and avoid complex control-flow merges. Small arrays can also be scalar-replaced, up to a limit (64 elements on the measurement machine: `EliminateAllocationArraySizeLimit`). **Do not rely on this optimization:** escape analysis takes effect only after C2 compilation, not during interpretation or C1. Android ART uses different AOT/JIT optimizations. +For escape-analysis-friendly code, keep methods small enough to inline, do not store short-lived objects in fields or collections, and avoid complex control-flow merges. Small arrays can also be scalar-replaced, up to a limit (64 elements on the measurement machine: `EliminateAllocationArraySizeLimit`). Do not rely on this optimization: escape analysis takes effect only after C2 compilation, not during interpretation or C1. Android ART uses different AOT/JIT optimizations. **Evidence (correlate):** Static JP smells vs HotSpot wall-clock/B/op — `scripts/run-static-vs-hotspot.sh` output `research/benches/static-vs-hotspot.jsonl` (cases 4 EA, 6 Vector control; JP rules never observe C1/C2 or call-site type profile). Package gate: `docs/evidence-policy.md`. @@ -1347,7 +1347,7 @@ public class J09_JitFriendly { ### 9.3 Intrinsics -HotSpot intrinsics such as `Integer.bitCount`, `Long.numberOfTrailingZeros`, `Math.fma`, `System.arraycopy`, `Arrays.equals`/`mismatch`, `String.indexOf`, and CRC32 compile to specialized instruction sequences. **Prefer JDK methods over hand-written bit tricks intended to optimize them.** +HotSpot intrinsics such as `Integer.bitCount`, `Long.numberOfTrailingZeros`, `Math.fma`, `System.arraycopy`, `Arrays.equals`/`mismatch`, `String.indexOf`, and CRC32 compile to specialized instruction sequences. Prefer JDK methods over hand-written bit tricks intended to optimize them. ### 9.4 Vector API (still incubating) @@ -1479,7 +1479,7 @@ Do not add padding without real write contention; it can waste 256 bytes per fie Virtual threads were finalized in JDK 21 (JEP 444). They suit services with many tasks that spend most of their time blocked on I/O. -- Use one virtual thread per task; **do not pool virtual threads**. Use a `Semaphore` to limit access to downstream resources. +- Use one virtual thread per task; do not pool virtual threads. Use a `Semaphore` to limit access to downstream resources. - Since JDK 24, `synchronized` no longer pins a virtual thread to its carrier (JEP 491). Native and foreign-function calls can still pin it. - CPU-bound tasks do not benefit; use a platform-thread pool or `ForkJoinPool`. - `ThreadLocal` memory adds up across millions of virtual threads. Replace `ThreadLocal` caches of large objects such as `SimpleDateFormat`; consider Scoped Values for shared data (finalized in JDK 25, JEP 506). @@ -1512,7 +1512,7 @@ public class J11_VirtualThreads { ### 10.3 Object pools: when to use them -Allocation in a thread-local allocation buffer (TLAB) is a pointer bump, and collecting short-lived young-generation objects is very cheap. Pooling small objects is often **slower**: they may be promoted to the old generation, need clearing or resetting, require synchronization, and can leak. Pool only genuinely expensive resources such as direct `ByteBuffer`s, large arrays, database connections, threads, or costly non-thread-safe objects such as `MessageDigest` and `Cipher`. +Allocation in a thread-local allocation buffer (TLAB) is a pointer bump, and collecting short-lived young-generation objects is very cheap. Pooling small objects is often slower: they may be promoted to the old generation, need clearing or resetting, require synchronization, and can leak. Pool only genuinely expensive resources such as direct `ByteBuffer`s, large arrays, database connections, threads, or costly non-thread-safe objects such as `MessageDigest` and `Cipher`. **Evidence (local):** JMH 1.37 on JDK 25 (`J24_PoolVsAlloc`) — batch of small escaping `Point` objects via TLAB `new` + `Blackhole` vs thread-local `ArrayDeque` pool reuse (`-prof gc`). JSONL: `research/bench-results/J24_PoolVsAlloc.jsonl` (re-run before citing). Pool cuts B/op near zero; wall time is often worse than TLAB for this size. @@ -1600,7 +1600,7 @@ Sources: [HotSpot GC tuning guide for JDK 25](https://docs.oracle.com/en/java/ja ### 12.1 ART vs HotSpot -- ART has no C2 compiler or HotSpot-style escape analysis and scalar replacement. It combines interpretation, JIT compilation, and profile-guided AOT compilation (`dex2oat`). **Do not assume the JIT will remove allocations on Android**; avoid them in hot paths such as `onDraw`, scroll callbacks, and animation frames. +- ART has no C2 compiler or HotSpot-style escape analysis and scalar replacement. It combines interpretation, JIT compilation, and profile-guided AOT compilation (`dex2oat`). Do not assume the JIT will remove allocations on Android; avoid them in hot paths such as `onDraw`, scroll callbacks, and animation frames. - References are already 32-bit and object headers are 8 bytes (`mirror::Object` has 32-bit class and monitor fields). ART has no compressed-reference or compact-header switch. - Since Android 14, ART Mainline updates use Concurrent Mark Compact (CMC), based on `userfaultfd`, by default. Android 16 QPR2 adds generational CMC, which collects recently allocated objects first. @@ -1776,7 +1776,7 @@ Sources: [JMH samples](https://github.com/openjdk/jmh/tree/master/jmh-samples/sr ## 15. jvmperf-lint: find patterns automatically -`jvmperf-lint/` is the guide's static checker. It is written in Java and uses tree-sitter via the JVM bindings (`io.github.bonede:tree-sitter` with `tree-sitter-java` and `tree-sitter-kotlin`) to inspect `.java`, `.kt`, and `.kts` files. It reads syntax only and **does not resolve types**. This keeps it fast and configuration-free, but it can report false positives or miss issues; each rule documents its risk. See [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md) for installation and usage. +`jvmperf-lint/` is the guide's static checker. It is written in Java and uses tree-sitter via the JVM bindings (`io.github.bonede:tree-sitter` with `tree-sitter-java` and `tree-sitter-kotlin`) to inspect `.java`, `.kt`, and `.kts` files. It reads syntax only and does not resolve types. This keeps it fast and configuration-free, but it can report false positives or miss issues; each rule documents its risk. See [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md) for installation and usage. ```sh cd jvmperf-lint @@ -1798,8 +1798,8 @@ Default severity is `error`, `hint`, or `off`, as printed by `bin/jvmperf-lint - | jp-unpresized-collection | `unpresized-collection` | off | Java/Kotlin | A collection or `StringBuilder` is filled in a loop without initial capacity | 5.2 | | jp-boxed-stream | `boxed-stream` | error | Java | `reduce(0, Integer::sum)` or `Collectors.summingInt` on `Stream` | 3.1 | | jp-exception-control-flow | `exception-control-flow` | off | Java/Kotlin | A loop catches parse exceptions to make a decision | 9.5 | -| jp-legacy-synchronized-type | — | **removed** | — | measured ~1.08×; see `evidence/_removed/` | — | -| jp-many-boolean-fields | — | **removed** | — | COH no win; see `evidence/_removed/` | — | +| jp-legacy-synchronized-type | — | removed | — | measured ~1.08×; see `evidence/_removed/` | — | +| jp-many-boolean-fields | — | removed | — | COH no win; see `evidence/_removed/` | — | | jp-enum-key-hash-collection | `enum-key-hash-collection` | error | Java/Kotlin | `HashMap` keyed by an enum or `HashSet` of enum values | 5.3 | | jp-collection-chain | `collection-chain` | off | Kotlin | A chain of 3+ `map`/`filter` operations, or 2 operations plus short-circuiting such as `first`, `take`, or `any` | 7.4 | | jp-value-class-boxing | `value-class-boxing` | hint | Kotlin | A value class used as a nullable value, generic argument, or array element | 7.2 | diff --git a/packages/jvmperf/docs/guide-src/en/00_head.md b/packages/jvmperf/docs/guide-src/en/00_head.md index d236110..0aa9d7c 100644 --- a/packages/jvmperf/docs/guide-src/en/00_head.md +++ b/packages/jvmperf/docs/guide-src/en/00_head.md @@ -4,8 +4,8 @@ > Last reviewed: 2026-10-21. > > - Every code sample comes from a standalone file under `examples/`. The Java samples were compiled with `javac --release 25` and run with self-checks; Kotlin samples were compiled with `kotlinc 2.4.21 -jvm-target 25` and run with self-checks. Android samples were compiled against `android-34/android.jar`. Logs are in `research/*-check.log`. Files under `research/` that this guide names (`*.txt`, `*.log`, `bench-results/`, `benches/`) are local run output and are not committed. -> - Values marked **Measured locally** were measured with JOL 0.17 on x86_64 Linux with 8 cores, JDK 25.0.4.1, G1, and compressed pointers enabled. Raw output is in `research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`, `research/ea-probe.txt`, and `research/jvm-flags-jdk25.txt`. Results vary across JDKs, pointer settings, and ART. -> - This guide does not invent speedup numbers. Use the JMH template in Chapter 14 to measure on your machine. +> - Values marked Measured locally were measured with JOL 0.17 on x86_64 Linux with 8 cores, JDK 25.0.4.1, G1, and compressed pointers enabled. Raw output is in `research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`, `research/ea-probe.txt`, and `research/jvm-flags-jdk25.txt`. Results vary across JDKs, pointer settings, and ART. +> - Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. This guide does not invent speedup numbers; use the JMH template in Chapter 14 to measure on your machine. > - Each section links to its sources and explains when its recommendations do not apply. ## Contents @@ -39,9 +39,9 @@ | JDK 22 | Foreign Function & Memory API (`MemorySegment`/`Arena`) finalized (JEP 454). | | JDK 23 | Generational mode became the default for ZGC (JEP 474). | | JDK 24 | Compact object headers preview (JEP 450); non-generational ZGC removed (JEP 490); `synchronized` no longer pins virtual threads (JEP 491); AOT class-loading cache (JEP 483). | -| **JDK 25 (LTS)** | Compact object headers finalized but **disabled by default** (JEP 519, `-XX:+UseCompactObjectHeaders`); generational Shenandoah finalized (JEP 521); Vector API incubator, 10th round (JEP 508); Stable Values preview (JEP 502); simplified AOT commands and method profiles (JEP 514/515); JFR CPU-time sampling experimental (JEP 509). | +| JDK 25 (LTS) | Compact object headers finalized but disabled by default (JEP 519, `-XX:+UseCompactObjectHeaders`); generational Shenandoah finalized (JEP 521); Vector API incubator, 10th round (JEP 508); Stable Values preview (JEP 502); simplified AOT commands and method profiles (JEP 514/515); JFR CPU-time sampling experimental (JEP 509). | | JDK 26 (2026-03) | G1 reduces synchronization to improve throughput (JEP 522); AOT object caching available with any GC (JEP 516); Stable Values renamed Lazy Constants (JEP 526, preview); Vector API incubator, 11th round. | -| JDK 27 (2026-09, non-LTS) | **Compact object headers enabled by default** (JEP 534); G1 is the default GC in all environments (JEP 523); Vector API incubator, 12th round (JEP 537, no API change). | +| JDK 27 (2026-09, non-LTS) | Compact object headers enabled by default (JEP 534); G1 is the default GC in all environments (JEP 523); Vector API incubator, 12th round (JEP 537, no API change). | | JDK 28 (planned 2027-03) | Valhalla value classes (JEP 401) are expected to enter the mainline as a preview, according to InfoQ; the Vector API is expected to move from incubator to preview later. | | Kotlin 2.0 | K2 compiler; lambdas use `invokedynamic` by default (`-Xlambdas=indy`). | | Kotlin 2.4 (2026-06, current 2.4.21) | Context parameters and other features stabilized; Java 26 target supported; `-language-version 1.9` can no longer select K1. | @@ -53,10 +53,10 @@ Sources: [JDK 25](https://openjdk.org/projects/jdk/25/) · [JDK 26](https://open ## 1. Method: estimate, then measure -1. **Estimate first.** JVM memory for an object is roughly its header, fields, and alignment padding, plus every object it references. Chapter 2 gives formulas and local measurements so you can estimate the cost of a million objects before changing code. -2. **Measure next.** Use JOL (`ClassLayout`/`GraphLayout`) for object sizes; JMH `-prof gc` and `gc.alloc.rate.norm`, or `ThreadMXBean.getThreadAllocatedBytes`, for allocation rates; async-profiler or JFR for CPU hotspots; and JMH for throughput and latency. Do not wrap a loop in `System.nanoTime()` and call it a benchmark: JIT warmup, dead-code elimination, and constant folding can invalidate the result. -3. **Optimize hotspots only.** Bit-packing a configuration object created a few dozen times is usually wasted effort. Replacing a `List` with ten million entries by an `int[]` can make a large difference (see the measurements in Chapter 3). -4. **Measure again after the change.** Keep the benchmark and its correctness checks. +1. Estimate first. JVM memory for an object is roughly its header, fields, and alignment padding, plus every object it references. Chapter 2 gives formulas and local measurements so you can estimate the cost of a million objects before changing code. +2. Measure next. Use JOL (`ClassLayout`/`GraphLayout`) for object sizes; JMH `-prof gc` and `gc.alloc.rate.norm`, or `ThreadMXBean.getThreadAllocatedBytes`, for allocation rates; async-profiler or JFR for CPU hotspots; and JMH for throughput and latency. Do not wrap a loop in `System.nanoTime()` and call it a benchmark: JIT warmup, dead-code elimination, and constant folding can invalidate the result. +3. Optimize hotspots only. Bit-packing a configuration object created a few dozen times is usually wasted effort. Replacing a `List` with ten million entries by an `int[]` can make a large difference (see the measurements in Chapter 3). +4. Measure again after the change. Keep the benchmark and its correctness checks. Chapter 14 explains how to use the measurement tools. diff --git a/packages/jvmperf/docs/guide-src/en/02_layout.md b/packages/jvmperf/docs/guide-src/en/02_layout.md index 2a7de41..ee60289 100644 --- a/packages/jvmperf/docs/guide-src/en/02_layout.md +++ b/packages/jvmperf/docs/guide-src/en/02_layout.md @@ -203,7 +203,7 @@ public class J01_Boxing { Key points: -- `Integer.valueOf` caches values from −128 through 127 (the upper bound can be raised with `-XX:AutoBoxCacheMax`; it was 128 by default on the measurement machine). **Never compare wrapper objects with `==`.** +- `Integer.valueOf` caches values from −128 through 127 (the upper bound can be raised with `-XX:AutoBoxCacheMax`; it was 128 by default on the measurement machine). Never compare wrapper objects with `==`. - For streams, use `mapToInt(...).sum()` instead of `map(...).reduce(0, Integer::sum)`, which boxes each intermediate value. - C2 has `EliminateAutoBox` enabled by default on the measurement machine. It can remove some local boxing and unboxing, but cannot remove objects stored in a collection. diff --git a/packages/jvmperf/docs/guide-src/en/04_case.md b/packages/jvmperf/docs/guide-src/en/04_case.md index 59a864c..28db592 100644 --- a/packages/jvmperf/docs/guide-src/en/04_case.md +++ b/packages/jvmperf/docs/guide-src/en/04_case.md @@ -1,10 +1,10 @@ ## 4. Case study: memory used by a hybrid car -A popular C/C++ example models whether a car has an electric motor or a gasoline engine. Two `int` fields use 8 bytes; one `int` bit field uses 4; a `uint8_t` uses 1. The JVM behaves differently because **an object's size is determined by its header and 8-byte alignment**. +A popular C/C++ example models whether a car has an electric motor or a gasoline engine. Two `int` fields use 8 bytes; one `int` bit field uses 4; a `uint8_t` uses 1. The JVM behaves differently because an object's size is determined by its header and 8-byte alignment. ### 4.1 A single object: packing still leaves 16 bytes -**Measured locally with JOL 0.17 on JDK 25.0.4.1** (`research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`): +Measured locally with JOL 0.17 on JDK 25.0.4.1 (`research/jol-jdk25.txt`, `research/jol-kotlin-jdk25.txt`): | Representation | Size in C | Default JVM | `+UseCompactObjectHeaders` | |---|---:|---:|---:| @@ -17,7 +17,7 @@ A popular C/C++ example models whether a car has an electric motor or a gasoline | Kotlin `data class HybridInts(Int, Int)` | — | 24 | 16 | | Kotlin `class Car(val fuels: FuelFlags)` (`FuelFlags` is a value class wrapping `Byte`) | — | 16 | 16 | -The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byte object. Changing `int` to `byte` only creates more padding. Two `int` fields need 20 bytes before alignment, so the object occupies 24 bytes. Compact headers reduce it to 16 bytes; **that flag can save more than manually packing fields**. `EnumSet` is convenient, but it is a separate object containing a `long` and adds another object when stored as a field. +The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byte object. Changing `int` to `byte` only creates more padding. Two `int` fields need 20 bytes before alignment, so the object occupies 24 bytes. Compact headers reduce it to 16 bytes; that flag can save more than manually packing fields. `EnumSet` is convenient, but it is a separate object containing a `long` and adds another object when stored as a field. ### 4.2 One thousand cars: avoid one object per car @@ -33,7 +33,7 @@ The default header is 12 bytes, so up to 4 bytes of fields still fit in a 16-byt On the JVM: 1. Packing flags inside one object rarely saves memory unless it crosses an 8-byte alignment boundary. -2. The large saving comes from replacing an **array of objects** with **primitive arrays** (Structure of Arrays; see Chapter 5). The example drops from 20 KB to 1 KB and removes 1,000 pointer chases. +2. The large saving comes from replacing an array of objects with primitive arrays (Structure of Arrays; see Chapter 5). The example drops from 20 KB to 1 KB and removes 1,000 pointer chases. 3. Bit encodings (`byte` operations, `EnumSet`, `BitSet`) matter more at the array level. Prefer readable fields for individual objects. Java version: diff --git a/packages/jvmperf/docs/guide-src/en/07_kotlin.md b/packages/jvmperf/docs/guide-src/en/07_kotlin.md index b142621..c007ae1 100644 --- a/packages/jvmperf/docs/guide-src/en/07_kotlin.md +++ b/packages/jvmperf/docs/guide-src/en/07_kotlin.md @@ -329,7 +329,7 @@ fun main() { ### 7.9 Coroutines -Each `suspend` function compiles to a state machine and needs a continuation object when it actually suspends. Each `launch` or `async` also creates an object. Split CPU-heavy work into chunks; **do not launch one coroutine per element**. +Each `suspend` function compiles to a state machine and needs a continuation object when it actually suspends. Each `launch` or `async` also creates an object. Split CPU-heavy work into chunks; do not launch one coroutine per element. `examples/kotlin/K10_Coroutines.kt` diff --git a/packages/jvmperf/docs/guide-src/en/08_jit.md b/packages/jvmperf/docs/guide-src/en/08_jit.md index 78e444b..c9e5533 100644 --- a/packages/jvmperf/docs/guide-src/en/08_jit.md +++ b/packages/jvmperf/docs/guide-src/en/08_jit.md @@ -62,7 +62,7 @@ Avoid it for ordinary business objects. Off-heap data needs manual serialization ### 8.2 Direct `ByteBuffer` -This older API is limited by `-XX:MaxDirectMemorySize` (zero by default means the maximum heap size) and is indirectly freed by a `Cleaner` during GC. Allocation and release cost more than heap memory, so **reuse direct buffers** (see Chapter 10) instead of creating one per request. A `ByteBuffer` is limited to 2 GB because it uses `int` indexes; `MemorySegment` has no such limit. +This older API is limited by `-XX:MaxDirectMemorySize` (zero by default means the maximum heap size) and is indirectly freed by a `Cleaner` during GC. Allocation and release cost more than heap memory, so reuse direct buffers (see Chapter 10) instead of creating one per request. A `ByteBuffer` is limited to 2 GB because it uses `int` indexes; `MemorySegment` has no such limit. ### 8.3 I/O @@ -117,7 +117,7 @@ Sources: [JEP 454: FFM API](https://openjdk.org/jeps/454) · [`Arena`](https://d ### 9.1 Escape analysis and scalar replacement -C2 checks whether an object escapes a method (for example, by being stored in a field or array, returned, or passed to a method that is not inlined). A non-escaping object can be **scalar-replaced**: its fields become local variables and the object is never allocated on the heap. +C2 checks whether an object escapes a method (for example, by being stored in a field or array, returned, or passed to a method that is not inlined). A non-escaping object can be scalar-replaced: its fields become local variables and the object is never allocated on the heap. **Measured locally** with `research/EaProbe.java`, which uses `ThreadMXBean.getThreadAllocatedBytes` to measure 1,000 iterations of `new Vec(...).plus(new Vec(1,1))`: @@ -157,7 +157,7 @@ public class J08_EscapeAnalysis { } ``` -For escape-analysis-friendly code, keep methods small enough to inline, do not store short-lived objects in fields or collections, and avoid complex control-flow merges. Small arrays can also be scalar-replaced, up to a limit (64 elements on the measurement machine: `EliminateAllocationArraySizeLimit`). **Do not rely on this optimization:** escape analysis takes effect only after C2 compilation, not during interpretation or C1. Android ART uses different AOT/JIT optimizations. +For escape-analysis-friendly code, keep methods small enough to inline, do not store short-lived objects in fields or collections, and avoid complex control-flow merges. Small arrays can also be scalar-replaced, up to a limit (64 elements on the measurement machine: `EliminateAllocationArraySizeLimit`). Do not rely on this optimization: escape analysis takes effect only after C2 compilation, not during interpretation or C1. Android ART uses different AOT/JIT optimizations. **Evidence (correlate):** Static JP smells vs HotSpot wall-clock/B/op — `scripts/run-static-vs-hotspot.sh` output `research/benches/static-vs-hotspot.jsonl` (cases 4 EA, 6 Vector control; JP rules never observe C1/C2 or call-site type profile). Package gate: `docs/evidence-policy.md`. @@ -203,7 +203,7 @@ public class J09_JitFriendly { ### 9.3 Intrinsics -HotSpot intrinsics such as `Integer.bitCount`, `Long.numberOfTrailingZeros`, `Math.fma`, `System.arraycopy`, `Arrays.equals`/`mismatch`, `String.indexOf`, and CRC32 compile to specialized instruction sequences. **Prefer JDK methods over hand-written bit tricks intended to optimize them.** +HotSpot intrinsics such as `Integer.bitCount`, `Long.numberOfTrailingZeros`, `Math.fma`, `System.arraycopy`, `Arrays.equals`/`mismatch`, `String.indexOf`, and CRC32 compile to specialized instruction sequences. Prefer JDK methods over hand-written bit tricks intended to optimize them. ### 9.4 Vector API (still incubating) @@ -335,7 +335,7 @@ Do not add padding without real write contention; it can waste 256 bytes per fie Virtual threads were finalized in JDK 21 (JEP 444). They suit services with many tasks that spend most of their time blocked on I/O. -- Use one virtual thread per task; **do not pool virtual threads**. Use a `Semaphore` to limit access to downstream resources. +- Use one virtual thread per task; do not pool virtual threads. Use a `Semaphore` to limit access to downstream resources. - Since JDK 24, `synchronized` no longer pins a virtual thread to its carrier (JEP 491). Native and foreign-function calls can still pin it. - CPU-bound tasks do not benefit; use a platform-thread pool or `ForkJoinPool`. - `ThreadLocal` memory adds up across millions of virtual threads. Replace `ThreadLocal` caches of large objects such as `SimpleDateFormat`; consider Scoped Values for shared data (finalized in JDK 25, JEP 506). @@ -368,7 +368,7 @@ public class J11_VirtualThreads { ### 10.3 Object pools: when to use them -Allocation in a thread-local allocation buffer (TLAB) is a pointer bump, and collecting short-lived young-generation objects is very cheap. Pooling small objects is often **slower**: they may be promoted to the old generation, need clearing or resetting, require synchronization, and can leak. Pool only genuinely expensive resources such as direct `ByteBuffer`s, large arrays, database connections, threads, or costly non-thread-safe objects such as `MessageDigest` and `Cipher`. +Allocation in a thread-local allocation buffer (TLAB) is a pointer bump, and collecting short-lived young-generation objects is very cheap. Pooling small objects is often slower: they may be promoted to the old generation, need clearing or resetting, require synchronization, and can leak. Pool only genuinely expensive resources such as direct `ByteBuffer`s, large arrays, database connections, threads, or costly non-thread-safe objects such as `MessageDigest` and `Cipher`. **Evidence (local):** JMH 1.37 on JDK 25 (`J24_PoolVsAlloc`) — batch of small escaping `Point` objects via TLAB `new` + `Blackhole` vs thread-local `ArrayDeque` pool reuse (`-prof gc`). JSONL: `research/bench-results/J24_PoolVsAlloc.jsonl` (re-run before citing). Pool cuts B/op near zero; wall time is often worse than TLAB for this size. diff --git a/packages/jvmperf/docs/guide-src/en/12_android.md b/packages/jvmperf/docs/guide-src/en/12_android.md index 2e6c170..044ef5f 100644 --- a/packages/jvmperf/docs/guide-src/en/12_android.md +++ b/packages/jvmperf/docs/guide-src/en/12_android.md @@ -2,7 +2,7 @@ ### 12.1 ART vs HotSpot -- ART has no C2 compiler or HotSpot-style escape analysis and scalar replacement. It combines interpretation, JIT compilation, and profile-guided AOT compilation (`dex2oat`). **Do not assume the JIT will remove allocations on Android**; avoid them in hot paths such as `onDraw`, scroll callbacks, and animation frames. +- ART has no C2 compiler or HotSpot-style escape analysis and scalar replacement. It combines interpretation, JIT compilation, and profile-guided AOT compilation (`dex2oat`). Do not assume the JIT will remove allocations on Android; avoid them in hot paths such as `onDraw`, scroll callbacks, and animation frames. - References are already 32-bit and object headers are 8 bytes (`mirror::Object` has 32-bit class and monitor fields). ART has no compressed-reference or compact-header switch. - Since Android 14, ART Mainline updates use Concurrent Mark Compact (CMC), based on `userfaultfd`, by default. Android 16 QPR2 adds generational CMC, which collects recently allocated objects first. diff --git a/packages/jvmperf/docs/guide-src/en/15_lint.md b/packages/jvmperf/docs/guide-src/en/15_lint.md index 0d7475a..8b00b48 100644 --- a/packages/jvmperf/docs/guide-src/en/15_lint.md +++ b/packages/jvmperf/docs/guide-src/en/15_lint.md @@ -1,6 +1,6 @@ ## 15. jvmperf-lint: find patterns automatically -`jvmperf-lint/` is the guide's static checker. It is written in Java and uses tree-sitter via the JVM bindings (`io.github.bonede:tree-sitter` with `tree-sitter-java` and `tree-sitter-kotlin`) to inspect `.java`, `.kt`, and `.kts` files. It reads syntax only and **does not resolve types**. This keeps it fast and configuration-free, but it can report false positives or miss issues; each rule documents its risk. See [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md) for installation and usage. +`jvmperf-lint/` is the guide's static checker. It is written in Java and uses tree-sitter via the JVM bindings (`io.github.bonede:tree-sitter` with `tree-sitter-java` and `tree-sitter-kotlin`) to inspect `.java`, `.kt`, and `.kts` files. It reads syntax only and does not resolve types. This keeps it fast and configuration-free, but it can report false positives or miss issues; each rule documents its risk. See [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md) for installation and usage. ```sh cd jvmperf-lint @@ -22,8 +22,8 @@ Default severity is `error`, `hint`, or `off`, as printed by `bin/jvmperf-lint - | jp-unpresized-collection | `unpresized-collection` | off | Java/Kotlin | A collection or `StringBuilder` is filled in a loop without initial capacity | 5.2 | | jp-boxed-stream | `boxed-stream` | error | Java | `reduce(0, Integer::sum)` or `Collectors.summingInt` on `Stream` | 3.1 | | jp-exception-control-flow | `exception-control-flow` | off | Java/Kotlin | A loop catches parse exceptions to make a decision | 9.5 | -| jp-legacy-synchronized-type | — | **removed** | — | measured ~1.08×; see `evidence/_removed/` | — | -| jp-many-boolean-fields | — | **removed** | — | COH no win; see `evidence/_removed/` | — | +| jp-legacy-synchronized-type | — | removed | — | measured ~1.08×; see `evidence/_removed/` | — | +| jp-many-boolean-fields | — | removed | — | COH no win; see `evidence/_removed/` | — | | jp-enum-key-hash-collection | `enum-key-hash-collection` | error | Java/Kotlin | `HashMap` keyed by an enum or `HashSet` of enum values | 5.3 | | jp-collection-chain | `collection-chain` | off | Kotlin | A chain of 3+ `map`/`filter` operations, or 2 operations plus short-circuiting such as `first`, `take`, or `any` | 7.4 | | jp-value-class-boxing | `value-class-boxing` | hint | Kotlin | A value class used as a nullable value, generic argument, or array element | 7.2 | diff --git a/packages/jvmperf/docs/guide-src/zh-CN/00_head.md b/packages/jvmperf/docs/guide-src/zh-CN/00_head.md index 7d85640..a571c64 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/00_head.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/00_head.md @@ -4,9 +4,9 @@ > 写作日期:2026-10-21。 > > 约定: -> - 文中所有代码都来自 `examples/` 目录下的独立文件,**全部在本机编译并运行通过**(Java:`javac --release 25` + 运行自检;Kotlin:`kotlinc 2.4.21 -jvm-target 25` + 运行自检;Android:对 `android-34/android.jar` 编译)。编译日志见 `research/*-check.log`。本文提到的 `research/` 下的 `*.txt`、`*.log`、`bench-results/`、`benches/` 是本地运行输出,不入库。 -> - 文中出现的字节数,凡标注 **【本机实测】** 的,都是用 JOL 0.17 在本机(x86_64 Linux,8 核,JDK 25.0.4.1,默认 G1、压缩指针开启)测得,原始输出见 `research/jol-jdk25.txt`、`research/jol-kotlin-jdk25.txt`、`research/ea-probe.txt`、`research/jvm-flags-jdk25.txt`。其他环境(不同 JDK、关闭压缩指针、ART)数字会不同。 -> - 本文**不给出自己编造的加速比**。需要性能数字时请用第 14 章的 JMH 模板在你的机器上测。 +> - 文中所有代码都来自 `examples/` 目录下的独立文件,全部在本机编译并运行通过(Java:`javac --release 25` + 运行自检;Kotlin:`kotlinc 2.4.21 -jvm-target 25` + 运行自检;Android:对 `android-34/android.jar` 编译)。编译日志见 `research/*-check.log`。本文提到的 `research/` 下的 `*.txt`、`*.log`、`bench-results/`、`benches/` 是本地运行输出,不入库。 +> - 文中出现的字节数,凡标注 【本机实测】 的,都是用 JOL 0.17 在本机(x86_64 Linux,8 核,JDK 25.0.4.1,默认 G1、压缩指针开启)测得,原始输出见 `research/jol-jdk25.txt`、`research/jol-kotlin-jdk25.txt`、`research/ea-probe.txt`、`research/jvm-flags-jdk25.txt`。其他环境(不同 JDK、关闭压缩指针、ART)数字会不同。 +> - 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。本文不编造加速比;请用第 14 章的 JMH 模板在你的机器上测。 > - 每节末尾给出来源链接;每个技巧都写了"什么时候不要用"。 ## 目录 @@ -40,9 +40,9 @@ | JDK 22 | FFM API(`MemorySegment`/`Arena`)正式(JEP 454) | | JDK 23 | 分代 ZGC 成为 ZGC 默认模式(JEP 474) | | JDK 24 | 紧凑对象头实验版(JEP 450);非分代 ZGC 移除(JEP 490);`synchronized` 不再钉住虚拟线程(JEP 491);AOT 类加载缓存(JEP 483) | -| **JDK 25(LTS)** | 紧凑对象头成为正式产品特性但**默认关闭**(JEP 519,`-XX:+UseCompactObjectHeaders`);分代 Shenandoah 正式(JEP 521);Vector API 第 10 轮孵化(JEP 508);Stable Values 预览(JEP 502);AOT 命令行简化与方法 profile(JEP 514/515);JFR CPU 时间采样(实验,JEP 509) | +| JDK 25(LTS) | 紧凑对象头成为正式产品特性但默认关闭(JEP 519,`-XX:+UseCompactObjectHeaders`);分代 Shenandoah 正式(JEP 521);Vector API 第 10 轮孵化(JEP 508);Stable Values 预览(JEP 502);AOT 命令行简化与方法 profile(JEP 514/515);JFR CPU 时间采样(实验,JEP 509) | | JDK 26(2026-03) | G1 减少同步提升吞吐(JEP 522);任意 GC 都可用 AOT 对象缓存(JEP 516);Stable Values 改名 Lazy Constants(JEP 526,预览);Vector API 第 11 轮孵化 | -| JDK 27(2026-09,非 LTS) | **紧凑对象头默认开启**(JEP 534);G1 在所有环境下都是默认 GC(JEP 523);Vector API 第 12 轮孵化(JEP 537,无 API 变化) | +| JDK 27(2026-09,非 LTS) | 紧凑对象头默认开启(JEP 534);G1 在所有环境下都是默认 GC(JEP 523);Vector API 第 12 轮孵化(JEP 537,无 API 变化) | | JDK 28(计划 2027-03) | Valhalla 的值类(JEP 401)以预览形式进入主线(据 InfoQ 报道),之后 Vector API 才会从孵化转为预览 | | Kotlin 2.0 | K2 编译器;lambda 默认用 `invokedynamic` 生成(`-Xlambdas=indy`) | | Kotlin 2.4(2026-06,当前 2.4.21) | 上下文参数等稳定;支持 Java 26 目标;不再能用 `-language-version 1.9` 回退到 K1 | @@ -54,16 +54,15 @@ ## 1. 方法论:先估算、再测量 -1. **先估算**:JVM 上"一个东西"的内存 ≈ 对象头 + 字段 + 对齐填充,再加上它引用的所有对象。第 2 章给出公式和本机实测值,写代码前就能算出 100 万个对象大约要多少内存。 -2. **再测量**: +1. 先估算:JVM 上"一个东西"的内存 ≈ 对象头 + 字段 + 对齐填充,再加上它引用的所有对象。第 2 章给出公式和本机实测值,写代码前就能算出 100 万个对象大约要多少内存。 +2. 再测量: - 对象大小 → JOL(`ClassLayout` / `GraphLayout`); - 分配速率 → JMH `-prof gc` 的 `gc.alloc.rate.norm`(每次操作分配多少字节),或 `ThreadMXBean.getThreadAllocatedBytes`; - CPU 热点 → async-profiler / JFR; - 吞吐与延迟 → JMH(不要用 `System.nanoTime()` 包一个循环,JIT 预热、死代码消除、常量折叠都会让结果失真)。 -3. **只优化热点**。对一个只会创建几十个实例的配置类做位压缩,是浪费时间;对一个装了 1000 万元素的 `List` 改成 `int[]`,是数量级的收益(见第 3 章实测)。 -4. **改完再测一次**,保留测试和断言。 +3. 只优化热点。对一个只会创建几十个实例的配置类做位压缩,是浪费时间;对一个装了 1000 万元素的 `List` 改成 `int[]`,是数量级的收益(见第 3 章实测)。 +4. 改完再测一次,保留测试和断言。 工具用法见第 14 章。 --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/02_layout.md b/packages/jvmperf/docs/guide-src/zh-CN/02_layout.md index 0084376..b62fd94 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/02_layout.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/02_layout.md @@ -202,7 +202,7 @@ public class J01_Boxing { ``` 要点: -- `Integer.valueOf` 只缓存 −128..127(`-XX:AutoBoxCacheMax` 可以调大上限,本机默认值 128)。**永远不要用 `==` 比较包装类型**。 +- `Integer.valueOf` 只缓存 −128..127(`-XX:AutoBoxCacheMax` 可以调大上限,本机默认值 128)。永远不要用 `==` 比较包装类型。 - 流:用 `mapToInt(...).sum()`,不要 `map(...).reduce(0, Integer::sum)`(每一步都装箱)。 - C2 有 `EliminateAutoBox`(本机默认开启),能消除部分局部的装箱/拆箱,但对放进集合里的对象无能为力。 @@ -253,4 +253,3 @@ public class J02_PrimitiveCollections { 来源:[Integer.valueOf](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#valueOf(int)) · [fastutil](https://fastutil.di.unimi.it/) · [Eclipse Collections](https://eclipse.dev/collections/) · [Koloboke 提交历史](https://github.com/leventov/Koloboke/commits/master/) --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/04_case.md b/packages/jvmperf/docs/guide-src/zh-CN/04_case.md index 2948d64..53c2c2a 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/04_case.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/04_case.md @@ -242,7 +242,7 @@ public class J13_Presize { ### 5.3 枚举键:EnumMap / EnumSet -`EnumMap` 内部是按 `ordinal()` 索引的数组,`EnumSet`(≤64 个常量)内部是一个 `long` 位向量。**【本机实测】** 3 个枚举常量时,`EnumMap` 自身 + 值数组共 72 字节(紧凑对象头 64 字节),没有 `HashMap.Node`。 +`EnumMap` 内部是按 `ordinal()` 索引的数组,`EnumSet`(≤64 个常量)内部是一个 `long` 位向量。【本机实测】 3 个枚举常量时,`EnumMap` 自身 + 值数组共 72 字节(紧凑对象头 64 字节),没有 `HashMap.Node`。 **不要用的时候**:需要 `null` 键(`EnumMap`/`EnumSet` 不允许);键类型不止一种枚举。 @@ -283,7 +283,7 @@ public class J18_Records { ### 6.1 Compact Strings 与字符串去重 -- JDK 9 起 `String` 内部是 `byte[]` + `coder`(JEP 254):纯 Latin-1 内容每字符 1 字节,否则 UTF-16 每字符 2 字节。**【本机实测】** 20 个 ASCII 字符的 `String`(含 `byte[]`)64 字节,20 个汉字 80 字节。`-XX:-CompactStrings` 可关闭(本机默认开启),几乎没有理由关。 +- JDK 9 起 `String` 内部是 `byte[]` + `coder`(JEP 254):纯 Latin-1 内容每字符 1 字节,否则 UTF-16 每字符 2 字节。【本机实测】 20 个 ASCII 字符的 `String`(含 `byte[]`)64 字节,20 个汉字 80 字节。`-XX:-CompactStrings` 可关闭(本机默认开启),几乎没有理由关。 - `-XX:+UseStringDeduplication`:GC 在后台把**内容相同的 `String` 的底层数组**合并(`String` 对象本身仍各自存在)。最初只有 G1(JEP 192),JDK 18 起 Serial、Parallel、ZGC 也加入了支持(JDK-8272609 / 8267185 / 8267186)。本机默认关闭。适合"大量重复字符串长期存活"的服务(例如反序列化出来的大量相同字段值)。 - `String.intern()`:返回字符串常量池里的规范实例,之后可以用 `==` 比较、只保留一份对象。只适合**取值少、重复多、长期存活**的场景;对高基数的值调用 `intern()` 会把常量池(本质是一个 native 哈希表)撑大。很多时候用自己的 `HashMap` 去重更可控。 @@ -404,4 +404,3 @@ public class J15_Regex { 来源:[JEP 254 Compact Strings](https://openjdk.org/jeps/254) · [JEP 192 String Deduplication in G1](https://openjdk.org/jeps/192) · [JDK 18 发布说明(String Deduplication 扩展到其他 GC)](https://www.oracle.com/java/technologies/javase/18-relnote-issues.html) · [JEP 280 Indify String Concatenation](https://openjdk.org/jeps/280) · [String.split](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/String.html#split(java.lang.String)) · [Kotlin -Xstring-concat](https://kotlinlang.org/docs/compiler-reference.html#xstring-concat) --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/07_kotlin.md b/packages/jvmperf/docs/guide-src/zh-CN/07_kotlin.md index f87b420..dd3c0d8 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/07_kotlin.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/07_kotlin.md @@ -323,7 +323,7 @@ fun main() { ### 7.9 协程 -每个 `suspend` 函数编译成状态机,真正挂起时需要续体(continuation)对象;每个 `launch`/`async` 也是对象。CPU 密集任务要分块,**不要每个元素开一个协程**。 +每个 `suspend` 函数编译成状态机,真正挂起时需要续体(continuation)对象;每个 `launch`/`async` 也是对象。CPU 密集任务要分块,不要每个元素开一个协程。 `examples/kotlin/K10_Coroutines.kt` @@ -398,4 +398,3 @@ fun main() { 来源:[Kotlin inline value classes](https://kotlinlang.org/docs/inline-classes.html) · [Inline functions](https://kotlinlang.org/docs/inline-functions.html) · [Kotlin 2.0:invokedynamic 生成 lambda](https://kotlinlang.org/docs/whatsnew20.html) · [Sequences](https://kotlinlang.org/docs/sequences.html) · [lazy](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/lazy.html) · [Java 互操作:@JvmField / const](https://kotlinlang.org/docs/java-to-kotlin-interop.html) · [Coroutines 基础](https://kotlinlang.org/docs/coroutines-basics.html) · [androidx.collection 版本说明](https://developer.android.com/jetpack/androidx/releases/collection) --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/08_jit.md b/packages/jvmperf/docs/guide-src/zh-CN/08_jit.md index 8e79163..6365f3d 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/08_jit.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/08_jit.md @@ -62,7 +62,7 @@ public class J06_OffHeapFfm { ### 8.2 direct `ByteBuffer` -老 API,容量受 `-XX:MaxDirectMemorySize` 限制(默认 0 表示与最大堆相同),由 `Cleaner` 随 GC 间接释放——分配/释放都比堆内贵,**适合长期复用**(见第 10 章对象池),不适合每个请求新建。单个 `ByteBuffer` 最大 2 GB(`int` 索引),`MemorySegment` 没有此限制。 +老 API,容量受 `-XX:MaxDirectMemorySize` 限制(默认 0 表示与最大堆相同),由 `Cleaner` 随 GC 间接释放——分配/释放都比堆内贵,适合长期复用(见第 10 章对象池),不适合每个请求新建。单个 `ByteBuffer` 最大 2 GB(`int` 索引),`MemorySegment` 没有此限制。 ### 8.3 I/O @@ -115,9 +115,9 @@ public class J12_Io { ### 9.1 逃逸分析与标量替换 -C2 会分析对象是否"逃出"方法(存进字段/数组、作为返回值、传给未内联的方法)。不逃逸的对象可以被**标量替换**——拆成几个局部变量,根本不在堆上分配。 +C2 会分析对象是否"逃出"方法(存进字段/数组、作为返回值、传给未内联的方法)。不逃逸的对象可以被标量替换——拆成几个局部变量,根本不在堆上分配。 -**【本机实测】**(`research/EaProbe.java`,`ThreadMXBean.getThreadAllocatedBytes` 统计 1000 次循环中每次 `new Vec(...).plus(new Vec(1,1))` 的分配量): +【本机实测】(`research/EaProbe.java`,`ThreadMXBean.getThreadAllocatedBytes` 统计 1000 次循环中每次 `new Vec(...).plus(new Vec(1,1))` 的分配量): | 运行参数 | 每次调用分配 | |---|---| @@ -155,7 +155,7 @@ public class J08_EscapeAnalysis { } ``` -写出 EA 友好代码的要点:小方法(能被内联)、对象不存入字段/集合、不跨越 `synchronized` 以外的复杂控制流合并点。小数组同样可以被标量替换,但有上限(本机 `EliminateAllocationArraySizeLimit = 64` 个元素)。**不要依赖它**:EA 只在 C2 编译后生效,解释执行与 C1 阶段照样分配;Android ART 的 AOT/JIT 编译器做的优化不同。 +写出 EA 友好代码的要点:小方法(能被内联)、对象不存入字段/集合、不跨越 `synchronized` 以外的复杂控制流合并点。小数组同样可以被标量替换,但有上限(本机 `EliminateAllocationArraySizeLimit = 64` 个元素)。不要依赖它:EA 只在 C2 编译后生效,解释执行与 C1 阶段照样分配;Android ART 的 AOT/JIT 编译器做的优化不同。 ### 9.2 内联与调用点形态 @@ -199,7 +199,7 @@ public class J09_JitFriendly { ### 9.3 intrinsic -`Integer.bitCount`、`Long.numberOfTrailingZeros`、`Math.fma`、`System.arraycopy`、`Arrays.equals/mismatch`、`String.indexOf`、CRC32 等是 HotSpot intrinsic,编译成专门的指令序列。**优先用 JDK 方法,不要手写位技巧去"优化"它们**。 +`Integer.bitCount`、`Long.numberOfTrailingZeros`、`Math.fma`、`System.arraycopy`、`Arrays.equals/mismatch`、`String.indexOf`、CRC32 等是 HotSpot intrinsic,编译成专门的指令序列。优先用 JDK 方法,不要手写位技巧去“优化”它们。 ### 9.4 Vector API(仍在孵化) @@ -327,7 +327,7 @@ public class J10_FalseSharing { JDK 21 正式(JEP 444)。适合"大量任务、大部分时间在阻塞 I/O"的服务。要点: -- 每个任务一个虚拟线程,**不要池化虚拟线程**;用 `Semaphore` 限制并发访问下游资源。 +- 每个任务一个虚拟线程,不要池化虚拟线程;用 `Semaphore` 限制并发访问下游资源。 - JDK 24 起 `synchronized` 不再把虚拟线程钉在载体线程上(JEP 491);调用本地方法/外部函数时仍会钉住。 - CPU 密集任务没有收益,用平台线程池 / `ForkJoinPool`。 - `ThreadLocal` 在百万虚拟线程下会放大内存,缓存大对象(如 `SimpleDateFormat`)的 `ThreadLocal` 写法要改掉;共享数据考虑 Scoped Values(JDK 25 正式,JEP 506)。 @@ -357,7 +357,7 @@ public class J11_VirtualThreads { ### 10.3 对象池:何时用、何时不用 -TLAB(线程本地分配缓冲)里的分配只是指针碰撞,年轻代回收短命对象几乎免费。池化小对象通常**更慢**:对象会被提升到老年代、需要清零/重置、需要并发控制、容易泄漏。只池化真正昂贵的东西:direct `ByteBuffer`、大数组、数据库连接、线程、`MessageDigest`/`Cipher` 之类初始化昂贵且非线程安全的对象。 +TLAB(线程本地分配缓冲)里的分配只是指针碰撞,年轻代回收短命对象几乎免费。池化小对象通常更慢:对象会被提升到老年代、需要清零/重置、需要并发控制、容易泄漏。只池化真正昂贵的东西:direct `ByteBuffer`、大数组、数据库连接、线程、`MessageDigest`/`Cipher` 之类初始化昂贵且非线程安全的对象。 `examples/java/J16_Pool.java` @@ -438,4 +438,3 @@ java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar 来源:[HotSpot GC 调优指南(JDK 25)](https://docs.oracle.com/en/java/javase/25/gctuning/) · [JEP 474](https://openjdk.org/jeps/474) · [JEP 490](https://openjdk.org/jeps/490) · [JEP 521](https://openjdk.org/jeps/521) · [JEP 522](https://openjdk.org/jeps/522) · [JEP 523](https://openjdk.org/jeps/523) · [JEP 318 Epsilon](https://openjdk.org/jeps/318) · [JEP 483](https://openjdk.org/jeps/483) · [JEP 514](https://openjdk.org/jeps/514) · [JEP 515](https://openjdk.org/jeps/515) · [JEP 516](https://openjdk.org/jeps/516) --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/12_android.md b/packages/jvmperf/docs/guide-src/zh-CN/12_android.md index ebf70d4..86c9966 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/12_android.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/12_android.md @@ -2,7 +2,7 @@ ### 12.1 ART 与 HotSpot 的差别 -- 没有 C2,也没有 HotSpot 的逃逸分析/标量替换能力;ART 是"解释器 + JIT + 基于 profile 的 AOT(dex2oat)"混合模式。**在 Android 上"让 JIT 帮你消除分配"的假设不成立**,热路径(`onDraw`、滚动回调、动画帧)里的分配要真正避免。 +- 没有 C2,也没有 HotSpot 的逃逸分析/标量替换能力;ART 是"解释器 + JIT + 基于 profile 的 AOT(dex2oat)"混合模式。在 Android 上“让 JIT 帮你消除分配”的假设不成立,热路径(`onDraw`、滚动回调、动画帧)里的分配要真正避免。 - 引用本来就是 32 位;对象头 8 字节(`mirror::Object` 只有 class 指针 + monitor 两个 32 位字段),没有压缩指针或紧凑对象头开关。 - GC:Android 14 起(通过 ART mainline 模块更新)默认是并发标记整理(CMC,基于 userfaultfd);Android 16 QPR2 起引入分代 CMC,优先回收新分配的对象。 @@ -175,4 +175,3 @@ public class J19_JmhBench { 来源:[JMH samples](https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples) · [JEP 328 Flight Recorder](https://openjdk.org/jeps/328) · [JEP 509 JFR CPU-Time Profiling](https://openjdk.org/jeps/509) · [async-profiler](https://github.com/async-profiler/async-profiler) · [JOL](https://github.com/openjdk/jol) · [Jetpack Benchmark](https://developer.android.com/topic/performance/benchmarking/benchmarking-overview) --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/15_lint.md b/packages/jvmperf/docs/guide-src/zh-CN/15_lint.md index 7cc22e9..161724c 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/15_lint.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/15_lint.md @@ -1,6 +1,6 @@ ## 15. jvmperf-lint:自动找出反模式 -`jvmperf-lint/` 是本指南配套的静态检查器:Java + tree-sitter(JVM 绑定 `io.github.bonede:tree-sitter`,以及 `tree-sitter-java` / `tree-sitter-kotlin`),同时支持 `.java`、`.kt`、`.kts`。它只看语法树,**不做类型解析**,所以快、零配置、不需要编译项目,但会有误报/漏报(每条规则都写明了误报风险)。完整说明见 [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md)。 +`jvmperf-lint/` 是本指南配套的静态检查器:Java + tree-sitter(JVM 绑定 `io.github.bonede:tree-sitter`,以及 `tree-sitter-java` / `tree-sitter-kotlin`),同时支持 `.java`、`.kt`、`.kts`。它只看语法树,不做类型解析,所以快、零配置、不需要编译项目,但会有误报/漏报(每条规则都写明了误报风险)。完整说明见 [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md)。 ```sh cd jvmperf-lint @@ -158,4 +158,3 @@ good 夹具上 detekt 只报了一处 `EmptyFunctionBlock`(`close() {}`)。 交叉验证:`scripts/run-static-vs-hotspot.sh` 的输出写在 `research/benches/`。**静态 lint 不等于 HotSpot 的判断**:不能只看源码就断言 C2 的编译层级或逃逸分析失败,也不要用 PrintCompilation 的行数当作“一致/不一致”的判据。 --- - diff --git a/packages/jvmperf/docs/guide-src/zh-CN/16_cheat.md b/packages/jvmperf/docs/guide-src/zh-CN/16_cheat.md index a32899a..2fc2b67 100644 --- a/packages/jvmperf/docs/guide-src/zh-CN/16_cheat.md +++ b/packages/jvmperf/docs/guide-src/zh-CN/16_cheat.md @@ -26,4 +26,3 @@ | 自动检查 | jvmperf-lint + PMD/Error Prone + detekt + Android Lint | 静态提示都要用 profiler 确认 | --- - diff --git a/packages/jvmperf/docs/zh-CN/corpus.md b/packages/jvmperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..96f95ff --- /dev/null +++ b/packages/jvmperf/docs/zh-CN/corpus.md @@ -0,0 +1,51 @@ +# jvmperf 语料库目标 + +## 固定目标 + +大型树在此包中**不**被供应,也不会随仓库一起发布。将它们克隆到任何本地目录,并将该路径传递给代码检查工具。这些树没有记录提交版本;请在结果旁边标注你扫描时的修订版本。 + +```sh +export JAVA_HOME=/opt/java/jdk-21.0.12.1+1 # or any JDK 21+ +cd packages/jvmperf/jvmperf-lint && ./gradlew jar +LINT=$PWD/bin/jvmperf-lint +$LINT --no-fail -f json -j 4 +``` + +检查器是 **Java**(`jvmperf-lint`)。这个工具包中没有 Python。 + +默认严重性遵循 `Rule.java`:**错误** jp-string-concat-hot、jp-boxed-stream、jp-enum-key-hash-collection、jp-data-copy-hot、jp-baos-tobytearray-copy、jp-hot-payload-copy、jp-client-factory-recreate、jp-string-intern-hot(G4 JSONL);**提示** jp-value-class-boxing、jp-android-sparse-array、jp-finalize、jp-hot-system-call、jp-bitmap-decode-no-reuse、jp-hot-post-lambda、jp-ui-text-append、jp-non-static-field-updater;其他所有规则均为**关闭**(可通过 `--enable` 选择)。 + +### 库存 + +```text +/ + okhttp/ retrofit/ kotlinx-coroutines/ guava/ vertx/ fastjson2/ + jetty/ # sparse jetty-core + netty/ # sparse buffer + common +``` + +不要将语料库来源提交到 `packages/jvmperf`。 + +## 克隆 + G3 (2026-10-22, 亚洲/上海) — r6 + +| 树 | 范围 | after4 → r5 → **r6** | +|------|-------|---------------------:| +| **okhttp** | 整体 | 48 → 9 → **6** (jp-data-copy-hot) | +| **retrofit** | whole | 6 → 0 → **0** | +| **kotlinx.coroutines** | 全部 | 4 → 0 → **0** | +| **番石榴** | `guava/src` 仅 | 30 → 3 → **1** | +| **vert.x** | whole | 60 → 10 → **1** | +| **fastjson2** | whole | 49 → 12 → **8** | +| **Jetty** | 精简的 `jetty-core` | 267 → 55 → **6** | +| **Netty** | 稀疏 `buffer` + `common` | 24 → 8 → **4** | +| **Total (8 trees)** | | **488 → 97 → 26** | + +**G3 分诊:** 本地运行;分诊记录不随同传送。 + +**语料库准备好:是** — r6 n=26 完整:TP 50%,FP+DOC+SKIP 0%;在 jp-data-copy-hot/jp-hot-payload-copy/jp-finalize 上的真实 TP;每个默认 **错误** 都有 G4 (`evidence/*.md`+`jsonl`)。 + +### 反向优化 + +- 路径:`samples?/examples?/recipes?/demos?/snippets?` → `source_group=test`(加上之前的基准/KMP 测试跳过) +- 将几乎所有的 OK 习语 / 无 G4 热启发式降级为 **关闭**(例如 jp-unpresized-collection、jp-hot-heavy-alloc、jp-hot-format-log) +- 保持测量的有效性主干为 **错误**(例如 jp-string-concat-hot、jp-data-copy-hot、jp-hot-payload-copy) diff --git a/packages/jvmperf/docs/zh-CN/evidence-policy.md b/packages/jvmperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..6335ec2 --- /dev/null +++ b/packages/jvmperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,73 @@ +# jvmperf 证据政策 + +## 门槛 + +只有当以下两者都存在于 `packages/jvmperf/evidence/` 下时,JP 规则才可以被记录/处理为 **`error`**(保留/升级合同以供指南和未来的 `--preset` 使用): + +1. `.md` — 声明、利用,**JDK 版本 + JVM 参数**(使用时包括 `-XX:+UseCompactObjectHeaders`),限制 +2. `.jsonl` — ≥1 个前/后测量对,字段包括 `rule`、`case`、`role`(`before` = 问题,`after` = 修复)、`jdk`、`jvm_args`、`ns_per_op`(或 `median_ms` / `total_size_bytes`)、`alloc_b_per_op`、`ratio`、`ts` + +否则该规则保持 **`hint`** 或 **`off`**(一个审核提示):适用于内部测试和固定装置,但不是“必须为速度修复”的信号。 + +CLI 仍然可能在任何发现时 **以非零退出**(除非使用 `--no-fail`);打印的严重性标签遵循此策略表。完整的 `--preset` 配置稍后可选(镜像 jsperf/cperf)。 + +## 相关兄弟 + +- 实时时钟 / 每操作字节协议:`scripts/run-static-vs-hotspot.sh`(写入 `research/benches/static-vs-hotspot.{json,jsonl}`) + +**不要**因为 PrintCompilation / compilation-log 的行数看起来吓人就升级——那些只是噪音(和 V8 `--trace-deopt` 计数的道理一样)。 + +## 当前映射 + +截至 2026-10-22(亚洲/上海);基准测试在 JDK 25.0.4.1 + COH 上进行。策略是 `Rule.java` 中每条规则的默认严重性。 + +| 规则 | 政策 | 证据 | 关联 / 注释 | +|------|--------|----------|------------------| +| **jp-boxed-primitive-collection** | **关闭** | [jp-boxed-primitive-collection](../../evidence/zh-CN/jp-boxed-primitive-collection.md) | JDK 27 n=1e6 ~**1.32×**, n=1e5 ~**7.37×**; 静默 JDK 21 ~**11×** / ~**8.5×** | +| **jp-string-concat-hot** | **错误** | [jp-string-concat-hot](../../evidence/zh-CN/jp-string-concat-hot.md) | JDK 27 J23 部件=256 ~**21.3×** | +| jp-regex-hot | **关闭** | [jp-regex-hot](../../evidence/zh-CN/jp-regex-hot.md) | JDK 27 时间 ~**0.76×**,分配 ~**2.4×**;安静 JDK 21 时间 ~**1.67×** | +| **jp-format-hot** | **关闭** | [jp-format-hot](../../evidence/zh-CN/jp-format-hot.md) | JDK 27 ~**29×** | +| jp-unpresized-collection | **关闭** | [jp-unpresized-collection](../../evidence/zh-CN/jp-unpresized-collection.md) | JDK 27 n=10k ~**1.43×** | +| **jp-boxed-stream** | **错误** | [jp-boxed-stream](../../evidence/zh-CN/jp-boxed-stream.md) | JDK 27 boxedReduce/mapToInt ~**37×** | +| **jp-exception-control-flow** | **关闭** | [jp-exception-control-flow](../../evidence/zh-CN/jp-exception-control-flow.md) | JDK 27 ~**4.0×**(峰值);静默 JDK 21 ~**27×** | +| jp-legacy-synchronized-type | **已移除** | [墓碑](../../evidence/_removed/jp-legacy-synchronized-type.REMOVED.md) | 测量约 ~1.08× / HT 阴性 — 匹配器已删除 | +| jp-many-boolean-fields | **已移除** | [墓碑](../../evidence/_removed/jp-many-boolean-fields.REMOVED.md) | COH 16=16 无胜 — 匹配器已删除 | +| **jp-enum-key-hash-collection** | **错误** | [jp-enum-key-hash-collection](../../evidence/zh-CN/jp-enum-key-hash-collection.md) | JDK 27 EnumMap ~**11.6×** | +| **jp-collection-chain** | **关闭** | [jp-collection-chain](../../evidence/zh-CN/jp-collection-chain.md) | JDK 27 / Kotlin 2.4.21 eager/seq ~**265×** | +| **jp-value-class-boxing** | **提示** | [jp-value-class-boxing](../../evidence/zh-CN/jp-value-class-boxing.md) | JDK 27 时间 ~**4.0×**, 分配 ~**5.0×** | +| **jp-lambda-to-non-inline-hot** | **关闭** | [jp-lambda-to-non-inline-hot](../../evidence/zh-CN/jp-lambda-to-non-inline-hot.md) | JDK 27 分配 ~**244×**;时间 ~**0.47×** | +| jp-lazy-default-mode | **关闭** | [jp-lazy-default-mode.md](../../evidence/zh-CN/jp-lazy-default-mode.md) 仅 | **待测量** — 情境懒惰模式 | +| **jp-data-copy-hot** | **错误** | [jp-data-copy-hot](../../evidence/zh-CN/jp-data-copy-hot.md) | JDK 27 时间 ~**208×**, 分配 ~**10001×** | +| jp-android-sparse-array | 提示 | 存根 md | **待定** — 需要 ART/设备 | +| **jp-log-hot** | **关闭** | [jp-log-hot](../../evidence/zh-CN/jp-log-hot.md) | JDK 27 时间平均 ~**0.43×**(一次峰值);安静的 JDK 21 ~**18×**;分配 ~**23200 B** | +| jp-sleep-polling | **关闭** | stub md | **待定** — 睡眠占主导;设计不是微型的 | +| jp-最终确认 | 提示 | 存根 md | **待定** — 最终确认正确性 / 更清理 | +| jp-expensive-alloc-hot | **关闭** | [jp-expensive-alloc-hot](../../evidence/zh-CN/jp-expensive-alloc-hot.md) | tiny-pool **负面** | +| jp-hot-heavy-alloc | **关闭** | 存根 md | **待处理** — ART/设备热分配 | +| jp-hot-format-log | **关闭** | stub md | **待处理** — ART/设备热格式/日志 | +| jp-hot-system-call | 提示 | 存根 md | **待定** — ART/设备系统调用 | +| jp-bitmap-decode-no-reuse | 提示 | 桩 MD | **待定** — ART/设备位图 | +| jp-hot-post-lambda | 提示 | 存根 md | **待处理** — ART 处理程序发布 | +| jp-ui-text-append | 提示 | stub md | **待定** — ART UI 文本 | +| **jp-baos-tobytearray-copy** | **错误** | [jp-baos-tobytearray-copy](../../evidence/zh-CN/jp-baos-tobytearray-copy.md) | JDK 27 toByteArray/writeTo ~**99×** | +| **jp-hot-payload-copy** | **错误** | [jp-hot-payload-copy](../../evidence/zh-CN/jp-hot-payload-copy.md) | JDK 27 newFrame/reuse ~**158×** | +| vt-unsafe-threadlocal-pool | —(未发布)| [存根](../../evidence/zh-CN/vt-unsafe-threadlocal-pool.md); 指南固定装置 | guide-first §10.2 | +| **jp-client-factory-recreate** | **错误** | [jp-client-factory-recreate](../../evidence/zh-CN/jp-client-factory-recreate.md) | JDK 27 HttpClient 新建+关闭/重用 ~**1.97e5×** | +| **jp-string-intern-hot** | **错误** | [jp-string-intern-hot](../../evidence/zh-CN/jp-string-intern-hot.md) | JDK 27 intern/plain ~**6.3×** | +| jp-non-static-field-updater | 提示 | [jp-non-static-field-updater](../../evidence/zh-CN/jp-non-static-field-updater.md) | JDK 27 创建 ~**526×**;静默 JDK 21 稳态 ~**1.13×** | + +**计数(当前默认值):** 活跃规则 **29**(原为 31;移除了 jp-legacy-synchronized-type / jp-many-boolean-fields):8 个 **错误**,8 个 **提示**,13 个 **关闭**。表格仍列出墓碑规则和未发布规则。默认严重性遵循 `Rule.java` / G3 反向优化。 + +## 再生 + +```sh +cd packages/jvmperf +# Lint/tests: JAVA_HOME → JDK 21. Optional benches may use 25: +JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 bash scripts/run_jmh.sh J32_RuleEvidenceBatch +JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 bash scripts/run_kotlin_alloc.sh K15 +# then refresh evidence/*.jsonl from bench-results if numbers move +``` + +在 JDK 主要升级后重新运行。永远不要发明比例。 + +另请参阅共享的 [rule-regression.md](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/jvmperf/docs/zh-CN/performance-guide.md b/packages/jvmperf/docs/zh-CN/performance-guide.md index 6a0a8ca..90bae46 100644 --- a/packages/jvmperf/docs/zh-CN/performance-guide.md +++ b/packages/jvmperf/docs/zh-CN/performance-guide.md @@ -4,9 +4,9 @@ > 写作日期:2026-10-21。 > > 约定: -> - 文中所有代码都来自 `examples/` 目录下的独立文件,**全部在本机编译并运行通过**(Java:`javac --release 25` + 运行自检;Kotlin:`kotlinc 2.4.21 -jvm-target 25` + 运行自检;Android:对 `android-34/android.jar` 编译)。编译日志见 `research/*-check.log`。本文提到的 `research/` 下的 `*.txt`、`*.log`、`bench-results/`、`benches/` 是本地运行输出,不入库。 -> - 文中出现的字节数,凡标注 **【本机实测】** 的,都是用 JOL 0.17 在本机(x86_64 Linux,8 核,JDK 25.0.4.1,默认 G1、压缩指针开启)测得,原始输出见 `research/jol-jdk25.txt`、`research/jol-kotlin-jdk25.txt`、`research/ea-probe.txt`、`research/jvm-flags-jdk25.txt`。其他环境(不同 JDK、关闭压缩指针、ART)数字会不同。 -> - 本文**不给出自己编造的加速比**。需要性能数字时请用第 14 章的 JMH 模板在你的机器上测。 +> - 文中所有代码都来自 `examples/` 目录下的独立文件,全部在本机编译并运行通过(Java:`javac --release 25` + 运行自检;Kotlin:`kotlinc 2.4.21 -jvm-target 25` + 运行自检;Android:对 `android-34/android.jar` 编译)。编译日志见 `research/*-check.log`。本文提到的 `research/` 下的 `*.txt`、`*.log`、`bench-results/`、`benches/` 是本地运行输出,不入库。 +> - 文中出现的字节数,凡标注 【本机实测】 的,都是用 JOL 0.17 在本机(x86_64 Linux,8 核,JDK 25.0.4.1,默认 G1、压缩指针开启)测得,原始输出见 `research/jol-jdk25.txt`、`research/jol-kotlin-jdk25.txt`、`research/ea-probe.txt`、`research/jvm-flags-jdk25.txt`。其他环境(不同 JDK、关闭压缩指针、ART)数字会不同。 +> - 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。本文不编造加速比;请用第 14 章的 JMH 模板在你的机器上测。 > - 每节末尾给出来源链接;每个技巧都写了"什么时候不要用"。 ## 目录 @@ -40,9 +40,9 @@ | JDK 22 | FFM API(`MemorySegment`/`Arena`)正式(JEP 454) | | JDK 23 | 分代 ZGC 成为 ZGC 默认模式(JEP 474) | | JDK 24 | 紧凑对象头实验版(JEP 450);非分代 ZGC 移除(JEP 490);`synchronized` 不再钉住虚拟线程(JEP 491);AOT 类加载缓存(JEP 483) | -| **JDK 25(LTS)** | 紧凑对象头成为正式产品特性但**默认关闭**(JEP 519,`-XX:+UseCompactObjectHeaders`);分代 Shenandoah 正式(JEP 521);Vector API 第 10 轮孵化(JEP 508);Stable Values 预览(JEP 502);AOT 命令行简化与方法 profile(JEP 514/515);JFR CPU 时间采样(实验,JEP 509) | +| JDK 25(LTS) | 紧凑对象头成为正式产品特性但默认关闭(JEP 519,`-XX:+UseCompactObjectHeaders`);分代 Shenandoah 正式(JEP 521);Vector API 第 10 轮孵化(JEP 508);Stable Values 预览(JEP 502);AOT 命令行简化与方法 profile(JEP 514/515);JFR CPU 时间采样(实验,JEP 509) | | JDK 26(2026-03) | G1 减少同步提升吞吐(JEP 522);任意 GC 都可用 AOT 对象缓存(JEP 516);Stable Values 改名 Lazy Constants(JEP 526,预览);Vector API 第 11 轮孵化 | -| JDK 27(2026-09,非 LTS) | **紧凑对象头默认开启**(JEP 534);G1 在所有环境下都是默认 GC(JEP 523);Vector API 第 12 轮孵化(JEP 537,无 API 变化) | +| JDK 27(2026-09,非 LTS) | 紧凑对象头默认开启(JEP 534);G1 在所有环境下都是默认 GC(JEP 523);Vector API 第 12 轮孵化(JEP 537,无 API 变化) | | JDK 28(计划 2027-03) | Valhalla 的值类(JEP 401)以预览形式进入主线(据 InfoQ 报道),之后 Vector API 才会从孵化转为预览 | | Kotlin 2.0 | K2 编译器;lambda 默认用 `invokedynamic` 生成(`-Xlambdas=indy`) | | Kotlin 2.4(2026-06,当前 2.4.21) | 上下文参数等稳定;支持 Java 26 目标;不再能用 `-language-version 1.9` 回退到 K1 | @@ -54,19 +54,18 @@ ## 1. 方法论:先估算、再测量 -1. **先估算**:JVM 上"一个东西"的内存 ≈ 对象头 + 字段 + 对齐填充,再加上它引用的所有对象。第 2 章给出公式和本机实测值,写代码前就能算出 100 万个对象大约要多少内存。 -2. **再测量**: +1. 先估算:JVM 上"一个东西"的内存 ≈ 对象头 + 字段 + 对齐填充,再加上它引用的所有对象。第 2 章给出公式和本机实测值,写代码前就能算出 100 万个对象大约要多少内存。 +2. 再测量: - 对象大小 → JOL(`ClassLayout` / `GraphLayout`); - 分配速率 → JMH `-prof gc` 的 `gc.alloc.rate.norm`(每次操作分配多少字节),或 `ThreadMXBean.getThreadAllocatedBytes`; - CPU 热点 → async-profiler / JFR; - 吞吐与延迟 → JMH(不要用 `System.nanoTime()` 包一个循环,JIT 预热、死代码消除、常量折叠都会让结果失真)。 -3. **只优化热点**。对一个只会创建几十个实例的配置类做位压缩,是浪费时间;对一个装了 1000 万元素的 `List` 改成 `int[]`,是数量级的收益(见第 3 章实测)。 -4. **改完再测一次**,保留测试和断言。 +3. 只优化热点。对一个只会创建几十个实例的配置类做位压缩,是浪费时间;对一个装了 1000 万元素的 `List` 改成 `int[]`,是数量级的收益(见第 3 章实测)。 +4. 改完再测一次,保留测试和断言。 工具用法见第 14 章。 --- - ## 2. 对象有多大:对象头、压缩指针、紧凑对象头 ### 2.1 对象头 @@ -271,7 +270,7 @@ public class J01_Boxing { ``` 要点: -- `Integer.valueOf` 只缓存 −128..127(`-XX:AutoBoxCacheMax` 可以调大上限,本机默认值 128)。**永远不要用 `==` 比较包装类型**。 +- `Integer.valueOf` 只缓存 −128..127(`-XX:AutoBoxCacheMax` 可以调大上限,本机默认值 128)。永远不要用 `==` 比较包装类型。 - 流:用 `mapToInt(...).sum()`,不要 `map(...).reduce(0, Integer::sum)`(每一步都装箱)。 - C2 有 `EliminateAutoBox`(本机默认开启),能消除部分局部的装箱/拆箱,但对放进集合里的对象无能为力。 @@ -322,7 +321,6 @@ public class J02_PrimitiveCollections { 来源:[Integer.valueOf](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#valueOf(int)) · [fastutil](https://fastutil.di.unimi.it/) · [Eclipse Collections](https://eclipse.dev/collections/) · [Koloboke 提交历史](https://github.com/leventov/Koloboke/commits/master/) --- - ## 4. 案例:一辆混动车在 JVM 上占多少内存 抖音上流行过一个 C/C++ 讲解:描述一辆车"有没有电动机、有没有汽油机",用两个 `int` 要 8 字节,换成一个 `int` 位标志要 4 字节,再换成 `uint8_t` 只要 1 字节。把同样的思路搬到 JVM,结论会很不一样——因为**单个对象的大小由对象头和 8 字节对齐决定**。 @@ -567,7 +565,7 @@ public class J13_Presize { ### 5.3 枚举键:EnumMap / EnumSet -`EnumMap` 内部是按 `ordinal()` 索引的数组,`EnumSet`(≤64 个常量)内部是一个 `long` 位向量。**【本机实测】** 3 个枚举常量时,`EnumMap` 自身 + 值数组共 72 字节(紧凑对象头 64 字节),没有 `HashMap.Node`。 +`EnumMap` 内部是按 `ordinal()` 索引的数组,`EnumSet`(≤64 个常量)内部是一个 `long` 位向量。【本机实测】 3 个枚举常量时,`EnumMap` 自身 + 值数组共 72 字节(紧凑对象头 64 字节),没有 `HashMap.Node`。 **不要用的时候**:需要 `null` 键(`EnumMap`/`EnumSet` 不允许);键类型不止一种枚举。 @@ -608,7 +606,7 @@ public class J18_Records { ### 6.1 Compact Strings 与字符串去重 -- JDK 9 起 `String` 内部是 `byte[]` + `coder`(JEP 254):纯 Latin-1 内容每字符 1 字节,否则 UTF-16 每字符 2 字节。**【本机实测】** 20 个 ASCII 字符的 `String`(含 `byte[]`)64 字节,20 个汉字 80 字节。`-XX:-CompactStrings` 可关闭(本机默认开启),几乎没有理由关。 +- JDK 9 起 `String` 内部是 `byte[]` + `coder`(JEP 254):纯 Latin-1 内容每字符 1 字节,否则 UTF-16 每字符 2 字节。【本机实测】 20 个 ASCII 字符的 `String`(含 `byte[]`)64 字节,20 个汉字 80 字节。`-XX:-CompactStrings` 可关闭(本机默认开启),几乎没有理由关。 - `-XX:+UseStringDeduplication`:GC 在后台把**内容相同的 `String` 的底层数组**合并(`String` 对象本身仍各自存在)。最初只有 G1(JEP 192),JDK 18 起 Serial、Parallel、ZGC 也加入了支持(JDK-8272609 / 8267185 / 8267186)。本机默认关闭。适合"大量重复字符串长期存活"的服务(例如反序列化出来的大量相同字段值)。 - `String.intern()`:返回字符串常量池里的规范实例,之后可以用 `==` 比较、只保留一份对象。只适合**取值少、重复多、长期存活**的场景;对高基数的值调用 `intern()` 会把常量池(本质是一个 native 哈希表)撑大。很多时候用自己的 `HashMap` 去重更可控。 @@ -729,7 +727,6 @@ public class J15_Regex { 来源:[JEP 254 Compact Strings](https://openjdk.org/jeps/254) · [JEP 192 String Deduplication in G1](https://openjdk.org/jeps/192) · [JDK 18 发布说明(String Deduplication 扩展到其他 GC)](https://www.oracle.com/java/technologies/javase/18-relnote-issues.html) · [JEP 280 Indify String Concatenation](https://openjdk.org/jeps/280) · [String.split](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/String.html#split(java.lang.String)) · [Kotlin -Xstring-concat](https://kotlinlang.org/docs/compiler-reference.html#xstring-concat) --- - ## 7. Kotlin 专项 Kotlin/JVM 最终也是 JVM 字节码,前面各章的结论都适用。本章讲 Kotlin 语法糖背后的分配与装箱。 @@ -1055,7 +1052,7 @@ fun main() { ### 7.9 协程 -每个 `suspend` 函数编译成状态机,真正挂起时需要续体(continuation)对象;每个 `launch`/`async` 也是对象。CPU 密集任务要分块,**不要每个元素开一个协程**。 +每个 `suspend` 函数编译成状态机,真正挂起时需要续体(continuation)对象;每个 `launch`/`async` 也是对象。CPU 密集任务要分块,不要每个元素开一个协程。 `examples/kotlin/K10_Coroutines.kt` @@ -1130,7 +1127,6 @@ fun main() { 来源:[Kotlin inline value classes](https://kotlinlang.org/docs/inline-classes.html) · [Inline functions](https://kotlinlang.org/docs/inline-functions.html) · [Kotlin 2.0:invokedynamic 生成 lambda](https://kotlinlang.org/docs/whatsnew20.html) · [Sequences](https://kotlinlang.org/docs/sequences.html) · [lazy](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/lazy.html) · [Java 互操作:@JvmField / const](https://kotlinlang.org/docs/java-to-kotlin-interop.html) · [Coroutines 基础](https://kotlinlang.org/docs/coroutines-basics.html) · [androidx.collection 版本说明](https://developer.android.com/jetpack/androidx/releases/collection) --- - ## 8. 堆外内存与 I/O ### 8.1 FFM API:`MemorySegment` + `Arena` @@ -1195,7 +1191,7 @@ public class J06_OffHeapFfm { ### 8.2 direct `ByteBuffer` -老 API,容量受 `-XX:MaxDirectMemorySize` 限制(默认 0 表示与最大堆相同),由 `Cleaner` 随 GC 间接释放——分配/释放都比堆内贵,**适合长期复用**(见第 10 章对象池),不适合每个请求新建。单个 `ByteBuffer` 最大 2 GB(`int` 索引),`MemorySegment` 没有此限制。 +老 API,容量受 `-XX:MaxDirectMemorySize` 限制(默认 0 表示与最大堆相同),由 `Cleaner` 随 GC 间接释放——分配/释放都比堆内贵,适合长期复用(见第 10 章对象池),不适合每个请求新建。单个 `ByteBuffer` 最大 2 GB(`int` 索引),`MemorySegment` 没有此限制。 ### 8.3 I/O @@ -1248,9 +1244,9 @@ public class J12_Io { ### 9.1 逃逸分析与标量替换 -C2 会分析对象是否"逃出"方法(存进字段/数组、作为返回值、传给未内联的方法)。不逃逸的对象可以被**标量替换**——拆成几个局部变量,根本不在堆上分配。 +C2 会分析对象是否"逃出"方法(存进字段/数组、作为返回值、传给未内联的方法)。不逃逸的对象可以被标量替换——拆成几个局部变量,根本不在堆上分配。 -**【本机实测】**(`research/EaProbe.java`,`ThreadMXBean.getThreadAllocatedBytes` 统计 1000 次循环中每次 `new Vec(...).plus(new Vec(1,1))` 的分配量): +【本机实测】(`research/EaProbe.java`,`ThreadMXBean.getThreadAllocatedBytes` 统计 1000 次循环中每次 `new Vec(...).plus(new Vec(1,1))` 的分配量): | 运行参数 | 每次调用分配 | |---|---| @@ -1288,7 +1284,7 @@ public class J08_EscapeAnalysis { } ``` -写出 EA 友好代码的要点:小方法(能被内联)、对象不存入字段/集合、不跨越 `synchronized` 以外的复杂控制流合并点。小数组同样可以被标量替换,但有上限(本机 `EliminateAllocationArraySizeLimit = 64` 个元素)。**不要依赖它**:EA 只在 C2 编译后生效,解释执行与 C1 阶段照样分配;Android ART 的 AOT/JIT 编译器做的优化不同。 +写出 EA 友好代码的要点:小方法(能被内联)、对象不存入字段/集合、不跨越 `synchronized` 以外的复杂控制流合并点。小数组同样可以被标量替换,但有上限(本机 `EliminateAllocationArraySizeLimit = 64` 个元素)。不要依赖它:EA 只在 C2 编译后生效,解释执行与 C1 阶段照样分配;Android ART 的 AOT/JIT 编译器做的优化不同。 ### 9.2 内联与调用点形态 @@ -1332,7 +1328,7 @@ public class J09_JitFriendly { ### 9.3 intrinsic -`Integer.bitCount`、`Long.numberOfTrailingZeros`、`Math.fma`、`System.arraycopy`、`Arrays.equals/mismatch`、`String.indexOf`、CRC32 等是 HotSpot intrinsic,编译成专门的指令序列。**优先用 JDK 方法,不要手写位技巧去"优化"它们**。 +`Integer.bitCount`、`Long.numberOfTrailingZeros`、`Math.fma`、`System.arraycopy`、`Arrays.equals/mismatch`、`String.indexOf`、CRC32 等是 HotSpot intrinsic,编译成专门的指令序列。优先用 JDK 方法,不要手写位技巧去“优化”它们。 ### 9.4 Vector API(仍在孵化) @@ -1460,7 +1456,7 @@ public class J10_FalseSharing { JDK 21 正式(JEP 444)。适合"大量任务、大部分时间在阻塞 I/O"的服务。要点: -- 每个任务一个虚拟线程,**不要池化虚拟线程**;用 `Semaphore` 限制并发访问下游资源。 +- 每个任务一个虚拟线程,不要池化虚拟线程;用 `Semaphore` 限制并发访问下游资源。 - JDK 24 起 `synchronized` 不再把虚拟线程钉在载体线程上(JEP 491);调用本地方法/外部函数时仍会钉住。 - CPU 密集任务没有收益,用平台线程池 / `ForkJoinPool`。 - `ThreadLocal` 在百万虚拟线程下会放大内存,缓存大对象(如 `SimpleDateFormat`)的 `ThreadLocal` 写法要改掉;共享数据考虑 Scoped Values(JDK 25 正式,JEP 506)。 @@ -1490,7 +1486,7 @@ public class J11_VirtualThreads { ### 10.3 对象池:何时用、何时不用 -TLAB(线程本地分配缓冲)里的分配只是指针碰撞,年轻代回收短命对象几乎免费。池化小对象通常**更慢**:对象会被提升到老年代、需要清零/重置、需要并发控制、容易泄漏。只池化真正昂贵的东西:direct `ByteBuffer`、大数组、数据库连接、线程、`MessageDigest`/`Cipher` 之类初始化昂贵且非线程安全的对象。 +TLAB(线程本地分配缓冲)里的分配只是指针碰撞,年轻代回收短命对象几乎免费。池化小对象通常更慢:对象会被提升到老年代、需要清零/重置、需要并发控制、容易泄漏。只池化真正昂贵的东西:direct `ByteBuffer`、大数组、数据库连接、线程、`MessageDigest`/`Cipher` 之类初始化昂贵且非线程安全的对象。 `examples/java/J16_Pool.java` @@ -1571,12 +1567,11 @@ java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar 来源:[HotSpot GC 调优指南(JDK 25)](https://docs.oracle.com/en/java/javase/25/gctuning/) · [JEP 474](https://openjdk.org/jeps/474) · [JEP 490](https://openjdk.org/jeps/490) · [JEP 521](https://openjdk.org/jeps/521) · [JEP 522](https://openjdk.org/jeps/522) · [JEP 523](https://openjdk.org/jeps/523) · [JEP 318 Epsilon](https://openjdk.org/jeps/318) · [JEP 483](https://openjdk.org/jeps/483) · [JEP 514](https://openjdk.org/jeps/514) · [JEP 515](https://openjdk.org/jeps/515) · [JEP 516](https://openjdk.org/jeps/516) --- - ## 12. Android(ART)专项 ### 12.1 ART 与 HotSpot 的差别 -- 没有 C2,也没有 HotSpot 的逃逸分析/标量替换能力;ART 是"解释器 + JIT + 基于 profile 的 AOT(dex2oat)"混合模式。**在 Android 上"让 JIT 帮你消除分配"的假设不成立**,热路径(`onDraw`、滚动回调、动画帧)里的分配要真正避免。 +- 没有 C2,也没有 HotSpot 的逃逸分析/标量替换能力;ART 是"解释器 + JIT + 基于 profile 的 AOT(dex2oat)"混合模式。在 Android 上“让 JIT 帮你消除分配”的假设不成立,热路径(`onDraw`、滚动回调、动画帧)里的分配要真正避免。 - 引用本来就是 32 位;对象头 8 字节(`mirror::Object` 只有 class 指针 + monitor 两个 32 位字段),没有压缩指针或紧凑对象头开关。 - GC:Android 14 起(通过 ART mainline 模块更新)默认是并发标记整理(CMC,基于 userfaultfd);Android 16 QPR2 起引入分代 CMC,优先回收新分配的对象。 @@ -1749,10 +1744,9 @@ public class J19_JmhBench { 来源:[JMH samples](https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples) · [JEP 328 Flight Recorder](https://openjdk.org/jeps/328) · [JEP 509 JFR CPU-Time Profiling](https://openjdk.org/jeps/509) · [async-profiler](https://github.com/async-profiler/async-profiler) · [JOL](https://github.com/openjdk/jol) · [Jetpack Benchmark](https://developer.android.com/topic/performance/benchmarking/benchmarking-overview) --- - ## 15. jvmperf-lint:自动找出反模式 -`jvmperf-lint/` 是本指南配套的静态检查器:Java + tree-sitter(JVM 绑定 `io.github.bonede:tree-sitter`,以及 `tree-sitter-java` / `tree-sitter-kotlin`),同时支持 `.java`、`.kt`、`.kts`。它只看语法树,**不做类型解析**,所以快、零配置、不需要编译项目,但会有误报/漏报(每条规则都写明了误报风险)。完整说明见 [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md)。 +`jvmperf-lint/` 是本指南配套的静态检查器:Java + tree-sitter(JVM 绑定 `io.github.bonede:tree-sitter`,以及 `tree-sitter-java` / `tree-sitter-kotlin`),同时支持 `.java`、`.kt`、`.kts`。它只看语法树,不做类型解析,所以快、零配置、不需要编译项目,但会有误报/漏报(每条规则都写明了误报风险)。完整说明见 [`jvmperf-lint/README.md`](../../jvmperf-lint/README.md)。 ```sh cd jvmperf-lint @@ -1910,7 +1904,6 @@ good 夹具上 detekt 只报了一处 `EmptyFunctionBlock`(`close() {}`)。 交叉验证:`scripts/run-static-vs-hotspot.sh` 的输出写在 `research/benches/`。**静态 lint 不等于 HotSpot 的判断**:不能只看源码就断言 C2 的编译层级或逃逸分析失败,也不要用 PrintCompilation 的行数当作“一致/不一致”的判据。 --- - ## 16. 速查表 | 想做的事 | 做法 | 不要做的时候 | @@ -1939,7 +1932,6 @@ good 夹具上 detekt 只报了一处 `EmptyFunctionBlock`(`close() {}`)。 | 自动检查 | jvmperf-lint + PMD/Error Prone + detekt + Android Lint | 静态提示都要用 profiler 确认 | --- - ## 参考资料 **OpenJDK / Java** diff --git a/packages/jvmperf/docs/zh-CN/rule-ids.md b/packages/jvmperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..2df2134 --- /dev/null +++ b/packages/jvmperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,57 @@ +# jvmperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `jp-boxed-primitive-collection` | +| `jp-string-concat-hot` | +| `jp-regex-hot` | +| `jp-format-hot` | +| `jp-unpresized-collection` | +| `jp-boxed-stream` | +| `jp-exception-control-flow` | +| `jp-enum-key-hash-collection` | +| `jp-collection-chain` | +| `jp-value-class-boxing` | +| `jp-lambda-to-non-inline-hot` | +| `jp-lazy-default-mode` | +| `jp-data-copy-hot` | +| `jp-android-sparse-array` | +| `jp-log-hot` | +| `jp-sleep-polling` | +| `jp-finalize` | +| `jp-expensive-alloc-hot` | +| `jp-hot-heavy-alloc` | +| `jp-hot-format-log` | +| `jp-hot-system-call` | +| `jp-bitmap-decode-no-reuse` | +| `jp-hot-post-lambda` | +| `jp-ui-text-append` | +| `jp-baos-tobytearray-copy` | +| `jp-hot-payload-copy` | +| `jp-client-factory-recreate` | +| `jp-string-intern-hot` | +| `jp-non-static-field-updater` | + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `jp-data-copy-in-loop` | `jp-data-copy-hot` | +| `jp-expensive-alloc-in-loop` | `jp-expensive-alloc-hot` | +| `jp-format-in-loop` | `jp-format-hot` | +| `jp-lambda-to-non-inline-in-loop` | `jp-lambda-to-non-inline-hot` | +| `jp-log-in-loop` | `jp-log-hot` | +| `jp-regex-in-loop` | `jp-regex-hot` | +| `jp-string-concat-in-loop` | `jp-string-concat-hot` | +| `jp-string-intern-in-loop` | `jp-string-intern-hot` | + +## Removed (tombstone; never reuse) + +- `jp-legacy-synchronized-type` — see `evidence/_removed/` +- `jp-many-boolean-fields` — see `evidence/_removed/` diff --git a/packages/jvmperf/evidence/zh-CN/README.md b/packages/jvmperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..2eeb2a3 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/README.md @@ -0,0 +1,37 @@ +# jvmperf 证据 + +每条规则的门对(`.md` + `.jsonl`)。政策:[../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。政策列是每条规则在 `Rule.java` 中的默认严重性。 + +| 规则 | 文件 | 政策 | +|------|-------|--------| +| jp-boxed-primitive-collection | 是 | 关闭 | +| jp-string-concat-hot | 是 | **错误** | +| jp-regex-hot | 是 | 关闭 | +| jp-format-hot | 是 | 关闭 | +| jp-unpresized-collection | 是 | 关闭 | +| jp-boxed-stream | 是 | **错误** | +| jp-异常控制流 | 是 | 关 | +| jp-legacy-synchronized-type | 已移除 | 见 `_removed/` | +| jp-many-boolean-fields | 已移除 | 请参见 `_removed/` | +| jp-enum-key-hash-collection | 是 | **错误** | +| jp-collection-chain | 是 | 关闭 | +| jp-value-class-boxing | 是 | 提示 | +| jp-lambda-to-non-inline-hot | 是 | 关闭 | +| jp-lazy-default-mode | md 存根 | 关闭(待定) | +| jp-data-copy-hot | 是 | **错误** | +| jp-android-sparse-array | md 存根 | 提示(待定) | +| jp-log-hot | 是 | 关闭 | +| jp-sleep-polling | md 孔 | 关闭(待定) | +| jp-finalize | md stub | 提示(待定) | +| jp-expensive-alloc-hot | 是 | 关闭(否定) | +| jp-hot-heavy-alloc–jp-ui-text-append | md 存根 | 热重分配和热格式日志关闭,其他四个提示(等待 ART) | +| jp-baos-tobytearray-copy | 是 | **错误** | +| jp-hot-payload-copy | 是 | **错误** | +| vt-unsafe-threadlocal-pool | [md stub](vt-unsafe-threadlocal-pool.md) | 未发布 / 指南优先 | +| jp-client-factory-recreate | 是 | **错误** | +| jp-string-intern-hot | 是 | **错误** | +| jp-non-static-field-updater | 是 | 提示 | + +原始转储:`research/bench-results/`(本地输出,未提交)。 + +关联:`research/benches/`(由 `scripts/run-static-vs-hotspot.sh` 编写)。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-android-sparse-array.md b/packages/jvmperf/evidence/zh-CN/jp-android-sparse-array.md new file mode 100644 index 0000000..472e316 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-android-sparse-array.md @@ -0,0 +1,13 @@ +# jp-android-sparse-array — 待测量 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-android-sparse-array.jsonl`。 + +## 原因 + +Android `SparseArray` 对比 `HashMap` — 需要 ART/设备;JVM 环境不能公平地衡量 Android 集合。 + +## 政策 + +保持 **提示** / 评论。在没有在正确的运行时提供诚实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-baos-tobytearray-copy.md b/packages/jvmperf/evidence/zh-CN/jp-baos-tobytearray-copy.md new file mode 100644 index 0000000..1ec2e27 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-baos-tobytearray-copy.md @@ -0,0 +1,29 @@ +# jp-baos-tobytearray-copy + +## 主张 + +`ByteArrayOutputStream.toByteArray()` 份;在重复使用的 BAOS 上优先使用 `writeTo(out)`。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `toByteArray_4k` +- **之后:** `writeTo_reuse` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| 之前 | **13982.720** ns_per_op, **8224.210** B/op | +| 之后 | **140.556** ns/次操作, **0.002** B/次操作 | +| **前/后比例** | **~99.5×** | + +## Lint 映射 / 策略 + +策略 **错误** (~99×)。 + +## 限制 + +在这台机器上进行微基准测试;参见 `scripts/run-static-vs-hotspot.sh`。不要随意制定比率。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-bitmap-decode-no-reuse.md b/packages/jvmperf/evidence/zh-CN/jp-bitmap-decode-no-reuse.md new file mode 100644 index 0000000..dab2eac --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-bitmap-decode-no-reuse.md @@ -0,0 +1,13 @@ +# jp-bitmap-decode-no-reuse — 待定措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-bitmap-decode-no-reuse.jsonl`。 + +## 原因 + +位图解码不重复使用——需要 ART/设备 + BitmapFactory。 + +## 政策 + +停留 **提示** / 评论。在没有在正确运行时提供诚实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-boxed-primitive-collection.md b/packages/jvmperf/evidence/zh-CN/jp-boxed-primitive-collection.md new file mode 100644 index 0000000..8fe12b5 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-boxed-primitive-collection.md @@ -0,0 +1,39 @@ +# jp-boxed-primitive-collection — 装箱原始类型集合 + +## 主张 + +在 HotSpot 上,对 `List` 或包装集合进行的热求和(或类似扫描)比对 `int[]`(或未装箱的流)进行相同操作要慢,主要是由于 **装箱、指针追踪和垃圾回收压力**——而不是因为静态分析检测到了 C2 失败。 + +## 如何测量 + +- 基准测试工具:通过 `scripts/run_jmh.sh J20_BoxingSum` 运行 `jmh-benches/.../J20_BoxingSum.java` +- 工作内容:处理 N=1_000_000 个元素;使用 JMH 1.37 的 avgt + `-prof gc`;`@Fork` 会附加 `-XX:+UseCompactObjectHeaders` +- **之前:** 对 `ArrayList` 使用 `sumArrayList` +- **之后:** 对 `int[]` 使用 `sumIntArray`(也记录了:`sumIntStream`) +- 原始研究数据行:`research/bench-results/J20_BoxingSum.jsonl` +- 对应关联:`scripts/run-static-vs-hotspot.sh` 中的 `1_boxing_sum` 案例 + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders`(JMH `@Fork` jvmArgsAppend) | +| before `sumArrayList[n=1000000]` | **478817.125 ns/op** | +| after `sumIntArray[n=1000000]` | **363120.071 ns/op** | +| **前/后比率** | **约1.32×** 在 n=1e6 时 | +| n=100000(相同的 JSONL) | 列表 **205276.327** 对比 `int[]` **27867.230** → **~7.37×** | +| vs `sumIntStream[n=1000000]` | 流 **618599.233 ns/op**,比列表慢 | + +在空闲主机上(Temurin 21.0.13,负载 0.16,热度 5,中位数 9)稍后进行的方向检查不能替代此 JSONL。数值仍从 1000 开始。n=1e5,40 次循环:list 2.014 毫秒,`int[]` 0.238 毫秒,约 8.5 倍。n=1e6,8 次循环:list 6.504 毫秒,`int[]` 0.589 毫秒,约 11 倍。样本非常集中。JDK 27 n=1e6 ~1.32 倍在这里无法复现。默认保持 **关闭**。 + +## 与 lint 的对应 + +`jvmperf-lint` jp-boxed-primitive-collection(`boxed-primitive-collection`)标记 `List`、`Map`、Kotlin `List` 等。默认严重性 **关闭**(使用 `--enable` 启用);参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。 + +## 限制 + +- 微基准测试,而不是应用程序配置文件;必须暴露装箱类型的 API 边界通常是虚假的“胜利”。 +- 不要将 jp-boxed-primitive-collection 与 HotSpot 的去优化或“JIT 失败”等同。 +- 语料库内部测试在测试/固定装置上仍然有浮点数——需要单独分类处理。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-boxed-stream.md b/packages/jvmperf/evidence/zh-CN/jp-boxed-stream.md new file mode 100644 index 0000000..9ae073f --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-boxed-stream.md @@ -0,0 +1,29 @@ +# jp-盒装流 + +## 主张 + +在 HotSpot 上,Boxed `Stream` reduce(`Integer::sum` / 类似)比 `mapToInt`/`mapToLong` 慢得多且分配更多内存。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `boxedReduce_n10k` +- **之后:** `mapToInt_n10k` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| 之前 | **745977.453** ns_per_op, **160178.813** B/op | +| 之后 | **20050.141** 每操作纳秒, **207.420** 每操作字节 | +| **前/后比例** | **~37.2×** | + +## Lint 映射 / 策略 + +策略**错误**(墙钟时间约37×)。 + +## 限制 + +在这台机器上进行微基准测试;参见 `scripts/run-static-vs-hotspot.sh`。不要编造比例。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-client-factory-recreate.md b/packages/jvmperf/evidence/zh-CN/jp-client-factory-recreate.md new file mode 100644 index 0000000..2305f74 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-client-factory-recreate.md @@ -0,0 +1,30 @@ +# jp-客户端-工厂-重建 + +## 主张 + +重新创建一个 HTTP 客户端工厂(`HttpClient.newHttpClient()`,类似于 OkHttpClient 等)相比重复使用代价非常高。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- 循环前调用了 `close()`,所以每个客户端都会释放其选择器。否则,JDK 27 会在迭代完成前达到进程文件描述符限制。 +- **之前:** `HttpClient_newEach` +- **之后:** `HttpClient_reuse` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| before | **81763.736** ns_per_op, **32558.095** B/op | +| after | **0.416** ns_per_op, **0.000** B/op | +| **前后比例** | **~1.97e5×** | + +## Lint 映射 / 策略 + +策略 **错误** (~1.97e5× 对比重用,包括 `close()`;创建时约 32.6 KB/操作)。测试固定装置名称 OkHttp;使用 JDK HttpClient 作为真实的昂贵工厂进行测量。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-collection-chain.md b/packages/jvmperf/evidence/zh-CN/jp-collection-chain.md new file mode 100644 index 0000000..fd83e93 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-collection-chain.md @@ -0,0 +1,30 @@ +# jp-collection-chain + +## 主张 + +Kotlin 迫切的 `map`/`filter` 链会分配完整的中间列表;当只使用前缀时,`asSequence()`(或命令式)可以避免这种情况。 + +## 如何测量 + +- 来源:`K13_SequenceAlloc.jsonl` +- JDK **27 (27+35)**,Kotlin **2.4.21**,JVM 参数 `-XX:+UseCompactObjectHeaders`。Linux x64,启动时负载 0.12。JDK 25 配对约为 391×。 +- **之前:** `eagerChain_n1e5_take5` +- **之后:** `sequenceChain_n1e5_take5` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| When | 2026-10-23 | +| before | **839101.9** ns_per_op, **2568512.0** B/op | +| after | **3170.2** ns_per_op, **232.0** B/op | +| **前/后比例** | **~264.7×** | +| 命令式 | **1178.0** 纳秒每操作, **80.0** 字节每操作, 大约 **712×** 对比 eager | + +## Lint 映射 / 策略 + +相对于序列测得约265×;相对于命令式测得约712×。分配量保持不变,大约为2.6 MB,而不是232 B。在 `Rule.java` 中默认严重性为**关闭**(使用 `--enable` 运行它)。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-data-copy-hot.md b/packages/jvmperf/evidence/zh-CN/jp-data-copy-hot.md new file mode 100644 index 0000000..c428f76 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-data-copy-hot.md @@ -0,0 +1,29 @@ +# jp-data-copy-hot + +## 主张 + +在循环中热使用 `data class.copy(...)` 会在每次迭代时分配一个新实例;局部变量可以避免这种情况。 + +## 如何测量 + +- 来源:`K14_LambdaCopyAlloc.jsonl` +- JDK **27 (27+35)**,Kotlin **2.4.21**,JVM 参数 `-XX:+UseCompactObjectHeaders`。Linux x64,负载 0.21。JDK 25 时间对大约是 1149×。 +- **之前:** `simulateCopy_n1e4` +- **之后:** `simulateLocals_n1e4` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| When | 2026-10-23 | +| 之前 | **20884.1** ns_per_op, **240024.0** B/op | +| 之后 | **100.6** ns/次操作, **24.0** B/次操作 | +| **前后比率** | **约207.6×** 时间,**约10001×** 每操作字节 | + +## Lint 映射 / 策略 + +策略**错误**。时间约为208×(原为~1149×)。分配仍约为10001× B/op。 + +## 限制 + +在这台机器上进行微基准测试;参见 `scripts/run-static-vs-hotspot.sh`。不要随意制定比率。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-enum-key-hash-collection.md b/packages/jvmperf/evidence/zh-CN/jp-enum-key-hash-collection.md new file mode 100644 index 0000000..36b4fd9 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-enum-key-hash-collection.md @@ -0,0 +1,29 @@ +# jp-enum-key-hash-collection + +## 主张 + +对于小枚举键集合,使用带枚举键的 `HashMap`/`HashSet` 比 `EnumMap`/`EnumSet` 更慢且分配更多。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `hashMap_enumKey` +- **之后:** `enumMap` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| 之前 | **660.673** ns/次操作, **192.010** B/次操作 | +| 之后 | **56.779** ns/次操作, **0.001** B/次操作 | +| **前/后比例** | **~11.6×** | + +## Lint 映射 / 策略 + +策略 **错误** (~11.6× 时间;HashMap 也 ~192 B/操作 vs ~0)。 + +## 限制 + +在这台机器上进行微基准测试;参见 `scripts/run-static-vs-hotspot.sh`。不要编造比例。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-exception-control-flow.md b/packages/jvmperf/evidence/zh-CN/jp-exception-control-flow.md new file mode 100644 index 0000000..bc43fe4 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-exception-control-flow.md @@ -0,0 +1,31 @@ +# jp-异常控制流 + +## 主张 + +使用异常来处理预期的解析失败比进行数字检查要慢得多(并且会分配异常对象)。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `tryParse_n2000` +- **之后:** `digitCheck_n2000` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| before | **230074.011** ns_per_op, **342403.663** B/op | +| after | **58066.417** ns_per_op, **0.862** B/op | +| **前/后比例** | **~3.96×** | + +## Lint 映射 / 策略 + +在 JDK 27 平均值上测得约 ~3.96×。两个带有基数检查的样本被加入,因此这个平均值比稳定的 ~4 µs 样本更低。 + +在一个空闲主机上(Temurin 21.0.13,负载 0.16)进行的后续方向检查不能替代这个 JSONL。依然是相同的 2000 个字符串,每第五个为 `nope`,重复 80 次,预热 5 次,中位数 9:尝试/解析 14.509 毫秒,数字检查 0.535 毫秒,约 27 倍。双方都很紧。JDK 27 的 ~4 倍平均值是高峰。默认严重性保持 **关闭**。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-expensive-alloc-hot.md b/packages/jvmperf/evidence/zh-CN/jp-expensive-alloc-hot.md new file mode 100644 index 0000000..b9d24dd --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-expensive-alloc-hot.md @@ -0,0 +1,32 @@ +# jp-expensive-alloc-hot — 循环中的高成本分配(作用域 + 负池神话) + +## 申诉(规则意图) + +应将**昂贵可重用**类型(`SimpleDateFormat`、`ObjectMapper`、`MessageDigest`、大型常量缓冲区等)的热构造提升或缓存。这就是 jp-expensive-alloc-hot 所标记的内容。 + +## 邻近测量(仅凭此**不要**升级) + +小型逃逸对象是另一回事:一个天真的线程本地/回收 **小型 `Point` 类对象的池可能在墙钟时间上输给普通 TLAB `new`**,同时在每次操作字节数上获胜。 + +- 测试平台:`J24_PoolVsAlloc.java` / `scripts/run_jmh.sh J24_PoolVsAlloc` +- 对比:`pooled` vs `tlabAlloc`,批量大小 `batch=1` +- 原始数据:`research/bench-results/J24_PoolVsAlloc.jsonl` +- 关联:STATIC-VS-HOTSPOT 案例 5 **部分 / 负面** + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders` | +| `pooled[batch=1]` | **8.250 ns/op**, **0.000 B/op** | +| `tlabAlloc[batch=1]` | **3.826 ns/op**, **16.000 B/op** | +| **池 / TLAB 时间** | **池慢约 2.16 倍** | + +## 与 lint 的对应 + +默认严重性 **关闭**(使用 `--enable` 运行它)。证据文档 **说明我们为何不将** jp-expensive-alloc-hot 从 J24 升级为 `error`:该规则的有价值目标是昂贵的工厂;J24 警告不要在循环中的每个 `new` 操作中都理解为“始终使用池”。 + +## 限制 + +- 没有JMH配对将“新简单数据格式”与“线程本地”/“日期Formatter”相隔开来——这将是正向升级的路径。 +- VT + 脂肪 ' Thread Local ' 池 → 见指南JP029(指南-第一固定装置),而不是jp - 成本-分配热。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-finalize.md b/packages/jvmperf/evidence/zh-CN/jp-finalize.md new file mode 100644 index 0000000..0c55d9c --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-finalize.md @@ -0,0 +1,13 @@ +# jp-finalize — 待处理措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-finalize.jsonl`。 + +## 原因 + +`finalize()` — 正确性 / GC 延迟气味;Shipilev 和 JDK 已弃用它。在进行公平的前/后速度微基准之前(Cleaner 是解决方法)。待作为实时时钟门控的测量。 + +## 政策 + +保持 **提示** / 复查。在没有在正确的运行时提供真实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-format-hot.md b/packages/jvmperf/evidence/zh-CN/jp-format-hot.md new file mode 100644 index 0000000..178c8a1 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-format-hot.md @@ -0,0 +1,35 @@ +# jp-format-hot — `String.format` / 在高频循环中格式化 + +## 主张 + +在 HotSpot 上,热使用 `String.format`(及类似方法)要比使用 `StringBuilder` 追加(或对日期型文本使用可重用的 `DateTimeFormatter`)构建相同的字符串慢得多且分配更多。 + +## 如何测量 + +- Harness:`jmh-benches/./J27 FormatHot.java'通过`scripts/run jmh.sh J27 FormatHot' +- 工作:小模式的代表=1;JMH 1.37 avgt + `-prof gc';`-XX:+ 使用压缩对象头' +- ** 在**`弦乐论坛 ' 之前 +- ** 之后:** `钢铁建造工附录 ' (又记录:`日期Time Formatter ' ) +- Raw:`研究/结果/J27-FormatHot.jsonl' +- 证据:STATIC-VS-HOTSPOT案3 + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders` | +| 在 `stringFormat[reps=1]` 之前 | **1022.449 纳秒/次操作**, **1368.006 字节/次操作** | +| 在 `stringBuilderAppend[reps=1]` 之后 | **35.147 ns/次操作**, **88.000 B/次操作** | +| **前/后比例** | **约29.1×** 时间 | +| vs `dateTimeFormatter[reps=1]` | **141.005 ns/次** → 格式化/DTF ~**7.3×** | + +## 与 lint 的对应 + +`jvmperf-lint` jp-format-hot (`format-in-loop`)。默认严重性 **关闭**(使用 `--enable` 启用)。 + +## 限制 + +- 日志记录 / 罕见错误路径:实际时间通常无关紧要 — 保持抑制。 +- 一次性脚本和异常信息是较弱的升级目标。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-hot-format-log.md b/packages/jvmperf/evidence/zh-CN/jp-hot-format-log.md new file mode 100644 index 0000000..6fb2db1 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-hot-format-log.md @@ -0,0 +1,13 @@ +# jp-hot-format-log — 待处理的措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-hot-format-log.jsonl`。 + +## 原因 + +Android 触摸路径的热路径格式/日志——需要 ART/设备。 + +## 政策 + +默认严重性 **关闭**(使用 `--enable` 运行)。在没有在正确运行时提供真实的前后情况之前,请不要将其升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-hot-heavy-alloc.md b/packages/jvmperf/evidence/zh-CN/jp-hot-heavy-alloc.md new file mode 100644 index 0000000..5f5d1ed --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-hot-heavy-alloc.md @@ -0,0 +1,13 @@ +# jp-hot-heavy-alloc — 待定措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-hot-heavy-alloc.jsonl`。 + +## 原因 + +在 Android `onDraw` / 类似设备上热路径大量分配——需要 ART/设备 + 公平帧测试工具。 + +## 政策 + +默认严重性 **关闭**(使用 `--enable` 运行)。在未在正确运行时提供真实的前后情况之前,请勿将其升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-hot-payload-copy.md b/packages/jvmperf/evidence/zh-CN/jp-hot-payload-copy.md new file mode 100644 index 0000000..faa9231 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-hot-payload-copy.md @@ -0,0 +1,29 @@ +# jp-hot-payload-copy + +## 主张 + +热点路径 `new byte[header+payload]` + 每帧都进行 arraycopy 分配;在帧处理时重用缓冲区。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `newFrameEach_1k` +- **之后:** `reuseBuffer_1k` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| 之前 | **2655.939** ns_per_op, **1040.037** B/op | +| 之后 | **16.820** ns/次操作, **0.000** B/次操作 | +| **前/后比例** | **~158×** | + +## Lint 映射 / 策略 + +策略 **错误** (~158×)。 + +## 限制 + +在这台机器上的微基准;参见 `scripts/run-static-vs-hotspot.sh`。不要编造比率。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-hot-post-lambda.md b/packages/jvmperf/evidence/zh-CN/jp-hot-post-lambda.md new file mode 100644 index 0000000..6a48549 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-hot-post-lambda.md @@ -0,0 +1,13 @@ +# jp-hot-post-lambda — 待定措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-hot-post-lambda.jsonl`。 + +## 原因 + +处理程序 `post { }` lambda 分配 — 需要 Android 运行时。 + +## 政策 + +停留 **提示** / 评论。在没有在正确运行时提供诚实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-hot-system-call.md b/packages/jvmperf/evidence/zh-CN/jp-hot-system-call.md new file mode 100644 index 0000000..eeeb5e1 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-hot-system-call.md @@ -0,0 +1,13 @@ +# jp-hot-system-call — 待处理措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-hot-system-call.jsonl`。 + +## 原因 + +热门系统 / 吐司 / Android 的无障碍功能 — 需要设备。 + +## 政策 + +保持 **提示** / 评论。在没有在正确的运行时提供诚实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-lambda-to-non-inline-hot.md b/packages/jvmperf/evidence/zh-CN/jp-lambda-to-non-inline-hot.md new file mode 100644 index 0000000..099d420 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-lambda-to-non-inline-hot.md @@ -0,0 +1,29 @@ +# jp-lambda到非内联热 + +## 主张 + +传递给**非内联**高阶函数的 Lambda 会进行封装/分配;`inline` 可以将分配保持在接近零的水平。 + +## 如何测量 + +- 来源:`K14_LambdaCopyAlloc.jsonl` +- JDK **27 (27+35)**,Kotlin **2.4.21**,JVM 参数 `-XX:+UseCompactObjectHeaders`。Linux x64,负载 0.21。 +- **之前:** `repeatSumBoxed_n1e4` +- **之后:** `repeatSumInline_n1e4` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| When | 2026-10-23 | +| before | **15439.7** ns_per_op, **3897.9** B/op | +| after | **32990.8** ns_per_op, **16.0** B/op | +| **前/后比率** | **约0.47×** 时间,**约244×** B/操作 | + +## Lint 映射 / 策略 + +分配仍然大约是 244×(之前约为 283×)。这次运行时间方向相反:非内联调用更快,大约为 0.47×。对于分配该规则保持不变。`Rule.java` 中的默认严重性是 **关闭**(使用 `--enable` 运行它)。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-lazy-default-mode.md b/packages/jvmperf/evidence/zh-CN/jp-lazy-default-mode.md new file mode 100644 index 0000000..29b7eef --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-lazy-default-mode.md @@ -0,0 +1,13 @@ +# jp-lazy-default-mode — 待定措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-lazy-default-mode.jsonl`。 + +## 原因 + +Kotlin `by lazy` 默认模式(SYNCHRONIZED)是情境性的——仅在争用或初始化成本被分析时测量。没有能概括的准确微基准;保持挂起状态。 + +## 政策 + +默认严重性 **关闭**(使用 `--enable` 运行)。在没有在正确运行时进行真实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-log-hot.md b/packages/jvmperf/evidence/zh-CN/jp-log-hot.md new file mode 100644 index 0000000..b7b6451 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-log-hot.md @@ -0,0 +1,31 @@ +# jp-log-hot + +## 主张 + +在进行级别检查之前构建日志字符串(`"x=" + x`)即使在调试关闭时也会进行分配。使用 `isDebugEnabled()`(或结构化惰性参数)进行保护。 + +## 如何测量 + +- 来源: `J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `concatAlways_debugOff_n1000` +- **之后:** `guarded_debugOff_n1000` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| before | **2838.115** ns_per_op, **23200.045** B/op | +| after | **6543.092** ns_per_op, **0.096** B/op | +| **前/后比率** | **~0.43×** 时间(平均),分配仍然约 **23200 B** 对比 ~0 | + +## Lint 映射 / 策略 + +JDK 27 平均时间朝相反方向变化(约 0.43×),因为一个受保护的样本飙升到约 25 微秒;其他四个样本维持在约 0.4–3.5 微秒,紧挨着 concat 的约 2.6–3.7 微秒。分配仍然约为每操作 23200 字节,而对比约为 0。 + +在一个空闲的16核主机上(Temurin 21.0.13,负载0.96,warm 5,中位数为9)进行的后期方向检查不能替代这个JSONL。为1000个整数构建`"x=" + x`,关闭调试,每个样本重复200次:拼接中位数为1.129毫秒,保护中位数为0.062毫秒,约18倍。前四个保护样本为0.28–0.45毫秒,其余保持在0.048–0.062毫秒,所以如果保留这些早期样本,保护仍然占优。默认严重性保持**关闭**。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-non-static-field-updater.md b/packages/jvmperf/evidence/zh-CN/jp-non-static-field-updater.md new file mode 100644 index 0000000..0dcb28c --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-non-static-field-updater.md @@ -0,0 +1,29 @@ +# jp-非静态字段更新器 + +## 主张 + +非静态的 `Atomic*FieldUpdater` 字段存在每个实例占用空间的风险,并可能被意外重新创建。**每次创建**一个 updater 比重用静态的慢约 526 倍。JDK 27 的稳态配对(每实例递增 248 ns,静态递增 1567 ns)在空闲主机上无法复现:Temurin 21.0.13,负载 0.96,对 256 个节点进行 4000 次遍历,每实例中位数 4.747 ms 对比静态 4.207 ms,约为 1.13 倍。该检查不能替代创建路径的 JSONL。稳态维持约 1 倍。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `newUpdaterEach` +- **之后:** `staticUpdater` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| before | **187.165** ns_per_op, **96.003** B/op | +| after | **0.356** ns_per_op, **0.000** B/op | +| **前/后比例** | **~526×** | + +## Lint 映射 / 策略 + +政策 **提示** — 只有在更新器被重新创建时才升级;常见的每实例持有一个的模式大多是一种痕迹/风格上的异味,而不是在增量路径上的实际延迟。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-regex-hot.md b/packages/jvmperf/evidence/zh-CN/jp-regex-hot.md new file mode 100644 index 0000000..ffc5422 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-regex-hot.md @@ -0,0 +1,37 @@ +# jp-regex-hot — 循环中编译正则表达式 + +## 主张 + +在每次调用时编译一个 `Pattern` 仍然会比重用 `static final Pattern` 分配大约 **2.4×**。在 JDK 27 上,1024 行时间对比则相反(**~0.76×**,每次编译更快)。行数=64 时,时间约为 **1.07×**。 + +## 如何测量 + +- 利用:`J26 Regexocility.java ' /`scripts/run jmh.sh J26 Regexocility ' +- 工作:将 " 线条 " 字符串与固定的regex匹配;JMH 1.37 avgt + `-prof gc ' ;`XX:+ 使用对象头 ' +- ** 之前:** `编译小组 ' +- ** 之后:**`静态终极 模式` +- Raw:`研究/成果/J26-Regexicul.jsonl' + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders` | +| 在 `compileEachCall[lines=1024]` 之前 | **128040.677 ns/次操作**, **827392.817 B/次操作** | +| 在 `staticFinalPattern[lines=1024]` 之后 | **167595.045 ns/次操作**, **344065.068 B/次操作** | +| **前/后比率** | **~0.76×** 时间(每次编译更快),**~2.40×** B/op | +| 行数=64(相同的 JSONL) | **~1.07×** 时间,**~2.40×** B/操作 | + +## 与 lint 的对应 + +默认严重性 **关闭**(运行时使用 `--enable`);实际运行时间仅略高于约 1.3×,不像 jp-string-concat-hot 那样出现明显的时间急剧上升。证据文件存在,因此该声明可以审计;保持为自愿审查提示,直到更热的工作负载显示出更大的时间陡坡。 + +在一台空闲的16核主机(Temurin 21.0.13,负载 0.96)上进行的后续方向检查不能替代此 JSONL。对1024行拆分进行200次重复,预热5次,中位数为9:每次编译 33.023 毫秒,静态模式 19.738 毫秒,大约1.67×,样本紧密。在该 JDK 上,静态模式略快。JDK 27 的时间对保持记录状态,而大约2.4×的分配差距是规则保持的原因。 + +## 限制 + +- 动态模式(用户提供的正则表达式)不能是静态-最终的 —— 预期有抑制。 +- 当模式已经编译时,匹配成本占主导;一次编译获胜会减少缩减。 +- 不要发明比 J26 记录更大的比例。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-sleep-polling.md b/packages/jvmperf/evidence/zh-CN/jp-sleep-polling.md new file mode 100644 index 0000000..99da94f --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-sleep-polling.md @@ -0,0 +1,13 @@ +# jp-sleep-polling — 待定措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-sleep-polling.jsonl`。 + +## 原因 + +`Thread.sleep` 轮询——挂钟时间主要由休眠本身占据;用 park/condition 替换是正确性/延迟的设计问题,而不是我们可以诚实地作为错误来限制的微小比例。 + +## 政策 + +默认严重性 **关闭**(使用 `--enable` 运行)。在没有在正确运行时进行真实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-string-concat-hot.md b/packages/jvmperf/evidence/zh-CN/jp-string-concat-hot.md new file mode 100644 index 0000000..f84a22d --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-string-concat-hot.md @@ -0,0 +1,36 @@ +# jp-string-concat-hot — 循环中的字符串拼接 (`+=` / `s = s + …`) + +## 主张 + +在循环中使用 `+=`(或 `s = s + x`)进行热 `String` 拼接比在 HotSpot 上使用 `StringBuilder`(特别是预设大小的)要慢得多且分配更多。原因是重复的不变 `String` 分配/复制——而不是 C2 “失败”。 + +## 如何测量 + +- Harness:`jmh-benches/./J23 StringConcat.java ' 通过`scripts/run jmh.sh J23 StringConcat' +- 工作:装配`零件 ' 固定碎片;JMH 1.37 avgt +`-prof gc ' ;`-XX:+ 使用对象头 ' +- ** 在:** " 附加量 " 之前 +- ** 之后:** " 钢筋建造厂 " (也记录为 " 钢筋建造厂 " ) +- 原始研究线:`研究/成果/J23-StringConcat.jsonl' +- 兄妹关系:延长STATIC-VS-HOTSPOT与这对阅读(空间/时间是) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders` | +| 在 `plusEquals[parts=256]` 之前 | **69754.657 纳秒/操作**, **141296.445 字节/操作** | +| 在 `stringBuilderPresized[parts=256]` 之后 | **3278.274 ns/次操作**, **2104.021 B/次操作** | +| **前/后比率** | **约 21.3×** 时间,**约 67×** 每操作字节 | +| parts=64(相同的 JSONL)| plusEquals **1043.665** → SB 预置 **628.989** → **~1.66×** 时间 | + +## 与 lint 的对应 + +`jvmperf-lint` jp-string-concat-hot (`string-concat-in-loop`)。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 政策,严重性为 **错误**。 + +## 限制 + +- 一次性/罕见路径:墙上时钟通常无关紧要。 +- Java 9+ `invokedynamic` 拼接有助于 **表达式** 树;**循环携带** 的 `+=` 仍然会重建。 +- 不要将其等同于 HotSpot 的去优化。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-string-intern-hot.md b/packages/jvmperf/evidence/zh-CN/jp-string-intern-hot.md new file mode 100644 index 0000000..79222a8 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-string-intern-hot.md @@ -0,0 +1,29 @@ +# jp-字符串-驻留-热点 + +## 主张 + +`String.intern()` 在一个热循环中竞争字符串表,比普通插入慢得多(如果需要,可用本地映射/Intern来去重)。 + +## 如何测量 + +- 来源:`J32_RuleEvidenceBatch.jsonl` +- JDK **27 (27+35)**,JVM 参数 `-XX:+UseCompactObjectHeaders` +- **之前:** `intern_n2000` +- **之后:** `plain_n2000` + +## 运行时(本证据文件) + +| 字段 | 值 | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| 之前 | **166619.350** ns_per_op, **48002.651** B/op | +| 之后 | **26335.679** ns_per_op, **48000.417** B/op | +| **前/后比例** | **~6.33×** | + +## Lint 映射 / 策略 + +策略**错误**(约6.3倍) + +## 限制 + +在这台机器上的微基准;参见 `scripts/run-static-vs-hotspot.sh`。不要编造比率。语料库残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-ui-text-append.md b/packages/jvmperf/evidence/zh-CN/jp-ui-text-append.md new file mode 100644 index 0000000..a3fb04b --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-ui-text-append.md @@ -0,0 +1,13 @@ +# jp-ui-text-append — 待处理的措施 + +## 状态 + +**待处理措施** — 此机器上没有 `evidence/jp-ui-text-append.jsonl`。 + +## 原因 + +UI 文本追加(TextView)——需要 Android UI 线程。 + +## 政策 + +停留 **提示** / 评论。在没有在正确运行时提供诚实的前后对比之前,不要升级为错误。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-unpresized-collection.md b/packages/jvmperf/evidence/zh-CN/jp-unpresized-collection.md new file mode 100644 index 0000000..5e37421 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-unpresized-collection.md @@ -0,0 +1,34 @@ +# jp-unpresized-collection — 无预设大小的集合填充 + +## 主张 + +在没有初始容量的情况下填充 `ArrayList`(或类似的集合)会分配更多内存,并且通常比已知 `n` 时的 `new ArrayList(n)` 略慢。在这台机器上,**时间**陡峭下降 **较为轻微**;而 **B/op** 的差异更为明显。 + +## 如何测量 + +- 测试框架:`J25_Presize.java` / `scripts/run_jmh.sh J25_Presize` +- **之前:** `growFromDefault[n=10000]` +- **之后:** `presized[n=10000]` +- 原始数据:`J25_Presize.jsonl` +- 相关性:STATIC-VS-HOTSPOT 案例 2 **部分** + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海 | +| JDK | **27 (27+35)** | +| JVM 参数 | `-XX:+UseCompactObjectHeaders` | +| before n=10k | **117725.103 ns/op**, **326832.742 B/op** | +| after n=10k | **82357.177 ns/op**, **197992.522 B/op** | +| **ratio** | **~1.43×** time, **~1.65×** B/op | +| n=100k(相同 JSONL) | ~**1.37×** 时间,~**1.44×** 每操作字节 | + +## 与 lint 的对应 + +默认严重性 **关闭**(使用 `--enable` 运行;部分实际时间)。证据文件存在,因此该声明是可审计的;不要将每一个 jp-unpresized-collection 语料库的命中都视为必须修复的速度漏洞。 + +## 限制 + +- 最终大小未知 → 无法总是预先确定尺寸;构建器上常见 FP。 +- 在 lint 反向优化轮之后,语料残余仍然升高 — 在升级为 `error` 之前进行分类处理。 diff --git a/packages/jvmperf/evidence/zh-CN/jp-value-class-boxing.md b/packages/jvmperf/evidence/zh-CN/jp-value-class-boxing.md new file mode 100644 index 0000000..28eaacf --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/jp-value-class-boxing.md @@ -0,0 +1,29 @@ +# jp-值类-装箱 + +## 主张 + +`@JvmInline` 值类在作为泛型/`Array`/`nullable` 元素使用时会进行装箱。在 JDK 27 / Kotlin 2.4.21 上,List 路径的时间大约是 `IntArray` 路径的 4.0 倍,分配大约是 5.0 倍。JDK 25 的时间对大约是 1.05 倍。 + +## 如何测量 + +- 来源:`K15_ValueClassBoxing.jsonl` +- JDK **27 (27+35)**,Kotlin **2.4.21**,JVM 参数 `-XX:+UseCompactObjectHeaders`。Linux x64,负载 0.78。 +- **之前:** `sumBoxedList_UserId_n1e4` +- **之后:** `sumPrimitive_intArray_n1e4` + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| When | 2026-10-23 | +| before | **65983.793** ns_per_op, **200056.0** B/op | +| after | **16566.447** ns_per_op, **40032.453** B/op | +| **前后比率** | **约 3.98×** 时间,**约 5.0×** 每操作字节 | + +## Lint 映射 / 策略 + +默认严重性 **提示**。List 与 `IntArray` 相比,大约慢 4.0 倍,分配内存约 5.0 倍。`Array` 路径为 33385 ns/op 和 200032 B/op,大约是 `IntArray` 时间的 2.0 倍。 + +## 限制 + +在这台机器上进行微基准测试;请参见 `scripts/run-static-vs-hotspot.sh`。不要随意编造比率。语料残差 ≠ 加速比。 diff --git a/packages/jvmperf/evidence/zh-CN/vt-unsafe-threadlocal-pool.md b/packages/jvmperf/evidence/zh-CN/vt-unsafe-threadlocal-pool.md new file mode 100644 index 0000000..1f7e2f7 --- /dev/null +++ b/packages/jvmperf/evidence/zh-CN/vt-unsafe-threadlocal-pool.md @@ -0,0 +1,21 @@ +# vt-unsafe-threadlocal-pool — 未发布(以指南为先) + +## 状态 + +**未发货 / 仅指南** — 没有 `evidence/vt-unsafe-threadlocal-pool.jsonl`。不要自行创建。规则 **不在** `Rule.RULES` 中,并且没有 `bad/`/`good/` 的 lint 配置。 + +## 原因 + +选项 B:保留作为教学材料,直到反向选择/FP 审查证明可以发布。导向夹具存放位置如下: + +- `jvmperf-lint/fixtures/guide/vt-unsafe-threadlocal-pool_bad.java` / `vt-unsafe-threadlocal-pool_good.java` +- `jvmperf-lint/fixtures/guide/vt-unsafe-threadlocal-pool_bad.kt` / `vt-unsafe-threadlocal-pool_good.kt` +- `jvmperf-lint/fixtures/guide/README.md` + +## 为什么没有 JSONL + +没有发货规则 → 没有速度门。待处理/不适用的证据存根适用于**已发货**的ID;此文件仅记录故意的缺失。 + +## 解阻条件 + +只有在导盲犬食品 + 反向优化显示低假阳性后才发货;然后添加 `bad/`/`good/`,连接 `Rule.RULES`,并在升级严重性之前进行测量。 diff --git a/packages/jvmperf/zh-CN/CONTRIBUTING.md b/packages/jvmperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..b74eee9 --- /dev/null +++ b/packages/jvmperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +JVM 检查:在 `jvmperf-lint/` 使用 JDK 21+ 运行 `./gradlew test`;指南与脚本见 [README.md](README.md)。不要把本应属于此处的 JMH 证据重复写进 kotlinperf。 diff --git a/packages/jvmperf/zh-CN/README.md b/packages/jvmperf/zh-CN/README.md new file mode 100644 index 0000000..99c0dad --- /dev/null +++ b/packages/jvmperf/zh-CN/README.md @@ -0,0 +1,67 @@ +# jvm性能 + +JVM(Java / Kotlin)性能笔记、可运行示例、JMH/JOL 基准,以及用 **Java** 编写的 `jvmperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**。小型证据 JSONL 位于 `evidence/` 下。 + +## 目录 + +- [英文指南](../docs/en/performance-guide.md) — 来自 `docs/guide-src/en/` +- [`jvmperf-lint`](../jvmperf-lint/) — **Java** 检查器(Gradle,tree-sitter) +- [语料库固定](../docs/zh-CN/corpus.md) +- [证据指针](../evidence/zh-CN/README.md) · [证据政策](../docs/zh-CN/evidence-policy.md) +- HotSpot 关联:`scripts/run-static-vs-hotspot.sh` + +## 焦点 + +分配/装箱,布局(SoA,原始映射),热路径字符串和 I/O,JIT/伪共享/虚拟线程,Kotlin 序列和内联。没有本地基准或引用的说法保持暂定;语料库计数是 lint 内部使用。 + +## 要求 + +- **JDK 21** 用于构建和测试 `jvmperf-lint`(CI:Temurin 21)。将 `JAVA_HOME` 固定为 21 — 仅使用主机 JDK 25 可能会导致 Gradle 报错,出现不明确的 `25.0.x` 信息。该项目使用 Java **21** 工具链 + Foojay 解析器。 +- **JDK 25** 可选,用于 JMH/JOL 示例和基准测试(运行这些脚本时使用单独的 `JAVA_HOME`) +- Kotlin 2.4.x 用于 Kotlin 示例(`scripts/check_kotlin.sh`) +- `lib/` 中的可选 jar 文件 — **不在 git 中**;使用 `bash scripts/fetch-lib.sh` 拉取(参考 `lib/README.md`)。默认 `JVMPERF_LIB_DIR=$ROOT/lib` + +## 运行 lint + +```sh +cd packages/jvmperf/jvmperf-lint +export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-21-openjdk-amd64}" +./gradlew test jar +bin/jvmperf-lint ../examples --no-fail +bin/jvmperf-lint --list-rules +``` + +当存在发现时退出代码为 `1`(除非使用 `--no-fail`)。在使用/输入输出错误时退出代码为 `2`。 + +**严重性说明:** 除非使用 `--no-fail`,否则进程退出仍会将任何发现视为失败。每条规则的默认严重性(`error`、`hint` 或 `off`)遵循 `docs/evidence-policy.md`;标记为 `off` 的规则不会运行,除非使用 `--enable` 选择(此时只运行所列规则)。完整的 `--preset` 配置可能稍后推出。 + +自检:`./gradlew test`(夹具 + 单元)。使用 `--no-fail` 的内部示例作为审查提示。 + +## 长凳与支票 + +```sh +# From packages/jvmperf: rebuild guides from docs/guide-src (code samples are inlined in the chapter files) +bash ../../scripts/build-guides.sh jvmperf +scripts/check_java.sh +scripts/check_kotlin.sh +# JMH / JOL / Kotlin alloc (writes JSONL under research/bench-results): +# scripts/run_jmh.sh … +# scripts/run_jol_maps.sh +# scripts/run_kotlin_alloc.sh +``` + +## 布局 + +```text +docs/guide-src/ chapter sources (en/, zh-CN/) +docs/ assembled guides + corpus.md +examples/ java + kotlin (+ android fixtures) +jvmperf-lint/ Java checker (Gradle) + fixtures +jmh-benches/ JMH sources +lib/ optional jars (gitignored; fetch-lib.sh) +research/ small in-tree Jol/EA probe sources (no *.log dumps) +scripts/ run_*, check_*, env.sh (build_guide is superseded by the repo-level scripts/build-guides.sh) +evidence/ pointers to research JSONL (not full dumps) +``` diff --git a/packages/kotlinperf/docs/guide-src/00_head.md b/packages/kotlinperf/docs/guide-src/00_head.md index abdc2ad..bc97671 100644 --- a/packages/kotlinperf/docs/guide-src/00_head.md +++ b/packages/kotlinperf/docs/guide-src/00_head.md @@ -1,18 +1,14 @@ # Kotlin performance guide (v0) -Kotlin-first collection / string / I/O checklists. For measured JVM evidence (JMH/JOL), see sibling **jvmperf**. +Kotlin-first collection, string, and I/O checklists. For measured JVM evidence (JMH/JOL), see sibling [jvmperf](../../jvmperf/). -> **Scope: Kotlin 1.9+ on JVM.** This kit does **not** replace [jvmperf](../../jvmperf/). Do not invent JMH ratios here — link or re-bench. +> **Scope: Kotlin 1.9+ on JVM.** This kit does not replace [jvmperf](../../jvmperf/). Do not invent JMH ratios here — link or re-bench. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (JMH via jvmperf, or simple wall timers). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Coroutine `launch` in a tight loop is often a correctness/load smell — profile before rewriting. +Profile first (JMH via jvmperf, or simple wall timers). Coroutine `launch` in a tight loop is often a correctness or load smell — profile before rewriting. ## Contents -- [How to read this](#how-to-read-this) - [1. Collections](#1-collections) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) diff --git a/packages/kotlinperf/docs/guide-src/01_collections.md b/packages/kotlinperf/docs/guide-src/01_collections.md index 0588768..5de4ccb 100644 --- a/packages/kotlinperf/docs/guide-src/01_collections.md +++ b/packages/kotlinperf/docs/guide-src/01_collections.md @@ -2,3 +2,5 @@ Prefer `ArrayList(n)` / `MutableList` with capacity when size is known. Avoid allocating a fresh list each iteration when a reused buffer works. Cross-link: jvmperf `jp-unpresized-collection` / `jp-collection-chain` for measured collection smells. +Lint: `kt-list-add-hot` (default off). + diff --git a/packages/kotlinperf/docs/guide-src/02_strings.md b/packages/kotlinperf/docs/guide-src/02_strings.md index 6687659..abfa0be 100644 --- a/packages/kotlinperf/docs/guide-src/02_strings.md +++ b/packages/kotlinperf/docs/guide-src/02_strings.md @@ -2,3 +2,5 @@ `s += x` in a loop is a classic builder case (`StringBuilder` / `buildString`). Keep `println` out of tight loops. For measured string concat, see jvmperf `jp-string-concat-hot`. +Lint: `kt-string-plus-hot`, `kt-println-hot` (default off). + diff --git a/packages/kotlinperf/docs/guide-src/zh-CN/00_head.md b/packages/kotlinperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..e8ba9fd --- /dev/null +++ b/packages/kotlinperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Kotlin 性能指南(v0) + +以 Kotlin 为主的集合、字符串与 I/O 核对清单。若要看有测量的 JVM 证据(JMH/JOL),见同级 [jvmperf](../../../jvmperf/)。 + +> **范围:Kotlin 1.9+(JVM)。** 本 kit 不替代 [jvmperf](../../../jvmperf/)。这里不编造 JMH 比值——请链接或自行重测。 +> 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(经 jvmperf 的 JMH,或简单墙钟计时)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. 集合](#1-集合) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/kotlinperf/docs/guide-src/zh-CN/01_collections.md b/packages/kotlinperf/docs/guide-src/zh-CN/01_collections.md new file mode 100644 index 0000000..9a2d440 --- /dev/null +++ b/packages/kotlinperf/docs/guide-src/zh-CN/01_collections.md @@ -0,0 +1,5 @@ +## 1. 集合 + +已知大小时优先 `ArrayList(n)` / 带容量的 `MutableList`。能复用缓冲区时,避免每次迭代新建 list。交叉链接:jvmperf 的 `jp-unpresized-collection` / `jp-collection-chain` 覆盖有测量的集合 smell。 + +Lint: `kt-list-add-hot`(默认 off) diff --git a/packages/kotlinperf/docs/guide-src/zh-CN/02_strings.md b/packages/kotlinperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..c5ecb01 --- /dev/null +++ b/packages/kotlinperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串与热 I/O + +循环里的 `s += x` 是经典 builder 场景(`StringBuilder` / `buildString`)。不要把 `println` 放进紧密循环。有测量的字符串拼接见 jvmperf `jp-string-concat-hot`。 + +Lint: `kt-string-plus-hot`, `kt-println-hot`(默认 off) diff --git a/packages/kotlinperf/docs/guide-src/zh-CN/99_tail.md b/packages/kotlinperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..d4e6cb8 --- /dev/null +++ b/packages/kotlinperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,14 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| kt-list-add-hot | 循环内 list 分配 / add 增长 | off | +| kt-string-plus-hot | 循环内字符串 `+=` | off | +| kt-println-hot | 循环内 `println` | off | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。有测量的 JVM 主张见:[jvmperf](../../../jvmperf/)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [jvmperf](../../../jvmperf/) diff --git a/packages/kotlinperf/docs/kotlin-performance-guide.md b/packages/kotlinperf/docs/kotlin-performance-guide.md index 895f36a..3a52642 100644 --- a/packages/kotlinperf/docs/kotlin-performance-guide.md +++ b/packages/kotlinperf/docs/kotlin-performance-guide.md @@ -1,18 +1,14 @@ # Kotlin performance guide (v0) -Kotlin-first collection / string / I/O checklists. For measured JVM evidence (JMH/JOL), see sibling **jvmperf**. +Kotlin-first collection, string, and I/O checklists. For measured JVM evidence (JMH/JOL), see sibling [jvmperf](../../jvmperf/). -> **Scope: Kotlin 1.9+ on JVM.** This kit does **not** replace [jvmperf](../../jvmperf/). Do not invent JMH ratios here — link or re-bench. +> **Scope: Kotlin 1.9+ on JVM.** This kit does not replace [jvmperf](../../jvmperf/). Do not invent JMH ratios here — link or re-bench. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (JMH via jvmperf, or simple wall timers). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. Coroutine `launch` in a tight loop is often a correctness/load smell — profile before rewriting. +Profile first (JMH via jvmperf, or simple wall timers). Coroutine `launch` in a tight loop is often a correctness or load smell — profile before rewriting. ## Contents -- [How to read this](#how-to-read-this) - [1. Collections](#1-collections) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) @@ -21,10 +17,14 @@ Kotlin-first collection / string / I/O checklists. For measured JVM evidence (JM Prefer `ArrayList(n)` / `MutableList` with capacity when size is known. Avoid allocating a fresh list each iteration when a reused buffer works. Cross-link: jvmperf `jp-unpresized-collection` / `jp-collection-chain` for measured collection smells. +Lint: `kt-list-add-hot` (default off). + ## 2. Strings and hot I/O `s += x` in a loop is a classic builder case (`StringBuilder` / `buildString`). Keep `println` out of tight loops. For measured string concat, see jvmperf `jp-string-concat-hot`. +Lint: `kt-string-plus-hot`, `kt-println-hot` (default off). + ## Lint map | ID | Smell | Default | diff --git a/packages/kotlinperf/docs/zh-CN/corpus.md b/packages/kotlinperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..965e6c0 --- /dev/null +++ b/packages/kotlinperf/docs/zh-CN/corpus.md @@ -0,0 +1,65 @@ +# kotlinperf 语料库目标 + +大型树木**不会**被转售,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任意本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +| 树 | 链接 | 提交 | 大小 | Lint 范围 | +|------|-----|--------|------|------------| +| kotlinx.coroutines | https://github.com/Kotlin/kotlinx.coroutines.git | `7e0d1b3e9ca83652427ae00dda7d1673f058a743` | ~27M | 整个树 | +| okhttp | https://github.com/square/okhttp.git | `2657315344316ba5bd1292d2fe9ca12636ae2290` | ~15M | 整个代码库(Kotlin 源码) | +| kotlinx.serialization | https://github.com/Kotlin/kotlinx.serialization.git | `4c28c3e33c145da536f1f4a671f70e5eb5b62b9c` | 约13M | 整个树 | + +浅克隆(`--depth 1`)。 + +## 狗粮 (2026-10-21 亚洲/上海) + +| 树 | 原始 (`--raw-paths`) | 反向优化后 | +|------|---------------------|-------------------| +| kotlinx.coroutines | **84** (kt-list-add-hot:2 kt-string-plus-hot:6 kt-println-hot:76) | **6** (1/5/0) | +| okhttp | **29** (11/1/17) | **8** (5/0/3) | +| kotlinx.serialization | **4** (2/0/2) | **1** (1/0/0) | + +规则顺序:kt-list-add-hot / kt-string-plus-hot / kt-println-hot。 + +### Reverse-opt(`kotlinperf-lint`) + +1. **`--raw-paths`:** 基线开关。 +2. **路径跳过:** `test/` `tests/` `androidTest/` `jvmTest/` `commonTest/` `nativeTest/` `benchmarks/` `samples/` `sample/` `examples/` `example/` `docs/`(加上始终跳过 `.git`/`build`/`.gradle`/`vendor`/`fixtures`)。 +3. **路径后缀:** `*Test.kt` / `*Tests.kt` / `/src/test/`。 +4. **kt-list-add-hot:** 在 reverse-opt 下禁止裸的空 `listOf()`(保留 `mutableListOf` / `arrayListOf` / `ArrayList()`)。 +5. **kt-println-hot:** 跳过 KDoc/块注释的延续行(`* … println`);跳过调试/演示文件名和 `DEBUG` 标记。 + +### 残差 + +- 协程:在指导/示例中有一些真正的 `+=` 字符串构建器,它们存在于被跳过的目录之外。 +- okhttp:列出热解码路径中的列表分配(信号)。 + +不要将语料库来源提交到 `packages/kotlinperf`。 + +## G3 TP/FP 分诊 (2026-10-22 亚洲/上海) + +| | r1 sample n=15 | r2 sample n=3 | +|--|----------------|---------------| +| TP | 0% | 0% | +| FP | 40% | 0% | +| OK-idiom | 46.7% | **100%** | +| SKIP-path | 13.3% | 0% | +| **FP+DOC+SKIP** | **53.3%** | **0%** | + +**语料准备好了吗?`否`** — r1 噪声 >40%;r2 全部符合习惯用语(懒惰列表 / 罕见类型)。保留规则 **提示**。 + +### 修复已提交 +- kt-string-plus-hot 集合 `+=` 抑制;kt-println-hot 仅限裸 `println`;kt-list-add-hot 延迟/常量 listOf +- 路径跳过:`test-utils/`、`testing-support/`、`rules-integration-tests/`、`okcurl/` +- r2 计数:协程 **1**,okhttp **2**,序列化 **0** + +## 重新设计 → 准备语料库(2026-10-22 亚洲/上海) + +| 树 | r2 | r3(默认) | 是否准备好语料库? | +|------|----|---------------|---------------| +| kotlinx.coroutines | 1 | **2** | | +| okhttp | 2 | **9** | | +| kotlinx.serialization | 0 | **1** | | + +**判定:语料库可用 `是`。** 样本 n=12:TP 58.3% / 合适成语 41.7% / FP+DOC+跳过 0%。默认设置:kt-list-add-hot–kt-println-hot **关闭**。已移除 KT004(抖动;它覆盖了空列表 + 在有大小提示的循环中添加)。无 G4 升级 / 无虚构 JSONL。 diff --git a/packages/kotlinperf/docs/zh-CN/evidence-policy.md b/packages/kotlinperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..c196dbf --- /dev/null +++ b/packages/kotlinperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,16 @@ +# kotlinperf证据政策 + +## 门槛 + +仅当`evidence/.md`+`.jsonl` **和* *为显式策略决策时,规则才可被视为**`error`。单独测量JSONL不会**自动升级。批次B (2026-10-22)保持包装默认值。 + +## 当前映射 + +截至2026-10-22(B批次G4)。 + +|规则|严重性(默认)|证据|气味| +|------|--------------------|----------|-------| +| kt-list-add-hot | off | * * yes * * ~ 2.1×| ArrayList空vs cap | +| kt-string-plus-hot | off | * * yes * * ~ 1170×| string+= | +lpuzmt1rpesc9ruj7cxv +ooo5nkvl4oeg185ia2dh diff --git a/packages/kotlinperf/docs/zh-CN/kotlin-performance-guide.md b/packages/kotlinperf/docs/zh-CN/kotlin-performance-guide.md new file mode 100644 index 0000000..b5702f1 --- /dev/null +++ b/packages/kotlinperf/docs/zh-CN/kotlin-performance-guide.md @@ -0,0 +1,39 @@ +# Kotlin 性能指南(v0) + +以 Kotlin 为主的集合、字符串与 I/O 核对清单。若要看有测量的 JVM 证据(JMH/JOL),见同级 [jvmperf](../../../jvmperf/)。 + +> **范围:Kotlin 1.9+(JVM)。** 本 kit 不替代 [jvmperf](../../../jvmperf/)。这里不编造 JMH 比值——请链接或自行重测。 +> 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(经 jvmperf 的 JMH,或简单墙钟计时)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. 集合](#1-集合) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + +## 1. 集合 + +已知大小时优先 `ArrayList(n)` / 带容量的 `MutableList`。能复用缓冲区时,避免每次迭代新建 list。交叉链接:jvmperf 的 `jp-unpresized-collection` / `jp-collection-chain` 覆盖有测量的集合 smell。 + +Lint: `kt-list-add-hot`(默认 off) +## 2. 字符串与热 I/O + +循环里的 `s += x` 是经典 builder 场景(`StringBuilder` / `buildString`)。不要把 `println` 放进紧密循环。有测量的字符串拼接见 jvmperf `jp-string-concat-hot`。 + +Lint: `kt-string-plus-hot`, `kt-println-hot`(默认 off) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| kt-list-add-hot | 循环内 list 分配 / add 增长 | off | +| kt-string-plus-hot | 循环内字符串 `+=` | off | +| kt-println-hot | 循环内 `println` | off | + +仅在有 `evidence/.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。有测量的 JVM 主张见:[jvmperf](../../../jvmperf/)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [jvmperf](../../../jvmperf/) diff --git a/packages/kotlinperf/docs/zh-CN/rule-ids.md b/packages/kotlinperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..b6cdb26 --- /dev/null +++ b/packages/kotlinperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,25 @@ +# kotlinperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `kt-list-add-hot` | +| `kt-string-plus-hot` | +| `kt-println-hot` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `kt-list-add-in-loop` | `kt-list-add-hot` | +| `kt-println-in-loop` | `kt-println-hot` | +| `kt-string-plus-in-loop` | `kt-string-plus-hot` | + +## Removed (tombstone; never reuse) + +- `KT004` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/kotlinperf/evidence/zh-CN/README.md b/packages/kotlinperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..9ade636 --- /dev/null +++ b/packages/kotlinperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# kotlinperf 证据 + +更新于 **2026-10-22** 亚洲/上海(B批次 G4 填充)。 + +kt-list-add-hot–kt-println-hot 在 kotlinc 2.0.21 / JRE 21 上测量。KT004 在一次不确定的复测后被移除(参见 `_removed/`)。默认设置未更改。 + +请参阅每条规则的 `*.md` + `*.jsonl`(或被阻止的存根)。政策:[../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。根矩阵:[../../../docs/rule-regression.md](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/kotlinperf/evidence/zh-CN/kt-list-add-hot.md b/packages/kotlinperf/evidence/zh-CN/kt-list-add-hot.md new file mode 100644 index 0000000..c8bf91b --- /dev/null +++ b/packages/kotlinperf/evidence/zh-CN/kt-list-add-hot.md @@ -0,0 +1,34 @@ +# kt-list-add-hot — ArrayList 空与容量 + +**状态:** 已测量 + +## 主张 + +在 N=2e6 个装箱整数时,空 `ArrayList()` + 添加 与 `ArrayList(n)` 测量结果约为 ~**2.1×**。 + +## 如何测量 + +- 测试工具:`examples/bench/EvidenceKt.kt` → jar +- kotlinc 2.0.21 / JRE 21 + +- 原始行: [`kt-list-add-hot.jsonl`](../kt-list-add-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| kotlin | **2.0.21** | +| jre | **21.0.12.1** | +| 前中值 (`list_add_empty`) | 5.303875 毫秒 | +| (`arraylist_capacity`) 之后的中位数 | 3.358458 毫秒 | +| **前后比例** | **~1.58×** | + +## 与 lint 的对应 + +`kotlinperf-lint` kt-list-add-hot:保持默认;仅限 kit-local KT*。 + +## 限制 + +- 拳击占主导地位;容量对某些情况有帮助。不是 jvmperf JMH ID。 +- 默认保持 **关闭**。 diff --git a/packages/kotlinperf/evidence/zh-CN/kt-println-hot.md b/packages/kotlinperf/evidence/zh-CN/kt-println-hot.md new file mode 100644 index 0000000..174cfab --- /dev/null +++ b/packages/kotlinperf/evidence/zh-CN/kt-println-hot.md @@ -0,0 +1,33 @@ +# kt-println-hot — 循环中的 println + +**状态:** 已测量 + +## 主张 + +每次迭代将 `println` 输出到 `/dev/null` 比安静循环加一次 `println` 慢得多(在 Kotlin 2.4.21 / JDK 27 上大约慢 270 倍)。 + +## 如何测量 + +- Harness: EvidenceKt.kt +- N=20000 + +- 原始行: [`kt-println-hot.jsonl`](../kt-println-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| kotlin | **2.0.21** | +| jre | **21.0.12.1** | +| 循环内打印前的中位数 (`println_in_loop`) | 8.620792 毫秒 | +| (`println_once`) 后的中位数 | 0.031875 毫秒 | +| **前/后比例** | **~270×** | + +## 与 lint 的对应 + +`kotlinperf-lint` kt-println-hot:保持默认;仅限 kit-local KT*。 + +## 限制 + +- 默认保持**关闭**。 diff --git a/packages/kotlinperf/evidence/zh-CN/kt-string-plus-hot.md b/packages/kotlinperf/evidence/zh-CN/kt-string-plus-hot.md new file mode 100644 index 0000000..01e1d0a --- /dev/null +++ b/packages/kotlinperf/evidence/zh-CN/kt-string-plus-hot.md @@ -0,0 +1,32 @@ +# kt-string-plus-hot — string += 与 buildString + +**状态:** 已测量 + +## 主张 + +`s +=` 在循环中比 `buildString` 慢得多(N=30000 时约快 521 倍),在 Kotlin 2.4.21 / JDK 27 上测试。 + +## 如何测量 + +- 安全带:EvidenceKt.kt + +- 原始行: [`kt-string-plus-hot.jsonl`](../kt-string-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| kotlin | **2.0.21** | +| jre | **21.0.12.1** | +| 中位数之前 (`string_plus`) | 93.440375 毫秒 | +| (`buildString`) 后的中位数 | 0.179375 毫秒 | +| **前/后比率** | **~521×** | + +## 与 lint 的对应 + +`kotlinperf-lint` kt-string-plus-hot:保持默认;仅限 kit-local KT*。 + +## 限制 + +- 默认保持**关闭**。 diff --git a/packages/kotlinperf/zh-CN/CONTRIBUTING.md b/packages/kotlinperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/kotlinperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/kotlinperf/zh-CN/README.md b/packages/kotlinperf/zh-CN/README.md new file mode 100644 index 0000000..d2a8491 --- /dev/null +++ b/packages/kotlinperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# kotlinperf + +> **范围说明:** **kotlinperf** 是 **以 Kotlin 为先**(协程 / 集合 / 热 I/O 异味)。同类项目 [jvmperf](../../jvmperf/) 仍然是经测量的 **JVM/Java(+Kotlin)** 工具包,并附有 JMH/JOL 证据 — **不要在这里重复 JMH 证据**;可将测量声明交叉链接到 jvmperf。本工具包的规则是启发式的,默认 **关闭**。 + +Kotlin 性能笔记、小示例,以及 `kotlinperf-lint`。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档: [docs/README.md](../../../docs/zh-CN/README.md)。**语料库就绪**。kt-list-add-hot–kt-println-hot **关闭**。移除了一条规则(墓碑,从未获得 slug)。指南 v0。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/kotlin-performance-guide.md) — 来自 `docs/guide-src/` +- [`kotlinperf-lint`](../kotlinperf-lint/README.md) — kt-list-add-hot / kt-string-plus-hot / kt-println-hot +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +避免在热点循环中重复分配列表 / 字符串 `+=` / `println`;更倾向于使用构建器,并参考经过测量的 JVM 性能指南来进行 JVM 范围的优化。 + +## 运行 lint + +```sh +cd packages/kotlinperf/kotlinperf-lint && bash build.sh +./kotlinperf-lint path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅在 **错误** 严重性发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ pending stubs until benches land +examples/ tiny demos +kotlinperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/luaperf/docs/guide-src/00_head.md b/packages/luaperf/docs/guide-src/00_head.md index 73ee451..9380acb 100644 --- a/packages/luaperf/docs/guide-src/00_head.md +++ b/packages/luaperf/docs/guide-src/00_head.md @@ -2,16 +2,12 @@ Fewer table resizes, fewer quadratic string joins, quieter hot loops. -> **Scope: Lua 5.1–5.4 / LuaJIT-shaped patterns.** Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Lua 5.1–5.4 / LuaJIT-shaped patterns.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (LuaJIT `-jv`, simple wall timers). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (LuaJIT `-jv`, simple wall timers). Treat chapters as checklists, not rewrite mandates. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Tables](#1-tables) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) diff --git a/packages/luaperf/docs/guide-src/01_tables.md b/packages/luaperf/docs/guide-src/01_tables.md index 942bb6f..bfea32f 100644 --- a/packages/luaperf/docs/guide-src/01_tables.md +++ b/packages/luaperf/docs/guide-src/01_tables.md @@ -9,3 +9,5 @@ for i = 1, n do end ``` +Lint: `lu-table-insert-hot` (default off). + diff --git a/packages/luaperf/docs/guide-src/02_strings.md b/packages/luaperf/docs/guide-src/02_strings.md index 58d1ce8..6f03c5c 100644 --- a/packages/luaperf/docs/guide-src/02_strings.md +++ b/packages/luaperf/docs/guide-src/02_strings.md @@ -2,3 +2,5 @@ `a .. b` in a loop builds many temporaries. Prefer collecting parts then `table.concat`. Keep `print` out of tight loops (gate or buffer). +Lint: `lu-string-concat-hot` (default error); `lu-print-hot` (default hint). + diff --git a/packages/luaperf/docs/guide-src/99_tail.md b/packages/luaperf/docs/guide-src/99_tail.md index 27b6443..6685ef5 100644 --- a/packages/luaperf/docs/guide-src/99_tail.md +++ b/packages/luaperf/docs/guide-src/99_tail.md @@ -6,7 +6,7 @@ | lu-string-concat-hot | string `..` in loop | error | | lu-print-hot | `print` in loop | hint | -Escalate only with `evidence/lu-*.jsonl` — see [evidence-policy.md](evidence-policy.md). lu-print-hot has measured evidence but stays **hint** (I/O sink–dependent). +Escalate only with `evidence/lu-*.jsonl` — see [evidence-policy.md](evidence-policy.md). `lu-print-hot` has measured evidence but stays hint (I/O sink–dependent). ## References diff --git a/packages/luaperf/docs/guide-src/zh-CN/00_head.md b/packages/luaperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..b112372 --- /dev/null +++ b/packages/luaperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,14 @@ +# Lua 性能指南(v0) + +少做 table 扩容,少做平方级字符串拼接,让热循环更安静。 + +> **范围:Lua 5.1–5.4 / LuaJIT 形态的模式。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(LuaJIT `-jv`、简单墙钟计时)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. Table](#1-table) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/luaperf/docs/guide-src/zh-CN/01_tables.md b/packages/luaperf/docs/guide-src/zh-CN/01_tables.md new file mode 100644 index 0000000..fdcc6d3 --- /dev/null +++ b/packages/luaperf/docs/guide-src/zh-CN/01_tables.md @@ -0,0 +1,12 @@ +## 1. Table + +不知道最终大小时 `table.insert` 仍可用,但热循环里反复扩容会受伤。已知 `n` 时,优先预分配数组部分后用 `t[#t+1] = x`,或按索引写字段。 + +```lua +local t = {} +for i = 1, n do + t[i] = i +end +``` + +Lint: `lu-table-insert-hot`(默认 off) diff --git a/packages/luaperf/docs/guide-src/zh-CN/02_strings.md b/packages/luaperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..8418832 --- /dev/null +++ b/packages/luaperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串与热 I/O + +循环里的 `a .. b` 会产生许多临时对象。优先收集片段再 `table.concat`。不要把 `print` 放进紧密循环(门控或缓冲)。 + +Lint: `lu-string-concat-hot`(default error); `lu-print-hot`(默认 hint) diff --git a/packages/luaperf/docs/guide-src/zh-CN/99_tail.md b/packages/luaperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..7138e7e --- /dev/null +++ b/packages/luaperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,13 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| lu-table-insert-hot | 循环内 table.insert / 扩容 | off | +| lu-string-concat-hot | 循环内字符串 `..` | error | +| lu-print-hot | 循环内 `print` | hint | + +仅在有 `evidence/lu-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。lu-print-hot 有测量证据但仍保持 hint(依赖 I/O sink)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/luaperf/docs/lua-performance-guide.md b/packages/luaperf/docs/lua-performance-guide.md index 03cc8c4..94fb606 100644 --- a/packages/luaperf/docs/lua-performance-guide.md +++ b/packages/luaperf/docs/lua-performance-guide.md @@ -2,16 +2,12 @@ Fewer table resizes, fewer quadratic string joins, quieter hot loops. -> **Scope: Lua 5.1–5.4 / LuaJIT-shaped patterns.** Claims that assert speedups name a local example or are provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Lua 5.1–5.4 / LuaJIT-shaped patterns.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (LuaJIT `-jv`, simple wall timers). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (LuaJIT `-jv`, simple wall timers). Treat chapters as checklists, not rewrite mandates. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Tables](#1-tables) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [Lint map](#lint-map) @@ -27,10 +23,14 @@ for i = 1, n do end ``` +Lint: `lu-table-insert-hot` (default off). + ## 2. Strings and hot I/O `a .. b` in a loop builds many temporaries. Prefer collecting parts then `table.concat`. Keep `print` out of tight loops (gate or buffer). +Lint: `lu-string-concat-hot` (default error); `lu-print-hot` (default hint). + ## Lint map | ID | Smell | Default | @@ -39,7 +39,7 @@ end | lu-string-concat-hot | string `..` in loop | error | | lu-print-hot | `print` in loop | hint | -Escalate only with `evidence/lu-*.jsonl` — see [evidence-policy.md](evidence-policy.md). lu-print-hot has measured evidence but stays **hint** (I/O sink–dependent). +Escalate only with `evidence/lu-*.jsonl` — see [evidence-policy.md](evidence-policy.md). `lu-print-hot` has measured evidence but stays hint (I/O sink–dependent). ## References diff --git a/packages/luaperf/docs/zh-CN/corpus.md b/packages/luaperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..0091698 --- /dev/null +++ b/packages/luaperf/docs/zh-CN/corpus.md @@ -0,0 +1,41 @@ +# luaperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +浅克隆。 + +| 树 | 提交 | 大小 | +|------|--------|------| +| **kong** | `2103212` | 31M | +| **apisix** | `016dcd0` | 51M | +| **lua-resty-core** | `873d072` | 2.8M | + +使用以下工具进行代码检查: + +```sh +lua5.4 packages/luaperf/luaperf-lint/luaperf-lint.lua \ + / --format json --no-fail +``` + +检查器是 **Lua 5.4**(`luaperf-lint`)。规则 lu-table-insert-hot–lu-print-hot(**lu-table-insert-hot 默认 `off`**;**lu-string-concat-hot 默认 `error`** 与 G4;lu-print-hot `hint` 与 G4)。 + +## 内部测试版:G3 r2 和 G4 r3 之后(2026-10-21 到 2026-10-22,亚洲/上海) + +| Tree | baseline → r1 → r2 → **r3** | +|------|------------------------------| +| **kong** | 2475 → 75 → 19 → **11** (lu-string-concat-hot:10 lu-print-hot:1) | +| **apisix** | 560 → 198 → 26 → **23** (lu-string-concat-hot:23) | +| **lua-resty-core** | 13 → 0 → 0 → **0** | + +**语料库准备好:是** — r3 样本 n=34:TP 73.5%,FP+DOC+SKIP 2.9%;在 lu-string-concat-hot 上的真实 TP;lu-string-concat-hot 错误有 G4(`evidence/lu-string-concat-hot.md`+`jsonl`)。lu-print-hot 有已测量的证据但保持提示。 + +### 反向优化 + +- 路径跳过:`spec/` `t/` `test(s)/` `example(s)/` `docs/` `benchmark(s)/` `.ci/` `autodoc/` `fixtures/` `vendor/` `cli/` `bin/` `scripts/` +- **lu-table-insert-hot 默认 `off`** — OpenResty/Lua `table.insert` 收集习惯用法 +- lu-string-concat-hot:仅在 **标识符边界** 上累积(在 `ineligible_key` 内部不匹配中间标识符 `key`) +- lu-print-hot:仅裸 `print(`(不包括 `buffered_print` / `ngx_print`) + +不要将语料库来源提交到 `packages/luaperf`。 diff --git a/packages/luaperf/docs/zh-CN/evidence-policy.md b/packages/luaperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..0d4cc5e --- /dev/null +++ b/packages/luaperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,22 @@ +# luaperf 证据政策 + +## 门槛 + +一条规则只有在 `packages/luaperf/evidence/` 下两者都存在时,才可以被视为 **`error`**: + +1. `.md` — 声明,如何测量,工具链版本,限制 +2. `.jsonl` — ≥1 条前/后行,包含 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts` + +否则规则保持 **`hint`**。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| lu-table-insert-hot | 关闭 | 已测量(`lu-table-insert-hot.md` + `lu-table-insert-hot.jsonl`);保持关闭 | 在循环中使用 table.insert 插入无大小或不断增长的表 | +| lu-string-concat-hot | **错误** | 是 (`lu-string-concat-hot.md` + `lu-string-concat-hot.jsonl`) | 循环中的字符串..连接 | +| lu-print-hot | 提示 | 是 (`lu-print-hot.md` + `lu-print-hot.jsonl`) | 循环打印 | + +lu-string-concat-hot 在 Lua 5.4.7 上的 G4 微基准测试后升级(固定形状的数字约 6.6×;固定 32 字节块约 117×;小 N 约 1×)。lu-table-insert-hot 保持 `off`(G3 OK-idiom)。lu-print-hot 有 G4 证据(在 Lua 5.4.7 上 print/quiet 到 `/dev/null` 约 85×),但仍保持 **hint** —— 依赖 I/O 接收器,JP002/CP003 风格(调试/CLI 打印器常用;仅缓冲约 2×)。 diff --git a/packages/luaperf/docs/zh-CN/lua-performance-guide.md b/packages/luaperf/docs/zh-CN/lua-performance-guide.md new file mode 100644 index 0000000..9bcdb5a --- /dev/null +++ b/packages/luaperf/docs/zh-CN/lua-performance-guide.md @@ -0,0 +1,44 @@ +# Lua 性能指南(v0) + +少做 table 扩容,少做平方级字符串拼接,让热循环更安静。 + +> **范围:Lua 5.1–5.4 / LuaJIT 形态的模式。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(LuaJIT `-jv`、简单墙钟计时)。各章是核对清单,不是强制重写令。 + +## 目录 + +- [1. Table](#1-table) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [Lint 对照](#lint-对照) + +## 1. Table + +不知道最终大小时 `table.insert` 仍可用,但热循环里反复扩容会受伤。已知 `n` 时,优先预分配数组部分后用 `t[#t+1] = x`,或按索引写字段。 + +```lua +local t = {} +for i = 1, n do + t[i] = i +end +``` + +Lint: `lu-table-insert-hot`(默认 off) +## 2. 字符串与热 I/O + +循环里的 `a .. b` 会产生许多临时对象。优先收集片段再 `table.concat`。不要把 `print` 放进紧密循环(门控或缓冲)。 + +Lint: `lu-string-concat-hot`(default error); `lu-print-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| lu-table-insert-hot | 循环内 table.insert / 扩容 | off | +| lu-string-concat-hot | 循环内字符串 `..` | error | +| lu-print-hot | 循环内 `print` | hint | + +仅在有 `evidence/lu-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。lu-print-hot 有测量证据但仍保持 hint(依赖 I/O sink)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/luaperf/docs/zh-CN/rule-ids.md b/packages/luaperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..ba6e0bb --- /dev/null +++ b/packages/luaperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,21 @@ +# luaperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `lu-table-insert-hot` | +| `lu-string-concat-hot` | +| `lu-print-hot` | + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `lu-print-in-loop` | `lu-print-hot` | +| `lu-string-concat-in-loop` | `lu-string-concat-hot` | +| `lu-table-insert-in-loop` | `lu-table-insert-hot` | diff --git a/packages/luaperf/evidence/zh-CN/README.md b/packages/luaperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..7f9d583 --- /dev/null +++ b/packages/luaperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# luaperf 证据 + +- **lu-string-concat-hot** — 测量值 (`lu-string-concat-hot.md` + `lu-string-concat-hot.jsonl`);默认严重性为 **错误**。 +- **lu-print-hot** — 测量值 (`lu-print-hot.md` + `lu-print-hot.jsonl`);默认严重性保持 **提示**(取决于 I/O 环境)。 +- **lu-table-insert-hot** — 测量值 (`lu-table-insert-hot.md` + `lu-table-insert-hot.jsonl`);默认保持 **关闭**(G3:在 OpenResty 代码中为 OK-惯用法)。 + +请参见 [../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。 diff --git a/packages/luaperf/evidence/zh-CN/lu-print-hot.md b/packages/luaperf/evidence/zh-CN/lu-print-hot.md new file mode 100644 index 0000000..d7a3089 --- /dev/null +++ b/packages/luaperf/evidence/zh-CN/lu-print-hot.md @@ -0,0 +1,54 @@ +# lu-print-hot — 在循环中使用 `print` + +## 主张 + +在 Lua 5.4 中,在热数值/`for` 循环中调用 `print(...)` 要比静默累加(或在之后只打印一次)慢得多,即使每个 `print` 都被重定向到 `/dev/null`。格式化和写入管道占据主导;在真实的 TTY/管道上通常更慢,而且**依赖环境**。 + +## 如何测量 + +- 测试工具: `packages/luaperf/examples/bench/evidence_lu003.lua` +- 计时器: Lua `os.clock()`(CPU 秒) +- 协议: 预热=3;取9轮的中位数;每次计时样本前执行 `collectgarbage("collect")` +- **标准输出目标:** 通过 `_G.print` 重载到 `/dev/null`(Lua 5.4 的 `print` 写入 C 的 `stdout` FILE*,所以单独 `io.stdout = …` 并不能重定向;测试工具替换了 `print`) +- 配对 A(对应测试用例 `bad/lu-print-hot_print_loop.lua` / `good/lu-print-hot_print_outside.lua`):N=20000 + - **之前:** 每次迭代 `print(i)` + - **之后:** 安静地 `s = s + i` 然后只 `print(s)` 一次 +- 配对 B(先缓冲再一次写入):N=20000 + - **之前:** 每次迭代 `print(i)` + - **之后:** `parts[i] = tostring(i)` 然后只 `print(table.concat(parts, "\n"))` 一次 +- 配对 C(小 N): N=100 +- 原始行: [`lu-print-hot.jsonl`](../lu-print-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| Lua | **Lua 5.4.7** | +| 标准输出 | **`/dev/null`** (`_G.print` → `io.open("/dev/null","w")`) | +| 中值之前 (`print_in_loop`) | 7.507 毫秒 | +| (`quiet_then_print_once`) 之后的中位数 | 0.088 毫秒 | +| **打印/静音 比率** | **~85.3×** | +| 调用前中位数 (`print_in_loop_vs_buffer`) | 7.551 毫秒 | +| (`buffer_then_one_print`) 后的中位数 | 3.783 毫秒 | +| **打印/缓冲比** | **~2.0×** | +| 循环前的中位数 (`small_print_in_loop`) | 0.037 毫秒 | +| (`small_quiet_once`) 之后的中位数 | 0.001 毫秒 | +| **比率 小 N** | **~37×**(之后接近计时器底值 — 仅方向性) | + +方向稳定:循环内 `print` 有成本;quiet/once 成本低。绝对毫秒数和比例会随接收端(null、pipe 或 TTY)、Lua 构建版本和 N 变化而波动。在 Lua 或机器更改后重新运行测试程序。 + +## 与 lint 的对应 + +`luaperf-lint` lu-print-hot 标记循环窗口内的 `print(`(不是像 `to_print` 这样的名字 — 参见 `good/lu-print-hot_to_print_name.lua`)。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md),默认严重性保持为 **`hint`**: + +- 即使使用 `/dev/null`,这台机器上的成本依然是**真实的**。 +- 仍然不是一个 `错误` 门控(与 jsperf **JP002** / cperf **CP003** 的理由相同):故意的调试循环、测试辅助工具和 CLI 进度打印是常见的;比率会随着接收端而大幅波动;仅通过缓冲在这里也只是~2倍。 +- 将其视为针对热路径的审查提示;只有在路径已知为热点且打印是非故意时,才使用 `--severity lu-print-hot=error` 逐树升级。 + +## 限制 + +- 微基准测试;不是应用程序配置文件。 +- 计时器是 `os.clock()`,在 Lua 级别的 `print` 覆盖下的 CPU 时间——低估了阻塞 TTY,可能与 C 语言 `stdout` 的 fwrite 成本不同。 +- 不测量 `io.write`、自定义记录器或 ngx.log。 +- 历史说明:lu-table-insert-hot 仍然“关闭”;在 G4 复查后,kit 后来改为**语料库准备就绪**(见 docs/corpus.md)。 diff --git a/packages/luaperf/evidence/zh-CN/lu-string-concat-hot.md b/packages/luaperf/evidence/zh-CN/lu-string-concat-hot.md new file mode 100644 index 0000000..07effca --- /dev/null +++ b/packages/luaperf/evidence/zh-CN/lu-string-concat-hot.md @@ -0,0 +1,49 @@ +# lu-string-concat-hot — 循环中使用 `..` 进行字符串累加 + +## 主张 + +在 Lua 5.4 中,在数值/`for` 循环中用 `s = s .. chunk` 来构建字符串比先收集各个部分然后使用 `table.concat` 要慢得多,因为每次使用 `..` 都会复制不断增长的前缀(经典的二次成本)。随着 N 和 chunk 大小增加,开销会增大;对于很小的 N,开销大约是 1×。 + +## 如何测量 + +- 测试工具:`packages/luaperf/examples/bench/evidence_lu002.lua` +- 计时器:Lua `os.clock()`(CPU 秒;单线程内核 ≈ 该机器上的实际时间) +- 协议:预热=3;9轮取中位数;每次计时样本前执行 `collectgarbage("collect")` +- 配对 A(匹配测试夹具 `bad/lu-string-concat-hot_concat_loop.lua` / `good/lu-string-concat-hot_concat.lua`):N=20000 + - **之前:** 循环中 `s = s .. tostring(i)` + - **之后:** `parts[i] = tostring(i)` 然后 `table.concat(parts)` +- 配对 B(固定 32 字节块):N=10000 + - **之前:** 循环中 `s = s .. chunk` + - **之后:** 索引填充 parts 然后 `table.concat` +- 配对 C(小 N 一次性):N=100 — 预期 ~1× +- 原始行:[`lu-string-concat-hot.jsonl`](../lu-string-concat-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| Lua | **Lua 5.4.7** | +| 中位数之前 (`digits_concat_loop`) | 54.149 毫秒 | +| (`digits_table_concat`) 后的中位数 | 8.248 毫秒 | +| **前后数字比例** | **~6.57×** | +| `chunk32_concat_loop` 之前的中位数 | 60.630 毫秒 | +| (`chunk32_table_concat`) 之后的中位数 | 0.518 毫秒 | +| **ratio chunk32 前/后** | **~117×** | +| 前中值(`small_n_concat_loop`) | 0.032 毫秒 | +| (`small_n_table_concat`) 之后的中位数 | 0.033 毫秒 | +| **ratio small N** | **~0.97×** (~1×) | + +各对之间方向稳定;绝对毫秒和比率对 N/块/机器敏感(呈二次关系)。在 Lua 或机器更改后重新运行测试工具。 + +## 与 lint 的对应 + +`luaperf-lint` lu-string-concat-hot 标记在循环窗口内进行字符串累加 `s = s .. x`(不是一次性构建的键值,如 `k .. "=" .. v` — 参见 `good/lu-string-concat-hot_oneshot_key.lua`)。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 的默认严重性是 **`error`**:该 JSONL 存在,wall/CPU 同意这是热累加,并且 G3 语料库的残差(例如 APISIX http-dubbo)看起来像真实的 TP。较小/冷的循环可能仍然没问题 — 当循环不频繁时,可以使用 `--severity lu-string-concat-hot=hint` 或 `off` 来覆盖。 + +## 限制 + +- 微型基准测试;不是应用程序配置文件。 +- 计时器是 `os.clock()` CPU 时间,而不是 `CLOCK_MONOTONIC`;对于单线程 CPU 密集型字符串操作可以,但不适用于 I/O 密集型路径。 +- Pair C 显示在极小的 N 下,这种现象是情境性的——不要把每个 `..` 都当作崩溃错误。 +- 不测量 `__concat` 元方法或用户数据。 +- 历史备注:lu-table-insert-hot 仍然是关闭状态;kit 后来在 G4 重新检查后切换为 **语料库就绪**(见 docs/corpus.md)。 diff --git a/packages/luaperf/evidence/zh-CN/lu-table-insert-hot.md b/packages/luaperf/evidence/zh-CN/lu-table-insert-hot.md new file mode 100644 index 0000000..73b6ba0 --- /dev/null +++ b/packages/luaperf/evidence/zh-CN/lu-table-insert-hot.md @@ -0,0 +1,37 @@ +# lu-table-insert-hot — table.insert 与 t[i]= + +## 主张 + +在这台机器上测量:主要热情况 **`table_insert`** 比例前/后约 **~4.74×**(之后更快)。默认严重性保持 **`off`**。仅凭此填充 **不要** 升级到 `error`。 + +## 如何测量 + +- 测试架构: `packages/luaperf/examples/bench/evidence_lu001.lua` +- 协议: warm=3;取9轮的中位数;在适用的情况下进行垃圾回收/收集 +- 原始数据行: [`lu-table-insert-hot.jsonl`](../lu-table-insert-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **Lua 5.4.7** | +| 在 (`index_assign`) 之后的中位数 | 2.029000 毫秒 (操作=200000,比率=1.000000) | +| 插入表之前的中位数 (`table_insert`) | 9.620000 毫秒 (操作数=200000,比例=4.741252) | +| (`hash_len_append`) 之后的中位数 | 3.926000 毫秒 (操作次数=200000,比例=1.000000) | +| 插入表与长度之前的中位数 (`table_insert_vs_len`) | 9.620000 毫秒 (操作=200000,比例=2.450331) | +| (`small_index`) 之后的中位数 | 3.814000 毫秒 (操作次数=100,比率=1.000000) | +| 插入前的中位数 (`small_insert`) | 13.184000 毫秒 (操作=100,比率=3.456738) | +| **主比率 前/后** | **~4.74×** (`table_insert`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试套件。 + +## 与 lint 的对应 + +`luaperf-lint` **lu-table-insert-hot** 默认保持 **`关闭`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比率声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/luaperf/zh-CN/CONTRIBUTING.md b/packages/luaperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/luaperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/luaperf/zh-CN/README.md b/packages/luaperf/zh-CN/README.md new file mode 100644 index 0000000..4ad5ceb --- /dev/null +++ b/packages/luaperf/zh-CN/README.md @@ -0,0 +1,38 @@ +# luaperf + +> **范围说明:** **luaperf** 针对 **Lua 5.1–5.4** / LuaJIT 的常见模式。启发式检测;在升级前先进行测量。 + +Lua 性能注意事项、小示例和 `luaperf-lint`。 + +[XPerf](../../../zh-CN/README.md) 的一部分。共享文档: [docs/README.md](../../../docs/zh-CN/README.md)。启发式 lint (**语料库就绪: 是**; **lu-string-concat-hot=错误** 与 G4; lu-table-insert-hot=关; lu-print-hot=提示 与 G4)。指南 v0。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/lua-performance-guide.md) — 来自 `docs/guide-src/` +- [`luaperf-lint`](../luaperf-lint/README.md) — lu-table-insert-hot–lu-print-hot +- [语料库固定](../docs/zh-CN/corpus.md) + +## 焦点 + +在已知长度时预先设置表大小,在循环中尽量使用 table.concat 而不是 `..`,避免在高频循环中使用 `print`。 + +## 运行 lint + +```sh +lua packages/luaperf/luaperf-lint/luaperf-lint.lua path/to/sources +bash packages/luaperf/scripts/smoke-lint.sh +bash packages/luaperf/scripts/self-lint.sh +``` + +仅在 **错误** 严重级别的发现时退出 `1`(提示默认退出 `0`,除非使用 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ lu-string-concat-hot measured (error); lu-print-hot measured (hint); lu-table-insert-hot measured (off) +examples/ tiny demos +luaperf-lint/ checker + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/nimperf/docs/guide-src/00_head.md b/packages/nimperf/docs/guide-src/00_head.md index 8f8ea3b..95c0bea 100644 --- a/packages/nimperf/docs/guide-src/00_head.md +++ b/packages/nimperf/docs/guide-src/00_head.md @@ -1,4 +1,13 @@ # Nim performance guide (v0) -Assembled from `docs/guide-src/`. Scaffold — not a measured handbook. Heuristic kit — measure before escalating lint to error. +Assembled from `docs/guide-src/`. Scaffold checklist — not a measured handbook. Measure before escalating lint to error. + +> **Scope: Nim (heuristic kit).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. + +## Contents + +- [1. Sequences](#1-sequences) +- [2. Strings](#2-strings) +- [3. I/O](#3-io) +- [Lint map](#lint-map) diff --git a/packages/nimperf/docs/guide-src/01_seq.md b/packages/nimperf/docs/guide-src/01_seq.md index 7fa8aa8..5982c90 100644 --- a/packages/nimperf/docs/guide-src/01_seq.md +++ b/packages/nimperf/docs/guide-src/01_seq.md @@ -1,4 +1,6 @@ -## Sequences +## 1. Sequences -Prefer `newSeqOfCap` / `setLen` + index writes when the length or iteration count is known. Repeated `.add` in hot loops reallocates. Companion: **nm-seq-no-capacity**. +Prefer `newSeqOfCap` / `setLen` + index writes when the length or iteration count is known. Repeated `.add` in hot loops reallocates. + +Lint: `nm-seq-add-hot` (default off after G3); `nm-seq-no-capacity` (empty seq + `.add` size-hinted; default hint). diff --git a/packages/nimperf/docs/guide-src/02_strings.md b/packages/nimperf/docs/guide-src/02_strings.md index 36428ff..84c2b9d 100644 --- a/packages/nimperf/docs/guide-src/02_strings.md +++ b/packages/nimperf/docs/guide-src/02_strings.md @@ -1,4 +1,4 @@ -## Strings +## 2. Strings On Nim 2.2.12, `-d:release`, `&=` of 20000 pieces was faster than `newSeq` plus `join` (about 0.79×). The lint that suggested `join` was removed. See `evidence/_removed/nm-string-amp-in-loop.REMOVED.md`. diff --git a/packages/nimperf/docs/guide-src/03_io.md b/packages/nimperf/docs/guide-src/03_io.md index 8da2a0b..827b814 100644 --- a/packages/nimperf/docs/guide-src/03_io.md +++ b/packages/nimperf/docs/guide-src/03_io.md @@ -1,4 +1,6 @@ -## IO +## 3. I/O Keep `echo` out of hot loops; gate or buffer. +Lint: `nm-echo-hot` (default off after G3). + diff --git a/packages/nimperf/docs/guide-src/zh-CN/00_head.md b/packages/nimperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..428ef8e --- /dev/null +++ b/packages/nimperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,13 @@ +# Nim 性能指南(v0) + +由 `docs/guide-src/` 组装。脚手架核对清单——不是有测量支撑的手册。把 lint 升为 error 前请先测量。 + +> **范围:Nim(启发式 kit)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +## 目录 + +- [1. 序列](#1-序列) +- [2. 字符串](#2-字符串) +- [3. I/O](#3-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/nimperf/docs/guide-src/zh-CN/01_seq.md b/packages/nimperf/docs/guide-src/zh-CN/01_seq.md new file mode 100644 index 0000000..f433f6b --- /dev/null +++ b/packages/nimperf/docs/guide-src/zh-CN/01_seq.md @@ -0,0 +1,5 @@ +## 1. 序列 + +已知长度或迭代次数时,优先 `newSeqOfCap` / `setLen` + 按索引写入。热循环里反复 `.add` 会重新分配。配套:**nm-seq-no-capacity**。 + +Lint: `nm-seq-add-hot`(默认 off,G3 后); `nm-seq-no-capacity`(empty seq + `.add` size-hinted; 默认 hint) diff --git a/packages/nimperf/docs/guide-src/zh-CN/02_strings.md b/packages/nimperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..4f6a862 --- /dev/null +++ b/packages/nimperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,3 @@ +## 2. 字符串 + +在 Nim 2.2.12、`-d:release` 下,对 20000 片做 `&=` 比 `newSeq` 再 `join` 更快(约 0.79×)。建议改用 `join` 的 lint 已移除。见 `evidence/_removed/nm-string-amp-in-loop.REMOVED.md`。 diff --git a/packages/nimperf/docs/guide-src/zh-CN/03_io.md b/packages/nimperf/docs/guide-src/zh-CN/03_io.md new file mode 100644 index 0000000..c29ba58 --- /dev/null +++ b/packages/nimperf/docs/guide-src/zh-CN/03_io.md @@ -0,0 +1,5 @@ +## 3. I/O + +不要把 `echo` 放进热循环;门控或缓冲。 + +Lint: `nm-echo-hot`(默认 off,G3 后) diff --git a/packages/nimperf/docs/guide-src/zh-CN/99_tail.md b/packages/nimperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..b266094 --- /dev/null +++ b/packages/nimperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,11 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| nm-seq-add-hot | 循环内 seq `.add` | off(G3) | +| nm-echo-hot | 循环内 `echo` | off(G3) | +| nm-seq-no-capacity | 空 seq + 有大小提示的 `.add` | hint | + +## 另见 + +规则 ID:[rule-ids.md](rule-ids.md)。证据策略:[evidence-policy.md](evidence-policy.md)。流程门槛:根目录 [AGENT.md](../../../../zh-CN/AGENT.md)。 diff --git a/packages/nimperf/docs/nim-performance-guide.md b/packages/nimperf/docs/nim-performance-guide.md index ac4e604..e153fb9 100644 --- a/packages/nimperf/docs/nim-performance-guide.md +++ b/packages/nimperf/docs/nim-performance-guide.md @@ -1,19 +1,32 @@ # Nim performance guide (v0) -Assembled from `docs/guide-src/`. Scaffold — not a measured handbook. Heuristic kit — measure before escalating lint to error. +Assembled from `docs/guide-src/`. Scaffold checklist — not a measured handbook. Measure before escalating lint to error. -## Sequences +> **Scope: Nim (heuristic kit).** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -Prefer `newSeqOfCap` / `setLen` + index writes when the length or iteration count is known. Repeated `.add` in hot loops reallocates. Companion: **nm-seq-no-capacity**. +## Contents -## Strings +- [1. Sequences](#1-sequences) +- [2. Strings](#2-strings) +- [3. I/O](#3-io) +- [Lint map](#lint-map) + +## 1. Sequences + +Prefer `newSeqOfCap` / `setLen` + index writes when the length or iteration count is known. Repeated `.add` in hot loops reallocates. + +Lint: `nm-seq-add-hot` (default off after G3); `nm-seq-no-capacity` (empty seq + `.add` size-hinted; default hint). + +## 2. Strings On Nim 2.2.12, `-d:release`, `&=` of 20000 pieces was faster than `newSeq` plus `join` (about 0.79×). The lint that suggested `join` was removed. See `evidence/_removed/nm-string-amp-in-loop.REMOVED.md`. -## IO +## 3. I/O Keep `echo` out of hot loops; gate or buffer. +Lint: `nm-echo-hot` (default off after G3). + ## Lint map | ID | Smell | Default | diff --git a/packages/nimperf/docs/zh-CN/corpus.md b/packages/nimperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..4e7993a --- /dev/null +++ b/packages/nimperf/docs/zh-CN/corpus.md @@ -0,0 +1,34 @@ +# nimperf 语料库目标 + +大型树木**不会**被供应,语料库也不会随仓库一起分发:将它克隆到仓库之外的任何本地目录,并将该路径传递给代码检查工具。 +使用以下命令进行代码检查: + +```sh +packages/nimperf/nimperf-lint/nimperf-lint \ + / --format json --no-fail +``` + +检查器是 **Nim**(`nimperf-lint`),同语言。需要在 PATH 上有 `nim`(choosenim)。**nm-seq-add-hot–nm-echo-hot 默认 `off`**;**nm-seq-no-capacity 提示**。 + +## 固定目标 + +| 树 | URL | 提交 | 备注 | +|------|-----|--------|-------| +| Nim | https://github.com/nim-lang/Nim | `58130d2` | 浅拷贝; 41M | +| Arraymancer | https://github.com/mratsim/Arraymancer | `dcc220c` | 浅拷贝; 7.0M | +| nimbus-eth2 | https://github.com/status-im/nimbus-eth2 | `963756c` | 浅克隆;25M | + +## 克隆的(2026-10-22)+ G3 重新设计 + +| Tree | r2 → r3 (defaults) | +|------|---------------------| +| **Nim** | 112 → **39** | +| **Arraymancer** | 17 → **2** | +| **nimbus-eth2** | 41 → **171** | + +### 重新设计 + +- nm-seq-no-capacity:在有大小提示的循环中对空序列使用 `.add`;推荐使用 `newSeqOfCap` +- 跳过 `compiler/`;在 OK-idiom 占主导后,将 nm-seq-add-hot–nm-echo-hot 降级 + +不要将语料库来源提交到 `packages/nimperf`。 diff --git a/packages/nimperf/docs/zh-CN/evidence-policy.md b/packages/nimperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..fecb8b4 --- /dev/null +++ b/packages/nimperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,17 @@ +# nimperf 证据政策 + +## 门槛 + +一条规则只有在同时存在 `evidence/.md` 和测量的 `evidence/.jsonl` 时才可以是 **`error`**。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| nm-seq-add-hot | **关闭** | Nim 2.2.12 ~**8.97×** | 循环中的 seq `.add`(G3 OK-惯用法) | +| nm-echo-hot | **关闭** | Nim 2.2.12 ~**1670×**(在 0.003 毫秒后)| 循环中的 `echo`(G3 CLI)| +| nm-seq-no-capacity | 提示 | Nim 2.2.12 ~**1.42×** | 在有大小提示的循环中对空序列使用 `.add` | + +没有测量的 JSONL — nm-seq-no-capacity 保持 **提示**。`nm-string-amp-in-loop` 已被移除:在 Nim 2.2.12 上 `join` 比 `&=` 慢(大约 0.79 倍,之前的一次运行大约 0.69 倍)。见 `evidence/_removed/`。 diff --git a/packages/nimperf/docs/zh-CN/nim-performance-guide.md b/packages/nimperf/docs/zh-CN/nim-performance-guide.md new file mode 100644 index 0000000..b09c86d --- /dev/null +++ b/packages/nimperf/docs/zh-CN/nim-performance-guide.md @@ -0,0 +1,37 @@ +# Nim 性能指南(v0) + +由 `docs/guide-src/` 组装。脚手架核对清单——不是有测量支撑的手册。把 lint 升为 error 前请先测量。 + +> **范围:Nim(启发式 kit)。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +## 目录 + +- [1. 序列](#1-序列) +- [2. 字符串](#2-字符串) +- [3. I/O](#3-io) +- [Lint 对照](#lint-对照) + +## 1. 序列 + +已知长度或迭代次数时,优先 `newSeqOfCap` / `setLen` + 按索引写入。热循环里反复 `.add` 会重新分配。配套:**nm-seq-no-capacity**。 + +Lint: `nm-seq-add-hot`(默认 off,G3 后); `nm-seq-no-capacity`(empty seq + `.add` size-hinted; 默认 hint) +## 2. 字符串 + +在 Nim 2.2.12、`-d:release` 下,对 20000 片做 `&=` 比 `newSeq` 再 `join` 更快(约 0.79×)。建议改用 `join` 的 lint 已移除。见 `evidence/_removed/nm-string-amp-in-loop.REMOVED.md`。 +## 3. I/O + +不要把 `echo` 放进热循环;门控或缓冲。 + +Lint: `nm-echo-hot`(默认 off,G3 后) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| nm-seq-add-hot | 循环内 seq `.add` | off(G3) | +| nm-echo-hot | 循环内 `echo` | off(G3) | +| nm-seq-no-capacity | 空 seq + 有大小提示的 `.add` | hint | + +## 另见 + +规则 ID:[rule-ids.md](rule-ids.md)。证据策略:[evidence-policy.md](evidence-policy.md)。流程门槛:根目录 [AGENT.md](../../../../zh-CN/AGENT.md)。 diff --git a/packages/nimperf/docs/zh-CN/rule-ids.md b/packages/nimperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..2f37f9e --- /dev/null +++ b/packages/nimperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,22 @@ +# nimperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `nm-seq-add-hot` | +| `nm-echo-hot` | +| `nm-seq-no-capacity` | + +Removed: `nm-string-amp-in-loop` — see `evidence/_removed/`. + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `nm-echo-in-loop` | `nm-echo-hot` | +| `nm-seq-add-in-loop` | `nm-seq-add-hot` | diff --git a/packages/nimperf/evidence/zh-CN/README.md b/packages/nimperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..7152ab9 --- /dev/null +++ b/packages/nimperf/evidence/zh-CN/README.md @@ -0,0 +1,4 @@ +# nimperf 证据 + +仅限脚手架存根。JSONL仅在经过前后运行测量后出现在此处(根目录 [AGENT.md](../../../../zh-CN/AGENT.md) 的 G4)。 +切勿编造比例。`nm-string-amp-in-loop` 已被移除;参见 `_removed/`。 diff --git a/packages/nimperf/evidence/zh-CN/nm-echo-hot.md b/packages/nimperf/evidence/zh-CN/nm-echo-hot.md new file mode 100644 index 0000000..5e8a786 --- /dev/null +++ b/packages/nimperf/evidence/zh-CN/nm-echo-hot.md @@ -0,0 +1,16 @@ +# nm-echo-hot — 循环回声 + +## 状态 + +**在** Nim 2.2.12 `-d:release` 上进行测量,标准输出指向 `/dev/null`。之后的样本为 0.003 毫秒,所以比率处于定时器底部。默认严重性保持**关闭**。 + +## 运行时 + +| 字段 | 值 | +|-------|--------| +| When | 2026-10-23 UTC | +| 在 `echo_in_loop` 之前 | 5.011 毫秒 (操作=10000) | +| 在 `echo_once` 之后 | 0.003 毫秒 | +| **前/后比例** | **~1670×** | + +线束:`examples/bench/evidence_nm.nim`。配对夹具保持在`nimperf-lint/fixtures/`下。 diff --git a/packages/nimperf/evidence/zh-CN/nm-seq-add-hot.md b/packages/nimperf/evidence/zh-CN/nm-seq-add-hot.md new file mode 100644 index 0000000..169c992 --- /dev/null +++ b/packages/nimperf/evidence/zh-CN/nm-seq-add-hot.md @@ -0,0 +1,16 @@ +# nm-seq-add-hot — 循环中添加序列 + +## 状态 + +**在 Nim 2.2.12 `-d:release` 上测量**。默认严重性保持 **关闭**。 + +## 运行时 + +| 字段 | 值 | +|-------|--------| +| When | 2026-10-23 UTC | +| 在 `seq_add` 之前 | 0.565 毫秒 (操作=200000) | +| 在 `index_assign` 之后 | 0.063 毫秒 | +| **前/后比例** | **~8.97×** | + +线束:`examples/bench/evidence_nm.nim`。配对夹具保持在`nimperf-lint/fixtures/`下。 diff --git a/packages/nimperf/evidence/zh-CN/nm-seq-no-capacity.md b/packages/nimperf/evidence/zh-CN/nm-seq-no-capacity.md new file mode 100644 index 0000000..c8cfc1a --- /dev/null +++ b/packages/nimperf/evidence/zh-CN/nm-seq-no-capacity.md @@ -0,0 +1,16 @@ +# nm-seq-no-capacity — 用 add 培养的空序列 + +## 状态 + +**在 Nim 2.2.12 `-d:release` 上测量**。默认严重性保持为 **提示**。 + +## 运行时 + +| 字段 | 值 | +|-------|--------| +| When | 2026-10-23 UTC | +| 在 `empty_add` 之前 | 0.699 毫秒 (操作=200000) | +| 在 `newSeqOfCap` 之后 | 0.493 毫秒 | +| **前后比例** | **~1.42×** | + +线束:`examples/bench/evidence_nm.nim`。配对夹具保持在`nimperf-lint/fixtures/`下。 diff --git a/packages/nimperf/zh-CN/CONTRIBUTING.md b/packages/nimperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/nimperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/nimperf/zh-CN/README.md b/packages/nimperf/zh-CN/README.md new file mode 100644 index 0000000..fcb303f --- /dev/null +++ b/packages/nimperf/zh-CN/README.md @@ -0,0 +1,29 @@ +# nimperf + +Nim 性能注意事项和同语言的 `nimperf-lint`。 + +[XPerf](../../../zh-CN/README.md) 的一部分。**语料准备就绪**(G3 2026-10-22)。默认设置:nm-seq-add-hot 和 nm-echo-hot **关闭**;nm-seq-no-capacity **提示**。`nm-string-amp-in-loop` 已移除。需要 `nim`(choosenim)。 + +## 目录 + +- [英文指南 (v0)](../docs/zh-CN/nim-performance-guide.md) — 由 `docs/guide-src/` 生成 +- [`nimperf-lint`](../nimperf-lint/README.md) — 规则和严重性 +- [规则ID](../docs/zh-CN/rule-ids.md) · [证据政策](../docs/zh-CN/evidence-policy.md) · [语料库固定](../docs/zh-CN/corpus.md) + +## 运行 lint + +```sh +packages/nimperf/nimperf-lint/nimperf-lint path/to/sources +bash packages/nimperf/scripts/smoke-lint.sh +bash packages/nimperf/scripts/self-lint.sh +``` + +## 布局 + +| 路径 | 内容 | +|------|----------| +| `docs/` | 指南(`guide-src/` 是源文件)、语料库固定、证据政策、规则 ID | +| `evidence/` | 每条规则的证据笔记(全部待定;参见 [evidence/README.md](../evidence/zh-CN/README.md)) | +| `examples/` | `m01_presize.nim`,`p01_join.nim` | +| `nimperf-lint/` | linter 源代码、测试夹具、README | +| `scripts/` | `smoke-lint.sh`,`self-lint.sh` | diff --git a/packages/objcperf/docs/guide-src/00_head.md b/packages/objcperf/docs/guide-src/00_head.md index ed50562..00ba13f 100644 --- a/packages/objcperf/docs/guide-src/00_head.md +++ b/packages/objcperf/docs/guide-src/00_head.md @@ -1,17 +1,13 @@ # Objective-C performance guide (v0) -Foundation collection / string / logging checklists. Distinct from **swiftperf**. +Foundation collection, string, and logging checklists. Distinct from [swiftperf](../../swiftperf/). -> **Scope: Objective-C (ARC or MRC).** Lint is a line-window scan of `.m`/`.h`. Measure on device/macOS before escalating. +> **Scope: Objective-C (ARC or MRC).** Lint is a line-window scan of `.m`/`.h`. Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (Instruments Time Profiler / Allocations). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (Instruments Time Profiler / Allocations). Measure on device or macOS before escalating. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Collections](#1-collections) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [3. Formatters, JSON, and regular expressions](#3-formatters-json-and-regular-expressions) diff --git a/packages/objcperf/docs/guide-src/02_strings.md b/packages/objcperf/docs/guide-src/02_strings.md index d72d9db..f4cd8aa 100644 --- a/packages/objcperf/docs/guide-src/02_strings.md +++ b/packages/objcperf/docs/guide-src/02_strings.md @@ -2,5 +2,5 @@ `stringByAppendingString:` in a loop allocates many immutable strings. Prefer `NSMutableString`. Keep `NSLog` out of tight loops (gate or use os_log carefully). -Companion lint: **oc-nsstring-append-hot**, **oc-nslog-hot**. +Lint: `oc-nsstring-append-hot`, `oc-nslog-hot` (default off). diff --git a/packages/objcperf/docs/guide-src/03_formatters.md b/packages/objcperf/docs/guide-src/03_formatters.md index df93b58..dd25fa6 100644 --- a/packages/objcperf/docs/guide-src/03_formatters.md +++ b/packages/objcperf/docs/guide-src/03_formatters.md @@ -1,6 +1,6 @@ ## 3. Formatters, JSON, and regular expressions -`[[NSDateFormatter alloc] init]`, `[NSDateFormatter new]`, `NSJSONSerialization` (`JSONObjectWithData` / `dataWithJSONObject`), and `NSRegularExpression` (`regularExpressionWithPattern`) inside a loop repeat that setup on every iteration. Create the formatter or expression once, and parse or encode a payload once when the data allows it. These lints are **hint**. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. They do not revive the removed array-capacity rules. +`[[NSDateFormatter alloc] init]`, `[NSDateFormatter new]`, `NSJSONSerialization` (`JSONObjectWithData` / `dataWithJSONObject`), and `NSRegularExpression` (`regularExpressionWithPattern`) inside a loop repeat that setup on every iteration. Create the formatter or expression once, and parse or encode a payload once when the data allows it. These lints are hint. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. They do not revive the removed array-capacity rules. `stringWithContentsOfFile:` of one path was about 450× versus reading once. A path that changes every iteration cannot be hoisted. @@ -8,5 +8,5 @@ `valueForKey:` every iteration was about 124× versus reading the field. A name that is not a fixed field cannot be rewritten as a direct read. This is the same family as Go `FieldByName`. -Companion lint: **oc-dateformatter-hot**, **oc-json-hot**, **oc-regex-hot**, **oc-file-read-hot**, **oc-clock-hot**, **oc-value-for-key-hot**. +Lint: `oc-dateformatter-hot`, `oc-json-hot`, `oc-regex-hot`, `oc-file-read-hot`, `oc-clock-hot`, `oc-value-for-key-hot` (default hint). diff --git a/packages/objcperf/docs/guide-src/zh-CN/00_head.md b/packages/objcperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..c5f2902 --- /dev/null +++ b/packages/objcperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Objective-C 性能指南(v0) + +Foundation 集合、字符串与日志核对清单。与 [swiftperf](../../../swiftperf/) 不同。 + +> **范围:Objective-C(ARC 或 MRC)。** lint 是对 `.m`/`.h` 的行窗扫描。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(Instruments Time Profiler / Allocations)。升为更高严重级别前,请在真机或 macOS 上测量。 + +## 目录 + +- [1. 集合](#1-集合) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [3. Formatter、JSON 与正则](#3-formatterjson-与正则) +- [Lint 对照](#lint-对照) + diff --git a/packages/objcperf/docs/guide-src/zh-CN/01_collections.md b/packages/objcperf/docs/guide-src/zh-CN/01_collections.md new file mode 100644 index 0000000..7f977c3 --- /dev/null +++ b/packages/objcperf/docs/guide-src/zh-CN/01_collections.md @@ -0,0 +1,3 @@ +## 1. 集合 + +`[[NSMutableArray alloc] init]` 再多次 `addObject:` 可能反复扩容。已知 `n` 时优先 `arrayWithCapacity:`。没有 lint 覆盖:抖动受控复测未显示可靠加速后,容量规则已移除。 diff --git a/packages/objcperf/docs/guide-src/zh-CN/02_strings.md b/packages/objcperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..ded5c39 --- /dev/null +++ b/packages/objcperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串与热 I/O + +循环里的 `stringByAppendingString:` 会分配许多不可变字符串。优先 `NSMutableString`。不要把 `NSLog` 放进紧密循环(门控,或谨慎使用 os_log)。 + +Lint: `oc-nsstring-append-hot`, `oc-nslog-hot`(默认 off) diff --git a/packages/objcperf/docs/guide-src/zh-CN/03_formatters.md b/packages/objcperf/docs/guide-src/zh-CN/03_formatters.md new file mode 100644 index 0000000..e0d6339 --- /dev/null +++ b/packages/objcperf/docs/guide-src/zh-CN/03_formatters.md @@ -0,0 +1,11 @@ +## 3. Formatter、JSON 与正则 + +循环里的 `[[NSDateFormatter alloc] init]`、`[NSDateFormatter new]`、`NSJSONSerialization`(`JSONObjectWithData` / `dataWithJSONObject`)以及 `NSRegularExpression`(`regularExpressionWithPattern`)每轮重复准备。formatter 或表达式只建一次;数据允许时,payload 只解析或编码一次。这些 lint 是 hint。各自有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。它们不会复活已移除的数组容量规则。 + +对同一路径 `stringWithContentsOfFile:` 约 450×(相对读一次)。每轮都变的路径不能外提。 + +每轮 `[NSDate date]` 相对采一次样约 769×。需要新鲜时间戳的循环没有可外提的东西。`NSDateFormatter` 仍是独立规则。 + +每轮 `valueForKey:` 相对读字段约 124×。不是固定字段的名字不能改写成直接读取。这与 Go 的 `FieldByName` 同族。 + +Lint: `oc-dateformatter-hot`, `oc-json-hot`, `oc-regex-hot`, `oc-file-read-hot`, `oc-clock-hot`, `oc-value-for-key-hot`(默认 hint) diff --git a/packages/objcperf/docs/guide-src/zh-CN/99_tail.md b/packages/objcperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..2ddd0ec --- /dev/null +++ b/packages/objcperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,16 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| oc-nsstring-append-hot | 循环内 stringByAppendingString | off | +| oc-nslog-hot | 循环内 NSLog | off | +| oc-dateformatter-hot | 循环内 date formatter alloc/init 或 new | hint(有 JSONL;未 corpus 分级) | +| oc-json-hot | 循环内 JSON 解析或编码 | hint(有 JSONL;未 corpus 分级) | +| oc-regex-hot | 循环内编译正则 | hint(有 JSONL;未 corpus 分级) | + +仅在有 `evidence/oc-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。两条数组容量规则(OC001、OC004)已移除;见 [rule-ids.md](rule-ids.md)。`oc-dateformatter-hot`、`oc-json-hot`、`oc-regex-hot` 有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [swiftperf](../../../swiftperf/)(同级) diff --git a/packages/objcperf/docs/objc-performance-guide.md b/packages/objcperf/docs/objc-performance-guide.md index 7f2a29d..5cbb265 100644 --- a/packages/objcperf/docs/objc-performance-guide.md +++ b/packages/objcperf/docs/objc-performance-guide.md @@ -1,17 +1,13 @@ # Objective-C performance guide (v0) -Foundation collection / string / logging checklists. Distinct from **swiftperf**. +Foundation collection, string, and logging checklists. Distinct from [swiftperf](../../swiftperf/). -> **Scope: Objective-C (ARC or MRC).** Lint is a line-window scan of `.m`/`.h`. Measure on device/macOS before escalating. +> **Scope: Objective-C (ARC or MRC).** Lint is a line-window scan of `.m`/`.h`. Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (Instruments Time Profiler / Allocations). -2. Treat each chapter as a checklist, not a rewrite mandate. +Profile first (Instruments Time Profiler / Allocations). Measure on device or macOS before escalating. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Collections](#1-collections) - [2. Strings and hot I/O](#2-strings-and-hot-io) - [3. Formatters, JSON, and regular expressions](#3-formatters-json-and-regular-expressions) @@ -25,11 +21,11 @@ Foundation collection / string / logging checklists. Distinct from **swiftperf** `stringByAppendingString:` in a loop allocates many immutable strings. Prefer `NSMutableString`. Keep `NSLog` out of tight loops (gate or use os_log carefully). -Companion lint: **oc-nsstring-append-hot**, **oc-nslog-hot**. +Lint: `oc-nsstring-append-hot`, `oc-nslog-hot` (default off). ## 3. Formatters, JSON, and regular expressions -`[[NSDateFormatter alloc] init]`, `[NSDateFormatter new]`, `NSJSONSerialization` (`JSONObjectWithData` / `dataWithJSONObject`), and `NSRegularExpression` (`regularExpressionWithPattern`) inside a loop repeat that setup on every iteration. Create the formatter or expression once, and parse or encode a payload once when the data allows it. These lints are **hint**. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. They do not revive the removed array-capacity rules. +`[[NSDateFormatter alloc] init]`, `[NSDateFormatter new]`, `NSJSONSerialization` (`JSONObjectWithData` / `dataWithJSONObject`), and `NSRegularExpression` (`regularExpressionWithPattern`) inside a loop repeat that setup on every iteration. Create the formatter or expression once, and parse or encode a payload once when the data allows it. These lints are hint. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. They do not revive the removed array-capacity rules. `stringWithContentsOfFile:` of one path was about 450× versus reading once. A path that changes every iteration cannot be hoisted. @@ -37,7 +33,7 @@ Companion lint: **oc-nsstring-append-hot**, **oc-nslog-hot**. `valueForKey:` every iteration was about 124× versus reading the field. A name that is not a fixed field cannot be rewritten as a direct read. This is the same family as Go `FieldByName`. -Companion lint: **oc-dateformatter-hot**, **oc-json-hot**, **oc-regex-hot**, **oc-file-read-hot**, **oc-clock-hot**, **oc-value-for-key-hot**. +Lint: `oc-dateformatter-hot`, `oc-json-hot`, `oc-regex-hot`, `oc-file-read-hot`, `oc-clock-hot`, `oc-value-for-key-hot` (default hint). ## Lint map diff --git a/packages/objcperf/docs/zh-CN/corpus.md b/packages/objcperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..a2dac0f --- /dev/null +++ b/packages/objcperf/docs/zh-CN/corpus.md @@ -0,0 +1,72 @@ +# objcperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +| 树 | 链接 | 提交 | 大小 | Lint 范围 | +|------|-----|--------|------|------------| +| AFNetworking | https://github.com/AFNetworking/AFNetworking.git | `d9f589cc2c1fe9d55eb5eea00558010afea7a41e` | 约3.5M | 整个仓库 | +| SDWebImage | https://github.com/SDWebImage/SDWebImage.git | `c3ad5e1a9bf55c9b76d4c362430b5fcded96c502` | ~45M | 整个仓库 | +| fmdb | https://github.com/ccgus/fmdb.git | `d3abf748a2788471535993286603322ccdd02c3d` | ~1.1M | 整棵树 | + +浅克隆(`--depth 1`)。纯 ObjC 树(没有仅限 Swift 的 pods)。 + +## 狗粮 (2026-10-21 亚洲/上海) + +| 树 | 原始 (`--raw-paths`) | 反向优化后 | +|------|---------------------|-------------------| +| AFNetworking | **7** (OC001:7) | **4** (4/0/0) | +| SDWebImage | **9** (8/0/1) | **1** (1/0/0) | +| fmdb | **28** (1/1/26) | **10** (1/1/8) | + +规则顺序:OC001 / oc-nsstring-append-hot / oc-nslog-hot。 + +### 反向优化(`objcperf-lint`) + +1. **`--raw-paths`:** 基线开关。 +2. **路径跳过:** `Tests/` `tests/` `Test/` `Example/` `Examples/` `Demo/` `sample/` `Sample/` `docs/` `Specs/` `benchmarks/` (另外总是跳过 `.git`/`Pods`/`Carthage`/`vendor`/`fixtures`)。 +3. **OC001:** 仅匹配 `addObject:` / `addObject]` — **不**匹配 `addObjectsFromArray`(子字符串误报)。 +4. **oc-nslog-hot:** 跳过 `DEBUG`/`NDEBUG` 令牌以及 Debug/Logger 路径名。 + +### 残差 + +- AFNetworking 查询/证书集合 `addObject:` 无附近容量(比较真实的情况)。 +- fmdb `FMDatabase.m` 在绑定/关闭路径中的诊断 `NSLog`(库调试;保留)。 +- fmdb 中一个 `stringByAppendingString` 版本归一化。 + +不要将语料库来源提交到 `packages/objcperf`。 + +## G3 TP/FP 分诊 (2026-10-22 亚洲/上海) + +| | r1 n=15 | r2 n=9 | +|--|---------|--------| +| TP | 13.3% | 22.2% | +| FP | 20.0% | 0% | +| OK-idiom | 66.7% | **77.8%** | +| **FP+DOC+SKIP** | **20.0%** | **0%** | + +**准备好语料库了吗?`否`** — 修复 `setWithCapacity` 漏报后,OK-idiom 占主导。轻微的真阳性仍然存在(查询/证书数组没有容量)。保留 **提示**。 + +### 修复已完成 +- OC001 识别 `setWithCapacity` / `dictionaryWithCapacity` +- oc-nslog-hot 跳过附近的 `DEBUG` / `_traceExecution` +- r2 计数:AFNetworking **2**,SDWebImage **1**,fmdb **6** + +## 重新设计 → 准备语料库(2026-10-22 亚洲/上海) + +| 树 | r2 | r3(默认) | 准备好语料库? | +|------|----|---------------|---------------| +| AFNetworking | 2 | **2** | | +| SDWebImage | 1 | **1** | | +| fmdb | 6 | **0** | | + +**判决:语料库就绪 `是`。** 样本 n=3:TP 66.7% / OK-成语 33.3% / FP+DOC+跳过 0%。当时的默认设置:OC001–oc-nslog-hot **关闭**;**OC004** **提示**。启发式路径 — G0–G3 不需要 Apple SDK(在这台机器上 OC001 G4 仍然在没有 `Foundation.h` 的情况下被阻止)。 + +OC001 和 OC004 随后被移除(见 [rule-ids.md](rule-ids.md))。在那时,lint 仅包含 `oc-nsstring-append-hot` 和 `oc-nslog-hot`,两者默认均为**关闭**。 + +## 2026-10-23 — 新提示规则,未分类 + +下面的 slug 是 **提示**。每一个都有一个 JSONL 微基准测试,并且尚未经过 G2/G3。2026-10-22 可用于语料库的裁定不包括它们。 + +- `oc-dateformatter-hot`,`oc-json-hot`,`oc-regex-hot` diff --git a/packages/objcperf/docs/zh-CN/evidence-policy.md b/packages/objcperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..35e95a9 --- /dev/null +++ b/packages/objcperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,22 @@ +# objcperf 证据政策 + +## 门槛 + +一条规则只有在有 `evidence/.md` + `.jsonl` **以及** 明确的政策决策时,才能被视为 **`error`**。仅测量的 JSONL 本身 **不会** 自动升级。2026-10-22 的 G4 填充保持了软件包默认值。 + +## 当前映射 + +截至2026-10-23。测量的行是2026-10-22的G4填充。三个新的分类标签是 hint、pending 和 not corpus-triaged。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| oc-nsstring-append-hot | 关闭 | **是** GNUstep ~569× | NSString 附加 | +| oc-nslog-hot | 关闭 | **是** fprintf 代理 ~191× | 循环记录 | +| oc-dateformatter-hot | 提示 | 已测量(`oc-dateformatter-hot.md` + `oc-dateformatter-hot.jsonl`);保持提示 | 循环中初始化/分配或新建日期格式化器;未进行语料库筛选 | +| oc-json-hot | 提示 | 测量(`oc-json-hot.md` + `oc-json-hot.jsonl`);保持提示 | 在循环中解析或编码 JSON;未进行语料库筛选 | +| oc-regex-hot | 提示 | 测量(`oc-regex-hot.md` + `oc-regex-hot.jsonl`);保持提示 | 在循环中编译的正则表达式;未经过语料库筛选 | +| oc-file-read-hot | 提示 | 测量(`oc-file-read-hot.md` + `oc-file-read-hot.jsonl`);保持提示 | 循环中的 `stringWithContentsOfFile`;仅限相同路径;未进行语料库分类 | +| oc-clock-hot | 提示 | 测量 (`oc-clock-hot.md` + `oc-clock-hot.jsonl`); 保持提示 | 循环中的 `[NSDate date]`; 未经语料库分类 | +| oc-value-for-key-hot | 提示 | 已测量 (`oc-value-for-key-hot.md` + `oc-value-for-key-hot.jsonl`); 保持提示 | 循环中的 `valueForKey:`;未经过语料库筛选 | +| OC001 | 已移除 | 墓碑 | 见 `evidence/_removed/` | +| OC004 | 已移除 | 墓碑 | 见 `evidence/_removed/` | diff --git a/packages/objcperf/docs/zh-CN/objc-performance-guide.md b/packages/objcperf/docs/zh-CN/objc-performance-guide.md new file mode 100644 index 0000000..4555118 --- /dev/null +++ b/packages/objcperf/docs/zh-CN/objc-performance-guide.md @@ -0,0 +1,50 @@ +# Objective-C 性能指南(v0) + +Foundation 集合、字符串与日志核对清单。与 [swiftperf](../../../swiftperf/) 不同。 + +> **范围:Objective-C(ARC 或 MRC)。** lint 是对 `.m`/`.h` 的行窗扫描。加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(Instruments Time Profiler / Allocations)。升为更高严重级别前,请在真机或 macOS 上测量。 + +## 目录 + +- [1. 集合](#1-集合) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [3. Formatter、JSON 与正则](#3-formatterjson-与正则) +- [Lint 对照](#lint-对照) + +## 1. 集合 + +`[[NSMutableArray alloc] init]` 再多次 `addObject:` 可能反复扩容。已知 `n` 时优先 `arrayWithCapacity:`。没有 lint 覆盖:抖动受控复测未显示可靠加速后,容量规则已移除。 +## 2. 字符串与热 I/O + +循环里的 `stringByAppendingString:` 会分配许多不可变字符串。优先 `NSMutableString`。不要把 `NSLog` 放进紧密循环(门控,或谨慎使用 os_log)。 + +Lint: `oc-nsstring-append-hot`, `oc-nslog-hot`(默认 off) +## 3. Formatter、JSON 与正则 + +循环里的 `[[NSDateFormatter alloc] init]`、`[NSDateFormatter new]`、`NSJSONSerialization`(`JSONObjectWithData` / `dataWithJSONObject`)以及 `NSRegularExpression`(`regularExpressionWithPattern`)每轮重复准备。formatter 或表达式只建一次;数据允许时,payload 只解析或编码一次。这些 lint 是 hint。各自有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。它们不会复活已移除的数组容量规则。 + +对同一路径 `stringWithContentsOfFile:` 约 450×(相对读一次)。每轮都变的路径不能外提。 + +每轮 `[NSDate date]` 相对采一次样约 769×。需要新鲜时间戳的循环没有可外提的东西。`NSDateFormatter` 仍是独立规则。 + +每轮 `valueForKey:` 相对读字段约 124×。不是固定字段的名字不能改写成直接读取。这与 Go 的 `FieldByName` 同族。 + +Lint: `oc-dateformatter-hot`, `oc-json-hot`, `oc-regex-hot`, `oc-file-read-hot`, `oc-clock-hot`, `oc-value-for-key-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| oc-nsstring-append-hot | 循环内 stringByAppendingString | off | +| oc-nslog-hot | 循环内 NSLog | off | +| oc-dateformatter-hot | 循环内 date formatter alloc/init 或 new | hint(有 JSONL;未 corpus 分级) | +| oc-json-hot | 循环内 JSON 解析或编码 | hint(有 JSONL;未 corpus 分级) | +| oc-regex-hot | 循环内编译正则 | hint(有 JSONL;未 corpus 分级) | + +仅在有 `evidence/oc-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。两条数组容量规则(OC001、OC004)已移除;见 [rule-ids.md](rule-ids.md)。`oc-dateformatter-hot`、`oc-json-hot`、`oc-regex-hot` 有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) +- [swiftperf](../../../swiftperf/)(同级) diff --git a/packages/objcperf/docs/zh-CN/rule-ids.md b/packages/objcperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..e66c81e --- /dev/null +++ b/packages/objcperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,36 @@ +# objcperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `oc-nsstring-append-hot` | +| `oc-nslog-hot` | +| `oc-dateformatter-hot` | +| `oc-json-hot` | +| `oc-regex-hot` | +| `oc-file-read-hot` | +| `oc-clock-hot` | +| `oc-value-for-key-hot` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `oc-clock-in-loop` | `oc-clock-hot` | +| `oc-dateformatter-in-loop` | `oc-dateformatter-hot` | +| `oc-file-read-in-loop` | `oc-file-read-hot` | +| `oc-json-in-loop` | `oc-json-hot` | +| `oc-nslog-in-loop` | `oc-nslog-hot` | +| `oc-nsstring-append-in-loop` | `oc-nsstring-append-hot` | +| `oc-regex-in-loop` | `oc-regex-hot` | +| `oc-value-for-key-in-loop` | `oc-value-for-key-hot` | + +## Removed (tombstone; never reuse) + +- `OC001` — see `evidence/_removed/` +- `OC004` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/objcperf/evidence/zh-CN/README.md b/packages/objcperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..479ebe2 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/README.md @@ -0,0 +1,10 @@ +# objcperf 证据 + +测量于 2026-10-22(亚洲/上海)在 **GNUstep** 上,而不是苹果运行环境上。 + +- **oc-nsstring-append-hot** — 已测量(`oc-nsstring-append-hot.md` + `oc-nsstring-append-hot.jsonl`);默认保持 **关闭**。 +- **oc-nslog-hot** — 使用 `fprintf` 代理测量(`oc-nslog-hot.md` + `oc-nslog-hot.jsonl`);默认保持 **关闭**。 +- **oc-dateformatter-hot**、**oc-json-hot**、**oc-regex-hot** — 测量 JSONL,默认 **提示**;未进行语料库筛选。有关比率和形状限制,请参见各自的 md 文件。 +- `OC001` 和 `OC004` 已删除;墓碑文件位于 `_removed/` 下。 + +挂载点:`examples/bench/evidence_oc.m`。策略:[`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md)。根矩阵:[`../../../docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/objcperf/evidence/zh-CN/oc-clock-hot.md b/packages/objcperf/evidence/zh-CN/oc-clock-hot.md new file mode 100644 index 0000000..0a4c95e --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-clock-hot.md @@ -0,0 +1,17 @@ +# oc-clock-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[oc-clock-hot.jsonl](../oc-clock-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`[NSDate date]` 每次迭代与一个样本。结果被保留,所以循环结束后不会被删除。`NSDateFormatter` 是一个不同的规则。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `sample_once` | 0.004 | 100000 | 1 | +| 之前 | `sample_each_iter` | 3.098 | 100000 | 769.061 | + +在采样一次之后,并且不会在循环内部构建日期。需要每次迭代都获取新时间戳的代码没有可提升的部分。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在进行任何默认更改之前,仍需进行语料库筛选。 diff --git a/packages/objcperf/evidence/zh-CN/oc-dateformatter-hot.md b/packages/objcperf/evidence/zh-CN/oc-dateformatter-hot.md new file mode 100644 index 0000000..92b6ff9 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-dateformatter-hot.md @@ -0,0 +1,20 @@ +# oc-dateformatter-hot — 已测量,仍有提示 + +## 状态 + +**提示**。实时时钟 JSONL:[oc-dateformatter-hot.jsonl](../oc-dateformatter-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识符。 + +## 测量 + +一种日期的2000种格式。之前在每次迭代中分配NSDateFormatter;之后重复使用一个。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `formatter_once` | 1.655000 | 2000 | 1 | +| 之前 | `formatter_each_iter` | 50.273000 | 2000 | 30.376 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/objcperf/evidence/zh-CN/oc-file-read-hot.md b/packages/objcperf/evidence/zh-CN/oc-file-read-hot.md new file mode 100644 index 0000000..9030ebc --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-file-read-hot.md @@ -0,0 +1,17 @@ +# oc-file-read-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[oc-file-read-hot.jsonl](../oc-file-read-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +一个单字节文件的 `stringWithContentsOfFile:`,读 500 次,与读一次相比。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 后 | `read_once` | 0.007 | 500 | 1 | +| 前 | `read_each_iter` | 3.184 | 500 | 449.707 | + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅凭此文件将默认值设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍需进行语料库分流。 diff --git a/packages/objcperf/evidence/zh-CN/oc-json-hot.md b/packages/objcperf/evidence/zh-CN/oc-json-hot.md new file mode 100644 index 0000000..90578c6 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-json-hot.md @@ -0,0 +1,20 @@ +# oc-json-hot — 稳定测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[oc-json-hot.jsonl](../oc-json-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +对一个固定的 JSON 对象进行 20000 次解析。解析一次之后。变化的负载在此结构之后不能使用。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `json_once` | 0.997000 | 20000 | 1 | +| 之前 | `json_each_iter` | 15.723000 | 20000 | 15.770 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/objcperf/evidence/zh-CN/oc-nslog-hot.md b/packages/objcperf/evidence/zh-CN/oc-nslog-hot.md new file mode 100644 index 0000000..bc61533 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-nslog-hot.md @@ -0,0 +1,31 @@ +# oc-nslog-hot — 循环中的 fprintf/NSLog 类似功能 + +**状态:** 已测量(gnustep 代理) + +## 主张 + +每次迭代将 `fprintf(stderr,…)` 输出到 `/dev/null` 与只执行一次大约 ~**191×**(作为 NSLog 噪音的代理)。 + +## 如何测量 + +- 工具:`packages/objcperf/examples/bench/evidence_oc.m` +- N=10000 +- 原始行:[`oc-nslog-hot.jsonl`](../oc-nslog-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| runtime | **gnustep** | +| 循环前中位数 (`fprintf_in_loop`) | ~0.6143 毫秒 | +| (`log_once`) 后的中位数 | ~0.003217 毫秒 | +| **前/后比率** | **~191×** | + +## 与 lint 的对应 + +`objcperf-lint` oc-nslog-hot:默认保持 **`关闭`**。 + +## 限制 + +- 不是真正的 NSLog;I/O 代理。默认保持**关闭**。 diff --git a/packages/objcperf/evidence/zh-CN/oc-nsstring-append-hot.md b/packages/objcperf/evidence/zh-CN/oc-nsstring-append-hot.md new file mode 100644 index 0000000..781a458 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-nsstring-append-hot.md @@ -0,0 +1,31 @@ +# oc-nsstring-append-hot — NSString 追加 vs NSMutableString + +**状态:** 已测量 (gnustep) + +## 主张 + +GNUstep:循环中的 `stringByAppendingFormat` 与 `NSMutableString` ~**569×** 在 N=15000 时。 + +## 如何测量 + +- 测试工具:`packages/objcperf/examples/bench/evidence_oc.m` +- 原始行:[`oc-nsstring-append-hot.jsonl`](../oc-nsstring-append-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海 (JSONL `ts` 为 UTC) | +| runtime | **gnustep** | +| `stringByAppending` 之前的中位数 | ~1236 毫秒 | +| (`mutable_string`) 之后的中位数 | ~2.172 毫秒 | +| **前/后比率** | **~569×** | + +## 与 lint 的对应 + +`objcperf-lint` oc-nsstring-append-hot:默认保持 **`关闭`**。 + +## 限制 + +- 绳索时间`弦-弦-弦-弦-弦-弦-弦-弦-弦-弦-弦-弦-弦 ' ; +- 强于GNUstep;苹果运行时间可能有所不同. 默认不发生****。 diff --git a/packages/objcperf/evidence/zh-CN/oc-regex-hot.md b/packages/objcperf/evidence/zh-CN/oc-regex-hot.md new file mode 100644 index 0000000..3e8055b --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-regex-hot.md @@ -0,0 +1,20 @@ +# oc-regex-hot — 测量过的,仍然提示 + +## 状态 + +**提示**。实时时钟 JSONL:[oc-regex-hot.jsonl](../oc-regex-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 + +2000 次固定字符串匹配。以前每次迭代都会构建 NSRegularExpression;之后只构建一次。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `regex_once` | 0.555000 | 2000 | 1 | +| 之前 | `regex_each_iter` | 8.290000 | 2000 | 14.937 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/objcperf/evidence/zh-CN/oc-value-for-key-hot.md b/packages/objcperf/evidence/zh-CN/oc-value-for-key-hot.md new file mode 100644 index 0000000..a3f2947 --- /dev/null +++ b/packages/objcperf/evidence/zh-CN/oc-value-for-key-hot.md @@ -0,0 +1,17 @@ +# oc-value-for-key-hot — 已测量,仍然提示 + +## 状态 +**提示**。实时时钟 JSONL:[oc-value-for-key-hot.jsonl](../oc-value-for-key-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`valueForKey:` 每次迭代与直接字段读取对比。200000 次操作。直接读取是易失性的,因此循环后仍然存在。之前一次运行在循环后打印 0 毫秒的结果已被丢弃。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `direct_field` | 0.051 | 200000 | 1 | +| 之前 | `value_for_key_each` | 6.334 | 200000 | 124.145 | + +每次迭代都会变化的名称无法成为直接字段。默认保持 **提示**。这一比率不是一般性的加速。 + +## 解锁 +不要仅凭此文件将默认值设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍然需要语料库分流。 diff --git a/packages/objcperf/zh-CN/CONTRIBUTING.md b/packages/objcperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/objcperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/objcperf/zh-CN/README.md b/packages/objcperf/zh-CN/README.md new file mode 100644 index 0000000..4ca051b --- /dev/null +++ b/packages/objcperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# objcperf + +> **范围说明:** **objcperf** 是 **Objective-C**(MRC/ARC;循环里的 `NSString` 拼接与 `NSLog`)。与姊妹 kit [swiftperf](../../swiftperf/) 不同。在 Linux 上检查器是对 `.m`/`.h` 的 **C 行窗扫描器**(clang 构建该工具);lint 不需要完整的 Apple 运行时。 + +Objective-C 性能笔记、小示例,以及 `objcperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。针对 2026-10-22 规则集 **corpus-ready**。oc-nsstring-append-hot / oc-nslog-hot 默认 **off**(已测,未升级)。日期格式化器、JSON 与正则 hint 尚待测量,不在该裁决内。两条规则已移除(墓碑,从未拿到 slug)。指南 v0。 + +## 目录 + +- [英文指南(v0)](../docs/zh-CN/objc-performance-guide.md) — 来自 `docs/guide-src/` +- [`objcperf-lint`](../objcperf-lint/README.md) — 两条实测规则 **off**;三条 hint 规则待测;两条规则已移除(墓碑,从未拿到 slug) +- [语料固定目标](../docs/zh-CN/corpus.md) + +## 关注点 + +预分配 `NSMutableArray`,避免循环里的 `stringByAppendingString`(优先 `NSMutableString`),热循环里不要放 `NSLog`。 + +## 运行 lint + +```sh +cd packages/objcperf/objcperf-lint && make +./objcperf-lint path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅 **error** 级发现时退出 `1`(hint 退出 `0`,除非 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ 章节源 +docs/ 组装后的指南 + corpus.md + evidence-policy.md +evidence/ 两条实测规则(均 off);三条待定 hint 笔记;OC001/OC004 墓碑 +examples/ 小演示 +objcperf-lint/ 检查器 + 夹具 +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/phpperf/docs/guide-src/00_head.md b/packages/phpperf/docs/guide-src/00_head.md index 8c9f077..ab663f3 100644 --- a/packages/phpperf/docs/guide-src/00_head.md +++ b/packages/phpperf/docs/guide-src/00_head.md @@ -2,17 +2,12 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: PHP 8+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: PHP 8+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`hrtime`, Blackfire, Xdebug). -2. Treat each chapter as a checklist. -3. Extensions / JIT differ — re-bench. +Profile first (`hrtime`, Blackfire, Xdebug). Extensions and JIT differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings and arrays](#1-strings-and-arrays) - [2. Hot I/O](#2-hot-io) - [Lint map](#lint-map) diff --git a/packages/phpperf/docs/guide-src/01_builders.md b/packages/phpperf/docs/guide-src/01_builders.md index 91f1d95..aa7dc1d 100644 --- a/packages/phpperf/docs/guide-src/01_builders.md +++ b/packages/phpperf/docs/guide-src/01_builders.md @@ -2,5 +2,5 @@ `$s .= $piece` in a loop can still be costly at scale; prefer collectors + `implode`. Repeated `$a = array_merge($a, …)` copies; prefer `$a[] = $item`. -Companion lint: **ph-string-dot-hot**, **ph-array-merge-rebind**. +Lint: `ph-string-dot-hot` (default off); `ph-array-merge-rebind` (default error). diff --git a/packages/phpperf/docs/guide-src/02_hot_io.md b/packages/phpperf/docs/guide-src/02_hot_io.md index 9c011e8..f613914 100644 --- a/packages/phpperf/docs/guide-src/02_hot_io.md +++ b/packages/phpperf/docs/guide-src/02_hot_io.md @@ -2,5 +2,5 @@ `echo` / `print` in a tight loop flushes. Gate debug output or buffer. -Companion lint: **ph-echo-hot**. +Lint: `ph-echo-hot` (default hint). diff --git a/packages/phpperf/docs/guide-src/99_tail.md b/packages/phpperf/docs/guide-src/99_tail.md index c2808f7..97b9d21 100644 --- a/packages/phpperf/docs/guide-src/99_tail.md +++ b/packages/phpperf/docs/guide-src/99_tail.md @@ -3,7 +3,7 @@ | ID | Smell | Default | |----|-------|---------| | ph-string-dot-hot | string `.=` in loop | off | -| ph-array-merge-rebind | `array_merge` rebinding in loop | **error** | +| ph-array-merge-rebind | `array_merge` rebinding in loop | error | | ph-echo-hot | `echo` / `print` in loop | hint | Escalate only with `evidence/ph-*.jsonl` — see [evidence-policy.md](evidence-policy.md). diff --git a/packages/phpperf/docs/guide-src/zh-CN/00_head.md b/packages/phpperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..3284718 --- /dev/null +++ b/packages/phpperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,14 @@ +# PHP 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:PHP 8+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`hrtime`、Blackfire、Xdebug)。扩展与 JIT 不同——请重测。 + +## 目录 + +- [1. 字符串与数组](#1-字符串与数组) +- [2. 热 I/O](#2-热-io) +- [Lint 对照](#lint-对照) + diff --git a/packages/phpperf/docs/guide-src/zh-CN/01_builders.md b/packages/phpperf/docs/guide-src/zh-CN/01_builders.md new file mode 100644 index 0000000..e178f0d --- /dev/null +++ b/packages/phpperf/docs/guide-src/zh-CN/01_builders.md @@ -0,0 +1,5 @@ +## 1. 字符串与数组 + +循环里的 `$s .= $piece` 在规模变大时仍可能昂贵;优先收集器 + `implode`。反复 `$a = array_merge($a, …)` 会拷贝;优先 `$a[] = $item`。 + +Lint: `ph-string-dot-hot`(默认 off); `ph-array-merge-rebind`(default error) diff --git a/packages/phpperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/phpperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..a54e50d --- /dev/null +++ b/packages/phpperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,5 @@ +## 2. 热 I/O + +紧密循环里的 `echo` / `print` 会刷新输出。门控调试输出或做缓冲。 + +Lint: `ph-echo-hot`(默认 hint) diff --git a/packages/phpperf/docs/guide-src/zh-CN/99_tail.md b/packages/phpperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..74db604 --- /dev/null +++ b/packages/phpperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,13 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| ph-string-dot-hot | 循环内字符串 `.=` | off | +| ph-array-merge-rebind | 循环内 `array_merge` 重绑定 | error | +| ph-echo-hot | 循环内 `echo` / `print` | hint | + +仅在有 `evidence/ph-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/phpperf/docs/php-performance-guide.md b/packages/phpperf/docs/php-performance-guide.md index fccc054..900dab2 100644 --- a/packages/phpperf/docs/php-performance-guide.md +++ b/packages/phpperf/docs/php-performance-guide.md @@ -2,17 +2,12 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: PHP 8+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: PHP 8+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`hrtime`, Blackfire, Xdebug). -2. Treat each chapter as a checklist. -3. Extensions / JIT differ — re-bench. +Profile first (`hrtime`, Blackfire, Xdebug). Extensions and JIT differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings and arrays](#1-strings-and-arrays) - [2. Hot I/O](#2-hot-io) - [Lint map](#lint-map) @@ -21,20 +16,20 @@ Fewer quadratic builders, quieter hot paths. `$s .= $piece` in a loop can still be costly at scale; prefer collectors + `implode`. Repeated `$a = array_merge($a, …)` copies; prefer `$a[] = $item`. -Companion lint: **ph-string-dot-hot**, **ph-array-merge-rebind**. +Lint: `ph-string-dot-hot` (default off); `ph-array-merge-rebind` (default error). ## 2. Hot I/O `echo` / `print` in a tight loop flushes. Gate debug output or buffer. -Companion lint: **ph-echo-hot**. +Lint: `ph-echo-hot` (default hint). ## Lint map | ID | Smell | Default | |----|-------|---------| | ph-string-dot-hot | string `.=` in loop | off | -| ph-array-merge-rebind | `array_merge` rebinding in loop | **error** | +| ph-array-merge-rebind | `array_merge` rebinding in loop | error | | ph-echo-hot | `echo` / `print` in loop | hint | Escalate only with `evidence/ph-*.jsonl` — see [evidence-policy.md](evidence-policy.md). diff --git a/packages/phpperf/docs/zh-CN/corpus.md b/packages/phpperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..67f120d --- /dev/null +++ b/packages/phpperf/docs/zh-CN/corpus.md @@ -0,0 +1,41 @@ +# phpperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +浅克隆。 + +| 树 | 提交 | 大小 | +|------|--------|------| +| **framework** | `6107396` | 46M | +| **symfony** | `ad1bfbb2` | 181M | +| **composer** | `e7417e7` | 13M | + +使用以下工具进行代码检查: + +```sh +php packages/phpperf/phpperf-lint/phpperf-lint.php \ + / --format json --no-fail +``` + +检查器是 **PHP 8+**(`phpperf-lint`)。规则 ph-string-dot-hot–ph-echo-hot(**ph-string-dot-hot 默认 `off`**;**ph-array-merge-rebind 默认 `error`** 与 G4;ph-echo-hot `hint`)。 + +## 内部测试:G3 r2 和 后 G4 r3(2026-10-21 至 2026-10-22,亚洲/上海) + +| Tree | baseline → r1 → r2 → **r3** | +|------|------------------------------| +| **框架** | 65 → 59 → 48 → **25** (ph-array-merge-rebind:21 ph-echo-hot:4) | +| **symfony** | 796 → 636 → 393 → **47** (ph-array-merge-rebind:34 ph-echo-hot:13) | +| **作曲家** | 107 → 103 → 47 → **17** (ph-array-merge-rebind:17) | + +**语料库就绪:是** — 在 ph-string-dot-hot 降级后,r3 n=89:TP 79.8%,FP+DOC+SKIP 0%;在 ph-array-merge-rebind 上的真实 TP;ph-array-merge-rebind 错误包含 G4(`evidence/ph-array-merge-rebind.md`+`jsonl`)。 + +### 反向优化 + +- 路径跳过:`test(s)/` `Tests/` `Fixtures/` `views/` `vendor/` `Tui/` `.github/` + 资源/bin 代码生成文件跳过 +- **ph-string-dot-hot 默认 `off`** — 框架 `.=` 主要是冷构建器/解析器;使用 `--severity ph-string-dot-hot=hint` 进行审查 +- ph-array-merge-rebind:仅 `$a = array_merge($a, …)` 增长重新绑定 +- ph-echo-hot:掩码字符串;类似语句的 echo/print + +不要将语料来源提交到 `packages/phpperf`。 diff --git a/packages/phpperf/docs/zh-CN/evidence-policy.md b/packages/phpperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..e2d0561 --- /dev/null +++ b/packages/phpperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,22 @@ +# phpperf 证据政策 + +## 门槛 + +一条规则只有在 `packages/phpperf/evidence/` 下两者都存在时,才可以被视为 **`error`**: + +1. `.md` — 声明,如何测量,工具链版本,限制 +2. `.jsonl` — ≥1 条前/后行,包含 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts` + +否则规则保持 **`hint``**。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| ph-string-dot-hot | 关闭 | 已测量(`ph-string-dot-hot.md` + `ph-string-dot-hot.jsonl`);保持关闭 | 循环中的字符串 `.=` | +| ph-array-merge-rebind | **错误** | 是 (`ph-array-merge-rebind.md` + `ph-array-merge-rebind.jsonl`) | 循环中的 `array_merge` 重新绑定 | +| ph-echo-hot | 提示 | 测量(`ph-echo-hot.md` + `ph-echo-hot.jsonl`);保留提示 | 循环中的 `echo` / `print` | + +在 PHP 8.4.26 上进行 G4 微基准测试后,ph-array-merge-rebind 出现了升级(固定形状标量约 1848 倍;8 元素块约 344 倍;小 N 约 13 倍)。ph-echo-hot 有测量证据(每次迭代 echo 与一次 `implode`,约 4.36 倍)但仍保持 **提示**。ph-string-dot-hot 有测量证据(约 3.29 倍)但仍保持 `关闭`(G3:大体上是 OK 习惯用法)。 diff --git a/packages/phpperf/docs/zh-CN/php-performance-guide.md b/packages/phpperf/docs/zh-CN/php-performance-guide.md new file mode 100644 index 0000000..6ac9423 --- /dev/null +++ b/packages/phpperf/docs/zh-CN/php-performance-guide.md @@ -0,0 +1,37 @@ +# PHP 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:PHP 8+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`hrtime`、Blackfire、Xdebug)。扩展与 JIT 不同——请重测。 + +## 目录 + +- [1. 字符串与数组](#1-字符串与数组) +- [2. 热 I/O](#2-热-io) +- [Lint 对照](#lint-对照) + +## 1. 字符串与数组 + +循环里的 `$s .= $piece` 在规模变大时仍可能昂贵;优先收集器 + `implode`。反复 `$a = array_merge($a, …)` 会拷贝;优先 `$a[] = $item`。 + +Lint: `ph-string-dot-hot`(默认 off); `ph-array-merge-rebind`(default error) +## 2. 热 I/O + +紧密循环里的 `echo` / `print` 会刷新输出。门控调试输出或做缓冲。 + +Lint: `ph-echo-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| ph-string-dot-hot | 循环内字符串 `.=` | off | +| ph-array-merge-rebind | 循环内 `array_merge` 重绑定 | error | +| ph-echo-hot | 循环内 `echo` / `print` | hint | + +仅在有 `evidence/ph-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/phpperf/docs/zh-CN/rule-ids.md b/packages/phpperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..6c49889 --- /dev/null +++ b/packages/phpperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,20 @@ +# phpperf规则id(slug) + +规范id是**slugs only**。旧地图上的数字是历史的,不被接受。 + +| Slug | +|------| +| `ph-string-dot-hot` | +| `ph-array-merge-rebind` | +| `ph-echo-hot` | + +另请参见[root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +##改名2026-10-27(墓碑旧;从不重复使用) + +默认**`--scope all`**(或等效套件)。旧`*-in-loop` ids误导工具/模型;他们被埋葬在`evidence/_removed/`下。 + +|旧鼻涕虫|新鼻涕虫| +|----------|----------| +| `ph-echo-in-loop` | `ph-echo-hot` | +| `ph-string-dot-in-loop` | `ph-string-dot-hot` | diff --git a/packages/phpperf/evidence/zh-CN/README.md b/packages/phpperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..2e25f17 --- /dev/null +++ b/packages/phpperf/evidence/zh-CN/README.md @@ -0,0 +1,7 @@ +# phpperf 证据 + +- **ph-array-merge-rebind** — 已测量 (`ph-array-merge-rebind.md` + `ph-array-merge-rebind.jsonl`);默认严重性 **错误**。 +- **ph-string-dot-hot** — 已测量 (`ph-string-dot-hot.md` + `ph-string-dot-hot.jsonl`);默认保持 **关闭**(G3: 大多数情况下 OK-惯用)。 +- **ph-echo-hot** — 已测量 (`ph-echo-hot.md` + `ph-echo-hot.jsonl`);默认保持 **提示**。 + +请参阅 [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录下的 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/phpperf/evidence/zh-CN/ph-array-merge-rebind.md b/packages/phpperf/evidence/zh-CN/ph-array-merge-rebind.md new file mode 100644 index 0000000..80ec996 --- /dev/null +++ b/packages/phpperf/evidence/zh-CN/ph-array-merge-rebind.md @@ -0,0 +1,48 @@ +# ph-array-merge-rebind — 循环中 `$a = array_merge($a, …)` 的重新绑定 + +## 主张 + +在循环中使用 `$a = array_merge($a, $chunk)` 重新绑定累加器比使用 `$a[] = …`(或 `array_push`)追加要慢得多,在 PHP 8.4 中尤为明显,因为每次 `array_merge` 都会分配并复制不断增长的前缀(经典的二次成本)。成本随着 N 和块大小增长;即使 N≈100,在这台机器上仍然显示出明显差距。 + +## 如何测量 + +- 测试工具:`packages/phpperf/examples/bench/evidence_ph002.php` +- 计时器:PHP `hrtime(true)` 墙时纳秒 +- 协议:预热=3;取9轮的中位数;每个计时样本前执行 `gc_collect_cycles()` +- A 对(对应测试用例 `bad/ph-array-merge-rebind_array_merge.php` / `good/ph-array-merge-rebind_push.php`):N=20000 + - **之前:** 循环中 `$a = array_merge($a, [$i])` + - **之后:** 循环中 `$a[] = $i` +- B 对(固定 8 元素块):N=5000 + - **之前:** 循环中 `$a = array_merge($a, $chunk)` + - **之后:** 循环中 foreach 推入 `$a` +- C 对(小 N):N=100 — 仍然多倍;绝对毫秒很小 +- 原始数据行:[`ph-array-merge-rebind.jsonl`](../ph-array-merge-rebind.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| PHP | **PHP 8.5.12** | +| 中位数之前 (`scalar_array_merge`) | 243.229 毫秒 | +| (`scalar_push`) 之后的中位数 | 0.234 毫秒 | +| **比率标量 前/后** | **~1038×** | +| `chunk8_array_merge` 之前的中位数 | 153.429 毫秒 | +| (`chunk8_push`) 后的中位数 | 0.267 毫秒 | +| **ratio chunk8 前/后** | **~574×** | +| `small_n_array_merge` 之前的中位数 | 0.019 毫秒 | +| (`small_n_push`) 后的中位数 | 0.001 毫秒 | +| **ratio small N** | **~12.8×** | + +各对之间方向稳定;绝对毫秒和比率对 N/块/机器敏感(呈二次关系)。在 PHP 或机器更改后重新运行测试工具。 + +## 与 lint 的对应 + +`phpperf-lint` ph-array-merge-rebind 标记在循环窗口内同一变量的增长性重绑定 `$a = array_merge($a, …)`(而不是循环外的一次性合并 —— 参见 `good/ph-array-merge-rebind_oneshot_merge.php`)。在 [evidence-policy](../../docs/zh-CN/evidence-policy.md) 下,默认严重性为 **`error`**:此 JSONL 存在,wall 同意热累积的情况,并且 G3 语料库残留(Composer/Laravel)看起来是可靠的真实正例。冷/小型构建器仍然可以使用 —— 当循环不热时,可用 `--severity ph-array-merge-rebind=hint` 或 `off` 覆盖。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 不测量 `array_merge` 的 *key* 覆盖/关联重映射(规则目标是增长列表重绑定)。 +- C 对的绝对时间接近计时器噪声;比率仍然有利于 push。 +- 历史注:ph-string-dot-hot 后来把 `off` 降级;工具包在 G4 复查后翻转为 **语料库就绪**(见 docs/corpus.md)。 diff --git a/packages/phpperf/evidence/zh-CN/ph-echo-hot.md b/packages/phpperf/evidence/zh-CN/ph-echo-hot.md new file mode 100644 index 0000000..37aac13 --- /dev/null +++ b/packages/phpperf/evidence/zh-CN/ph-echo-hot.md @@ -0,0 +1,33 @@ +# ph-echo-hot — 每次迭代使用 echo/fwrite 与 一次性使用 implode + +## 主张 + +在这台机器上测量:主要热案例 **`echo_each_iter`** 比率前/后 **~21.31×**(之后更快)。默认严重性保持 **`hint`**。仅凭此填充 **不要** 升级为 `error`。 + +## 如何测量 + +- 测试套件:`packages/phpperf/examples/bench/evidence_ph001_ph003.php` +- 协议:预热=3;9轮的中位数;在适用时进行垃圾回收/收集 +- 原始数据行:[`ph-echo-hot.jsonl`](../ph-echo-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **PHP 8.5.12** | +| 在 (`implode_once`) 之后的中位数 | 0.575792 毫秒 (操作=20000,比率=1.000000) | +| 中位数之前 (`echo_each_iter`) | 12.272208 毫秒 (操作=20000,比率=21.313613) | +| **主要比率 前/后** | **~21.31×** (`echo_each_iter`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试程序。 + +## 与 lint 的对应 + +`phpperf-lint` **ph-echo-hot** 默认保持 **`hint`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能分析。 +- 小样本行数可能接近计时器噪声;对于比例声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)会丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/phpperf/evidence/zh-CN/ph-string-dot-hot.md b/packages/phpperf/evidence/zh-CN/ph-string-dot-hot.md new file mode 100644 index 0000000..65514e6 --- /dev/null +++ b/packages/phpperf/evidence/zh-CN/ph-string-dot-hot.md @@ -0,0 +1,35 @@ +# ph-string-dot-hot — 字符串 .= 与 implode + +## 主张 + +在这台机器上测量:主要热情况 **`str_dot`** 比率前/后约 **~2.54×**(之后更快)。默认严重性保持 **`off`**。仅凭此填充 **不要** 升级到 `error`。 + +## 如何测量 + +- 测试套件:`packages/phpperf/examples/bench/evidence_ph001_ph003.php` +- 协议:预热=3;9轮的中位数;在适用时进行垃圾回收/收集 +- 原始数据行:[`ph-string-dot-hot.jsonl`](../ph-string-dot-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **PHP 8.5.12** | +| `implode` 后的中值 | 0.319334 毫秒 (操作=50000,比例=1.000000) | +| 中位数之前 (`str_dot`) | 0.811167 毫秒 (操作=50000, 比率=2.540184) | +| 中位数之后 (`small_implode`) | 0.000375 毫秒 (操作=100, 比率=1.000000) | +| 中位数之前 (`small_str_dot`) | 0.000917 毫秒 (操作=100, 比率=2.445333) | +| **主要比率前/后** | **~2.54×** (`str_dot`) | + +方向和绝对毫秒数对机器/光学敏感。在工具链或机器更改后,请重新运行测试夹具。 + +## 与 lint 的对应 + +`phpperf-lint` **ph-string-dot-hot** 默认保持 **`关闭`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能分析。 +- 小样本行数可能接近计时器噪声;对于比例声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)会丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/phpperf/zh-CN/CONTRIBUTING.md b/packages/phpperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/phpperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/phpperf/zh-CN/README.md b/packages/phpperf/zh-CN/README.md new file mode 100644 index 0000000..263dbef --- /dev/null +++ b/packages/phpperf/zh-CN/README.md @@ -0,0 +1,37 @@ +# phpperf + +PHP 性能笔记、小示例,以及 `phpperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。启发式 lint(**corpus-ready: 是**;**ph-array-merge-rebind=error** 带 G4;**ph-string-dot-hot=off**;ph-echo-hot=hint)。指南 v0。 + +## 目录 + +- [英文指南(v0)](../docs/zh-CN/php-performance-guide.md) — 来自 `docs/guide-src/` +- [`phpperf-lint`](../phpperf-lint/README.md) — ph-string-dot-hot–ph-echo-hot +- [语料固定目标](../docs/zh-CN/corpus.md) + +## 关注点 + +避免平方级字符串增长(`.=`),优先 push 而非 `array_merge` 重绑定,门控热路径 `echo`/`print`。 + +## 运行 lint + +```sh +cd packages/phpperf +php phpperf-lint/phpperf-lint.php path/to/sources +bash scripts/smoke-lint.sh +bash scripts/self-lint.sh +``` + +仅 **error** 级发现时退出 `1`(hint 退出 `0`,除非 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ 章节源 +docs/ 组装后的指南 + corpus.md + evidence-policy.md +evidence/ ph-array-merge-rebind 已测(error);ph-string-dot-hot 已测(off);ph-echo-hot 已测(hint) +examples/ 小演示 +phpperf-lint/ 检查器 + 夹具 +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/pyperf/docs/guide-src/00_head.md b/packages/pyperf/docs/guide-src/00_head.md index 0069ffc..82960e9 100644 --- a/packages/pyperf/docs/guide-src/00_head.md +++ b/packages/pyperf/docs/guide-src/00_head.md @@ -2,18 +2,14 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: CPython 3.10+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: CPython 3.10+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`timeit`, `py-spy`, `cProfile`). -2. Treat each chapter as a checklist. -3. PyPy / other VMs differ — re-bench. +Profile first (`timeit`, `py-spy`, `cProfile`). PyPy and other VMs differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings and lists](#1-strings-and-lists) - [2. Hot I/O](#2-hot-io) +- [3. Parse and deep copy](#3-parse-and-deep-copy) - [Lint map](#lint-map) diff --git a/packages/pyperf/docs/guide-src/01_builders.md b/packages/pyperf/docs/guide-src/01_builders.md index ff8c7b9..045dc95 100644 --- a/packages/pyperf/docs/guide-src/01_builders.md +++ b/packages/pyperf/docs/guide-src/01_builders.md @@ -2,5 +2,5 @@ `s += piece` in a loop is quadratic on CPython. Prefer `''.join(...)` or `io.StringIO`. Repeated `xs = xs + [item]` similarly copies; prefer `append`. -Companion lint: **pp-str-plus-hot**, **pp-list-plus-rebind**. +Lint: `pp-str-plus-hot` (default off after G3); `pp-list-plus-rebind` (default hint). diff --git a/packages/pyperf/docs/guide-src/02_hot_io.md b/packages/pyperf/docs/guide-src/02_hot_io.md index 048c3d1..dd7d1ad 100644 --- a/packages/pyperf/docs/guide-src/02_hot_io.md +++ b/packages/pyperf/docs/guide-src/02_hot_io.md @@ -2,5 +2,5 @@ `print` in a tight loop flushes and formats. Gate debug output or buffer. -Companion lint: **pp-print-hot**. +Lint: `pp-print-hot` (default off after G3). diff --git a/packages/pyperf/docs/guide-src/03_hot_calls.md b/packages/pyperf/docs/guide-src/03_hot_calls.md index e8a694a..5c641b0 100644 --- a/packages/pyperf/docs/guide-src/03_hot_calls.md +++ b/packages/pyperf/docs/guide-src/03_hot_calls.md @@ -1,7 +1,8 @@ ## 3. Parse and deep copy -`json.loads` / `json.dumps` and `deepcopy` inside a loop repeat work that belongs outside it when the value does not change. A changing payload cannot be hoisted. These lints are **hint** and have a microbench. `re.compile` was removed: CPython caches it, and the measured difference stayed under 3×. `datetime.strptime` / `strftime` was removed: a fixed string is faster only when the call is skipped, and a changing string has nothing to hoist. +`json.loads` / `json.dumps` and `deepcopy` inside a loop repeat work that belongs outside it when the value does not change. A changing payload cannot be hoisted. These lints are hint and have a microbench. `re.compile` was removed: CPython caches it, and the measured difference stayed under 3×. `datetime.strptime` / `strftime` was removed: a fixed string is faster only when the call is skipped, and a changing string has nothing to hoist. `open` of one path inside a loop repeats the open. One measured shape, a one-byte file, was about 409×. A path that changes every iteration cannot be hoisted. `time.perf_counter_ns` was about 2.9× and `getattr` about 1.4×; those were checked and not given a rule. -Companion lint: **pp-json-hot**, **pp-deepcopy-hot**, **pp-open-hot**. +Lint: `pp-json-hot`, `pp-deepcopy-hot`, `pp-open-hot` (default hint). + diff --git a/packages/pyperf/docs/guide-src/zh-CN/00_head.md b/packages/pyperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..c80d280 --- /dev/null +++ b/packages/pyperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Python 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:CPython 3.10+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`timeit`、`py-spy`、`cProfile`)。PyPy 与其他 VM 不同——请重测。 + +## 目录 + +- [1. 字符串与列表](#1-字符串与列表) +- [2. 热 I/O](#2-热-io) +- [3. 解析与深拷贝](#3-解析与深拷贝) +- [Lint 对照](#lint-对照) + diff --git a/packages/pyperf/docs/guide-src/zh-CN/01_builders.md b/packages/pyperf/docs/guide-src/zh-CN/01_builders.md new file mode 100644 index 0000000..b6903e0 --- /dev/null +++ b/packages/pyperf/docs/guide-src/zh-CN/01_builders.md @@ -0,0 +1,5 @@ +## 1. 字符串与列表 + +循环里的 `s += piece` 在 CPython 上是平方级的。优先 `''.join(...)` 或 `io.StringIO`。反复 `xs = xs + [item]` 同样会拷贝;优先 `append`。 + +Lint: `pp-str-plus-hot`(默认 off,G3 后); `pp-list-plus-rebind`(默认 hint) diff --git a/packages/pyperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/pyperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..c0e5611 --- /dev/null +++ b/packages/pyperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,5 @@ +## 2. 热 I/O + +紧密循环里的 `print` 会刷新并格式化。门控调试输出或做缓冲。 + +Lint: `pp-print-hot`(默认 off,G3 后) diff --git a/packages/pyperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/pyperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..6a592d9 --- /dev/null +++ b/packages/pyperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,7 @@ +## 3. 解析与深拷贝 + +循环里的 `json.loads` / `json.dumps` 与 `deepcopy` 会重复本可在循环外完成的工作(当值不变时)。变化的 payload 不能外提。这些 lint 是 hint,并有微基准。`re.compile` 已移除:CPython 会缓存,测得差距低于 3×。`datetime.strptime` / `strftime` 已移除:固定字符串仅在跳过调用时更快,变化的字符串没有可外提的东西。 + +循环里对同一路径 `open` 会反复打开。某一测量形态(1 字节文件)约 409×。每轮都变的路径不能外提。`time.perf_counter_ns` 约 2.9×,`getattr` 约 1.4×;已检查但未给规则。 + +Lint: `pp-json-hot`, `pp-deepcopy-hot`, `pp-open-hot`(默认 hint) diff --git a/packages/pyperf/docs/guide-src/zh-CN/99_tail.md b/packages/pyperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..3586b7e --- /dev/null +++ b/packages/pyperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,15 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| pp-str-plus-hot | 循环内字符串 `+=` | off(G3) | +| pp-list-plus-rebind | 循环内 list `+` 重绑定 | hint | +| pp-print-hot | 循环内 `print(` | off(G3) | +| pp-json-hot | 循环内 `json.loads` / `dumps` | hint(有测量;未 corpus 分级) | +| pp-deepcopy-hot | 循环内 `deepcopy` | hint(有测量;未 corpus 分级) | + +仅在有 `evidence/pp-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。临时的 `re.METHOD(literal)` 规则(PP004)已移除;见 [rule-ids.md](rule-ids.md)。`pp-regex-compile-in-loop` 与 `pp-datetime-format-in-loop` 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/pyperf/docs/python-performance-guide.md b/packages/pyperf/docs/python-performance-guide.md index ab4cfc5..9196e11 100644 --- a/packages/pyperf/docs/python-performance-guide.md +++ b/packages/pyperf/docs/python-performance-guide.md @@ -2,40 +2,37 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: CPython 3.10+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: CPython 3.10+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## How to read this - -1. **Measure first** (`timeit`, `py-spy`, `cProfile`). -2. Treat each chapter as a checklist. -3. PyPy / other VMs differ — re-bench. +Profile first (`timeit`, `py-spy`, `cProfile`). PyPy and other VMs differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings and lists](#1-strings-and-lists) - [2. Hot I/O](#2-hot-io) +- [3. Parse and deep copy](#3-parse-and-deep-copy) - [Lint map](#lint-map) ## 1. Strings and lists `s += piece` in a loop is quadratic on CPython. Prefer `''.join(...)` or `io.StringIO`. Repeated `xs = xs + [item]` similarly copies; prefer `append`. -Companion lint: **pp-str-plus-hot**, **pp-list-plus-rebind**. +Lint: `pp-str-plus-hot` (default off after G3); `pp-list-plus-rebind` (default hint). ## 2. Hot I/O `print` in a tight loop flushes and formats. Gate debug output or buffer. -Companion lint: **pp-print-hot**. +Lint: `pp-print-hot` (default off after G3). ## 3. Parse and deep copy -`json.loads` / `json.dumps` and `deepcopy` inside a loop repeat work that belongs outside it when the value does not change. A changing payload cannot be hoisted. These lints are **hint** and have a microbench. `re.compile` was removed: CPython caches it, and the measured difference stayed under 3×. `datetime.strptime` / `strftime` was removed: a fixed string is faster only when the call is skipped, and a changing string has nothing to hoist. +`json.loads` / `json.dumps` and `deepcopy` inside a loop repeat work that belongs outside it when the value does not change. A changing payload cannot be hoisted. These lints are hint and have a microbench. `re.compile` was removed: CPython caches it, and the measured difference stayed under 3×. `datetime.strptime` / `strftime` was removed: a fixed string is faster only when the call is skipped, and a changing string has nothing to hoist. `open` of one path inside a loop repeats the open. One measured shape, a one-byte file, was about 409×. A path that changes every iteration cannot be hoisted. `time.perf_counter_ns` was about 2.9× and `getattr` about 1.4×; those were checked and not given a rule. -Companion lint: **pp-json-hot**, **pp-deepcopy-hot**, **pp-open-hot**. +Lint: `pp-json-hot`, `pp-deepcopy-hot`, `pp-open-hot` (default hint). + ## Lint map | ID | Smell | Default | diff --git a/packages/pyperf/docs/zh-CN/corpus.md b/packages/pyperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..e332df1 --- /dev/null +++ b/packages/pyperf/docs/zh-CN/corpus.md @@ -0,0 +1,38 @@ +# pyperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任意本地目录,并将该路径传递给代码检查器。 + +这个包是 **XPerf 的 Python 嗅探工具包**,不是 PyPI 上的 `pyperf` 基准测试运行器。 + +## 固定目标 + +| 树 | URL | 提交 | 备注 | +|------|-----|--------|-------| +| django | https://github.com/django/django | `3d3d943b7442f1f5278ed876331a06047fa96ea8` | 浅层 | +| flask | https://github.com/pallets/flask | `d086db856be187255b8ec61ef409357393020f32` | 浅层 | +| requests | https://github.com/psf/requests | `611c6162cbc4ac2020a2f91c7cfa4f3abf9bbb60` | 浅拷贝; 默认下 0 | + +## G3 摘要(2026-10-22 亚洲/上海)——重新设计 + +| 树 | r2 | r3(默认值) | 语料库就绪? | +|------|----|---------------|---------------| +| django | 27 | **28** | PP004 | +| flask | 0 | **1** | CLI OK-惯用语 | +| requests | 0 | **0** | clean | + +**判决:语料库就绪 `是`。** 样本 n=29:TP 51.7% / OK-成语 48.3% / FP+DOC+跳过 0%。默认值:pp-str-plus-hot/pp-print-hot **关闭**;pp-list-plus-rebind **提示**。PP004 已移除(抖动)。测量的 JSONL 填充存在,但不会更改默认值(参见 [evidence-policy.md](evidence-policy.md))。 + +### 重新设计笔记 + +- PP004:函数内部的临时 `re.METHOD(literal)` +- 跳过:`scripts/`,`management/commands/` + +不要将语料库来源提交到 `packages/pyperf`。 + +## 2026-10-23 — 提示2026-10-22裁决之外的规则 + +2026-10-22 的语料库准备判决不包括以下子弹头。它们仍然保持 **提示** 状态,尚未经过 G2/G3。 + +- `pp-json-hot`、`pp-deepcopy-hot` — 磁盘上的 JSONL;仍然是提示 + +`pp-regex-compile-in-loop` 和 `pp-datetime-format-in-loop` 已被移除。请参见 `evidence/_removed/`。它们不是该裁决的一部分。 diff --git a/packages/pyperf/docs/zh-CN/evidence-policy.md b/packages/pyperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..f622919 --- /dev/null +++ b/packages/pyperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,21 @@ +# pyperf 证据政策 + +## 门槛 + +一条规则只有在同时存在 `evidence/.md` 和测量的 `evidence/.jsonl` 时才可以是 **`error`**。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性 | 证据 | 异味 | +|------|----------|----------|-------| +| pp-str-plus-hot | **关闭** | 已测量(`pp-str-plus-hot.md` + `pp-str-plus-hot.jsonl`);保持关闭 | 循环中的字符串 `+=`(G3 OK-成语) | +| pp-list-plus-rebind | 提示 | 测量(`pp-list-plus-rebind.md` + `pp-list-plus-rebind.jsonl`);保留提示 | 循环中的列表 `+` 重新绑定 | +| pp-print-hot | **关** | 已测量(`pp-print-hot.md` + `pp-print-hot.jsonl`);保持关闭 | 循环中的 `print(` (G3 CLI) | +| pp-json-hot | 提示 | 测量(`pp-json-hot.md` + `pp-json-hot.jsonl`);保持提示 | 循环中的 `json.loads` / `dumps` | +| pp-deepcopy-hot | 提示 | 测量 (`pp-deepcopy-hot.md` + `pp-deepcopy-hot.jsonl`); 保持提示 | 循环中的 `deepcopy` | +| pp-open-hot | 提示 | 已测量(`pp-open-hot.md` + `pp-open-hot.jsonl`);保留提示 | 在循环中 `open`;仅相同路径;非语料库筛选 | +| PP004 | 已移除 | 墓碑 | 见 `evidence/_removed/` | + +2026-10-22 规则的已测量 JSONL 存在,但没有被升级:pp-str-plus-hot/pp-print-hot 保持 `off`(G3:CLI 和构建器代码中是 OK-习语),pp-list-plus-rebind 保持 **hint**。PP004 已移除;其墓碑位于 `evidence/_removed/` 下。`pp-regex-compile-in-loop` 和 `pp-datetime-format-in-loop` 已被移除。 diff --git a/packages/pyperf/docs/zh-CN/python-performance-guide.md b/packages/pyperf/docs/zh-CN/python-performance-guide.md new file mode 100644 index 0000000..3db47b3 --- /dev/null +++ b/packages/pyperf/docs/zh-CN/python-performance-guide.md @@ -0,0 +1,47 @@ +# Python 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:CPython 3.10+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`timeit`、`py-spy`、`cProfile`)。PyPy 与其他 VM 不同——请重测。 + +## 目录 + +- [1. 字符串与列表](#1-字符串与列表) +- [2. 热 I/O](#2-热-io) +- [3. 解析与深拷贝](#3-解析与深拷贝) +- [Lint 对照](#lint-对照) + +## 1. 字符串与列表 + +循环里的 `s += piece` 在 CPython 上是平方级的。优先 `''.join(...)` 或 `io.StringIO`。反复 `xs = xs + [item]` 同样会拷贝;优先 `append`。 + +Lint: `pp-str-plus-hot`(默认 off,G3 后); `pp-list-plus-rebind`(默认 hint) +## 2. 热 I/O + +紧密循环里的 `print` 会刷新并格式化。门控调试输出或做缓冲。 + +Lint: `pp-print-hot`(默认 off,G3 后) +## 3. 解析与深拷贝 + +循环里的 `json.loads` / `json.dumps` 与 `deepcopy` 会重复本可在循环外完成的工作(当值不变时)。变化的 payload 不能外提。这些 lint 是 hint,并有微基准。`re.compile` 已移除:CPython 会缓存,测得差距低于 3×。`datetime.strptime` / `strftime` 已移除:固定字符串仅在跳过调用时更快,变化的字符串没有可外提的东西。 + +循环里对同一路径 `open` 会反复打开。某一测量形态(1 字节文件)约 409×。每轮都变的路径不能外提。`time.perf_counter_ns` 约 2.9×,`getattr` 约 1.4×;已检查但未给规则。 + +Lint: `pp-json-hot`, `pp-deepcopy-hot`, `pp-open-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| pp-str-plus-hot | 循环内字符串 `+=` | off(G3) | +| pp-list-plus-rebind | 循环内 list `+` 重绑定 | hint | +| pp-print-hot | 循环内 `print(` | off(G3) | +| pp-json-hot | 循环内 `json.loads` / `dumps` | hint(有测量;未 corpus 分级) | +| pp-deepcopy-hot | 循环内 `deepcopy` | hint(有测量;未 corpus 分级) | + +仅在有 `evidence/pp-*.jsonl` 时升级——见 [evidence-policy.md](evidence-policy.md)。临时的 `re.METHOD(literal)` 规则(PP004)已移除;见 [rule-ids.md](rule-ids.md)。`pp-regex-compile-in-loop` 与 `pp-datetime-format-in-loop` 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/pyperf/docs/zh-CN/rule-ids.md b/packages/pyperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..58a776e --- /dev/null +++ b/packages/pyperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,32 @@ +# pyperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `pp-str-plus-hot` | +| `pp-list-plus-rebind` | +| `pp-print-hot` | +| `pp-json-hot` | +| `pp-deepcopy-hot` | +| `pp-open-hot` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `pp-deepcopy-in-loop` | `pp-deepcopy-hot` | +| `pp-json-in-loop` | `pp-json-hot` | +| `pp-open-in-loop` | `pp-open-hot` | +| `pp-print-in-loop` | `pp-print-hot` | +| `pp-str-plus-in-loop` | `pp-str-plus-hot` | + +## Removed (tombstone; never reuse) + +- `PP004` — see `evidence/_removed/` +- `pp-regex-compile-in-loop` — see `evidence/_removed/` +- `pp-datetime-format-in-loop` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/pyperf/evidence/zh-CN/README.md b/packages/pyperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..852f333 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/README.md @@ -0,0 +1,9 @@ +# pyperf 证据 + +2026-10-22 规则已经测量了填充(`.md` + `.jsonl`,使用 `examples/bench/evidence_pp.py`)。没有被升级:pp-str-plus-hot 和 pp-print-hot 保持 **关闭**,pp-list-plus-rebind 保持 **提示**。 + +`pp-json-hot` 和 `pp-deepcopy-hot` 有 JSONL 并保持 **hint**。`pp-regex-compile-in-loop` 和 `pp-datetime-format-in-loop` 已被移除;请参见 `_removed/`。 + +`PP004` 墓碑位于 `_removed/` 下。 + +请参阅 [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录下的 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/pyperf/evidence/zh-CN/pp-deepcopy-hot.md b/packages/pyperf/evidence/zh-CN/pp-deepcopy-hot.md new file mode 100644 index 0000000..6b690eb --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-deepcopy-hot.md @@ -0,0 +1,22 @@ +# pp-deepcopy-hot — 已测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[pp-deepcopy-hot.jsonl](../pp-deepcopy-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +5000 个同样的小嵌套字典副本。重复使用后,它会使用相同的对象而不是复制。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `reuse_tree` | 0.154042 | 5000 | 1 | +| 之前 | `deepcopy_each_iter` | 29.745167 | 5000 | 193.098 | + +CPython 3.15.1,2026-10-23。Warm 3,9的中位数。在读取相同的嵌套字段后不进行复制。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流处理。 diff --git a/packages/pyperf/evidence/zh-CN/pp-json-hot.md b/packages/pyperf/evidence/zh-CN/pp-json-hot.md new file mode 100644 index 0000000..4f236c6 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-json-hot.md @@ -0,0 +1,22 @@ +# pp-json-hot — 已测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[pp-json-hot.jsonl](../pp-json-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +2 万次加载一个固定的 JSON 对象。解析一次后再次读取该字段。每次迭代都会变化的有效负载不能在此结构之后使用。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `loads_once` | 0.699000 | 20000 | 1 | +| 之前 | `loads_each_iter` | 20.000584 | 20000 | 28.613 | + +CPython 3.15.1,2026-10-23。Warm 3,9 的中位数。解析一次后再次读取字段。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/pyperf/evidence/zh-CN/pp-list-plus-rebind.md b/packages/pyperf/evidence/zh-CN/pp-list-plus-rebind.md new file mode 100644 index 0000000..b86ab55 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-list-plus-rebind.md @@ -0,0 +1,35 @@ +# pp-list-plus-rebind — xs = xs + [i] 与 append + +## 主张 + +在这台机器上测量:主要热用例 **`list_plus_rebind`** 比率前/后 **~1872.93×**(之后更快)。默认严重性保持 **`hint`**。仅凭这一填充 **不要** 升级为 `error`——比率很高,但策略/默认值和 G3 OK-idiom 历史保持严重性不变。 + +## 如何测量 + +- 测试工具:`packages/pyperf/examples/bench/evidence_pp.py` +- 协议:warm=3;9轮的中位数;在适用的地方执行GC/收集 +- 原始数据行:[`pp-list-plus-rebind.jsonl`](../pp-list-plus-rebind.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **CPython 3.15.1** | +| 在 (`list_append`) 之后的中位数 | 0.302584 毫秒 (操作=20000,比率=1.000000) | +| `list_plus_rebind` 之前的中位数 | 566.717875 毫秒(操作数=20000,比例=1872.927988) | +| (`small_append`) 后的中位数 | 0.006208 毫秒 (操作=100,比例=1.000000) | +| 中位数之前 (`small_list_plus`) | 0.013167 毫秒 (操作次数=100,比率=2.120912) | +| **前后主要比率** | **~1872.93×** (`list_plus_rebind`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试套件。 + +## 与 lint 的对应 + +`pyperf-lint` **pp-list-plus-rebind** 默认保持 **`hint`**。推荐升级:**否(强烈的微基准测试,保持当前默认;升级需要政策候选 + 语料库一致性)**。 + +## 限制 + +- 微基准;不是应用程序性能概况。 +- 小行数的测试结果可能接近计时器噪声;在进行比率声明时,优先使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 依赖环境。 diff --git a/packages/pyperf/evidence/zh-CN/pp-open-hot.md b/packages/pyperf/evidence/zh-CN/pp-open-hot.md new file mode 100644 index 0000000..ac21480 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-open-hot.md @@ -0,0 +1,19 @@ +# pp-open-hot — 测量过的,仍有暗示 + +## 状态 +**提示**。实时时钟 JSONL:[pp-open-hot.jsonl](../pp-open-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +内置 `open` 对一个单字节文件进行 2000 次操作,与只打开一次进行比较。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在…之后 | `open_once` | 0.047792 | 2000 | 1 | +| 之前 | `open_each_iter` | 31.169750 | 2000 | 652.193 | + +CPython 3.15.1,2026-10-23。Warm 3,9 的中位数。基准重新打开一个单字节文件。 + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅凭此文件将默认值设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍需进行语料库分流。 diff --git a/packages/pyperf/evidence/zh-CN/pp-print-hot.md b/packages/pyperf/evidence/zh-CN/pp-print-hot.md new file mode 100644 index 0000000..e8f9f19 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-print-hot.md @@ -0,0 +1,33 @@ +# pp-print-hot — 循环打印 vs 仅 join 一次(标准输出被丢弃) + +## 主张 + +在这台机器上测量:主要热案例 **`print_in_loop`** 比例前/后 **~2.10×**(之后更快)。默认严重性保持 **`off`**。仅凭此填充不要升级为 `error`。 + +## 如何测量 + +- 测试工具: `packages/pyperf/examples/bench/evidence_pp.py` +- 协议: warm=3; 9轮的中位数; 在适用的地方进行GC/收集 +- 原始数据行: [`pp-print-hot.jsonl`](../pp-print-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **CPython 3.15.1** | +| (`print_join_once`) 之后的中位数 | 1.073375 毫秒 (操作次数=20000,比例=1.000000) | +| 之前的中值(`print_in_loop`) | 2.258042 毫秒(操作=20000,比例=2.103684) | +| **主要比率 前/后** | **~2.10×** (`print_in_loop`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试环境。 + +## 与 lint 的对应 + +`pyperf-lint` **pp-print-hot** 默认保持 **`关闭`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能概况。 +- 小行数的测试结果可能接近计时器噪声;在进行比率声明时,最好使用热点(非小规模)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 依赖环境。 diff --git a/packages/pyperf/evidence/zh-CN/pp-str-plus-hot.md b/packages/pyperf/evidence/zh-CN/pp-str-plus-hot.md new file mode 100644 index 0000000..e294087 --- /dev/null +++ b/packages/pyperf/evidence/zh-CN/pp-str-plus-hot.md @@ -0,0 +1,35 @@ +# pp-str-plus-hot — str += 与 str.join + +## 主张 + +在此机器上测量:主要热案例 **`str_plus`** 比率前/后约 **~11.60×**(之后更快)。默认严重性保持 **`off`**。仅凭此填充**不要**升级为 `error`。 + +## 如何测量 + +- 测试框架:`packages/pyperf/examples/bench/evidence_pp.py` +- 协议:预热=3;9轮的中位数;在适用的地方执行GC/收集 +- 原始数据文件:[`pp-str-plus-hot.jsonl`](../pp-str-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-23 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **CPython 3.15.1** | +| 中位数(`str_join` 之后) | 0.243541 毫秒(操作=50000,比率=1.000000) | +| 中位数之前 (`str_plus`) | 2.824209 毫秒 (操作=50000, 比率=11.596449) | +| `small_join` 后的中位数 | 0.003750 毫秒 (操作次数=100, 比率=1.000000) | +| 中位数之前 (`small_str_plus`) | 0.007625 毫秒 (操作=100, 比率=2.033404) | +| **主要比率 前/后** | **~11.60×** (`str_plus`) | + +方向和绝对毫秒数对机器/工具/选项敏感。在工具链或机器更改后,请重新运行测试工具。 + +## 与 lint 的对应 + +`pyperf-lint` **pp-str-plus-hot** 默认保持 **`关闭`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比率声明,建议使用热(非小)情况。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/pyperf/zh-CN/CONTRIBUTING.md b/packages/pyperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/pyperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/pyperf/zh-CN/README.md b/packages/pyperf/zh-CN/README.md new file mode 100644 index 0000000..1ce669e --- /dev/null +++ b/packages/pyperf/zh-CN/README.md @@ -0,0 +1,35 @@ +# pyperf + +Python 性能笔记和 `pyperf-lint`(不是 PyPI 微基准测试工具)。 + +[XPerf](../../../zh-CN/README.md) 的一部分。**语料库就绪**(G3 2026-10-22)。默认设置:pp-str-plus-hot/pp-print-hot **关闭**,pp-list-plus-rebind **提示**。删除了一条规则(墓碑,从未获得标识符)。 + +## 目录 + +- [英文指南](../docs/zh-CN/python-performance-guide.md) +- [`pyperf-lint`](../pyperf-lint/README.md) — pp-str-plus-hot / pp-list-plus-rebind / pp-print-hot +- [语料库固定](../docs/zh-CN/corpus.md) + +## 关注点 + +在性能关键的循环中,优先使用 `''.join(...)` 而不是 `s +=`,优先使用 `append` 而不是 `xs = xs + [...]`;避免在紧密循环中使用 `print`。 + +## 运行 lint + +```sh +export PYTHONPATH=packages/pyperf/pyperf-lint +python3 -m pyperf_lint path/to/sources +bash packages/pyperf/scripts/smoke-lint.sh +bash packages/pyperf/scripts/self-lint.sh +``` + +## 布局 + +```text +docs/guide-src/ chapter sources +docs/ assembled guide + corpus.md + evidence-policy.md +evidence/ pp-str-plus-hot, pp-list-plus-rebind, pp-print-hot measured (not escalated); PP004 tombstone +examples/ tiny demos + evidence bench +pyperf-lint/ checker (pyperf_lint package) + fixtures +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/rubyperf/docs/guide-src/00_head.md b/packages/rubyperf/docs/guide-src/00_head.md index 2f9c5b4..3848cb9 100644 --- a/packages/rubyperf/docs/guide-src/00_head.md +++ b/packages/rubyperf/docs/guide-src/00_head.md @@ -2,18 +2,14 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: MRI Ruby 3+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: MRI Ruby 3+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (`benchmark`, stackprof). -2. Treat each chapter as a checklist. -3. JRuby / TruffleRuby differ — re-bench. +Profile first (`benchmark`, stackprof). JRuby and TruffleRuby differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings, arrays, and hashes](#1-strings-arrays-and-hashes) - [2. Hot I/O](#2-hot-io) +- [3. Compile and parse](#3-compile-and-parse) - [Lint map](#lint-map) diff --git a/packages/rubyperf/docs/guide-src/01_builders.md b/packages/rubyperf/docs/guide-src/01_builders.md index f22e4d8..a116d3b 100644 --- a/packages/rubyperf/docs/guide-src/01_builders.md +++ b/packages/rubyperf/docs/guide-src/01_builders.md @@ -4,5 +4,5 @@ `Hash#merge` in a loop was removed. On Ruby 4.0.8 the suggested literal build was slower than `merge` (~0.78×, and again ~0.65×) and allocated about twice as many objects. See `evidence/_removed/rb-hash-merge-in-loop.REMOVED.md`. -Companion lint: **rb-string-plus-hot**, **rb-array-plus-rebind**. +Lint: `rb-string-plus-hot`, `rb-array-plus-rebind` (default off). diff --git a/packages/rubyperf/docs/guide-src/02_hot_io.md b/packages/rubyperf/docs/guide-src/02_hot_io.md index 9449f8e..524633f 100644 --- a/packages/rubyperf/docs/guide-src/02_hot_io.md +++ b/packages/rubyperf/docs/guide-src/02_hot_io.md @@ -2,5 +2,5 @@ `puts` / `print` in a tight loop flushes. Gate debug output or buffer. -Companion lint: **rb-puts-hot**. +Lint: `rb-puts-hot` (default off). diff --git a/packages/rubyperf/docs/guide-src/03_hot_calls.md b/packages/rubyperf/docs/guide-src/03_hot_calls.md index 4a994d2..04adcac 100644 --- a/packages/rubyperf/docs/guide-src/03_hot_calls.md +++ b/packages/rubyperf/docs/guide-src/03_hot_calls.md @@ -1,7 +1,8 @@ ## 3. Compile and parse -`Regexp.new` compiles on every call. A regexp literal is cached. `JSON.parse` / `generate` on a fixed payload repeats the parse. A changing payload cannot be hoisted. These lints are **hint** and have a microbench. `Date.parse` / `Time.parse` / `DateTime.parse` was removed for the same reason as a fixed-string parse: the after side only wins by not parsing. +`Regexp.new` compiles on every call. A regexp literal is cached. `JSON.parse` / `generate` on a fixed payload repeats the parse. A changing payload cannot be hoisted. These lints are hint and have a microbench. `Date.parse` / `Time.parse` / `DateTime.parse` was removed for the same reason as a fixed-string parse: the after side only wins by not parsing. `File.read` of one path was about 346× versus reading once. `File.open` is the same lint. A path that changes every iteration cannot be hoisted. `Process.clock_gettime` was about 2.3× and `send` about 1.9×; those were checked and not given a rule. -Companion lint: **rb-regexp-new-hot**, **rb-json-hot**, **rb-file-read-hot**. +Lint: `rb-regexp-new-hot`, `rb-json-hot`, `rb-file-read-hot` (default hint). + diff --git a/packages/rubyperf/docs/guide-src/zh-CN/00_head.md b/packages/rubyperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..d008970 --- /dev/null +++ b/packages/rubyperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Ruby 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:MRI Ruby 3+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`benchmark`、stackprof)。JRuby 与 TruffleRuby 不同——请重测。 + +## 目录 + +- [1. 字符串、数组与哈希](#1-字符串数组与哈希) +- [2. 热 I/O](#2-热-io) +- [3. 编译与解析](#3-编译与解析) +- [Lint 对照](#lint-对照) + diff --git a/packages/rubyperf/docs/guide-src/zh-CN/01_builders.md b/packages/rubyperf/docs/guide-src/zh-CN/01_builders.md new file mode 100644 index 0000000..9212899 --- /dev/null +++ b/packages/rubyperf/docs/guide-src/zh-CN/01_builders.md @@ -0,0 +1,7 @@ +## 1. 字符串、数组与哈希 + +`s += piece` 每次分配新字符串;优先 `s << piece`(或缓冲区)。`arr = arr + [x]` 会拷贝;优先 `<<` / `push`。 + +循环里的 `Hash#merge` 已移除。在 Ruby 4.0.8 上,建议的字面量构建比 `merge` 更慢(约 0.78×,再次约 0.65×),并大约多分配一倍对象。见 `evidence/_removed/rb-hash-merge-in-loop.REMOVED.md`。 + +Lint: `rb-string-plus-hot`, `rb-array-plus-rebind`(默认 off) diff --git a/packages/rubyperf/docs/guide-src/zh-CN/02_hot_io.md b/packages/rubyperf/docs/guide-src/zh-CN/02_hot_io.md new file mode 100644 index 0000000..516d1ad --- /dev/null +++ b/packages/rubyperf/docs/guide-src/zh-CN/02_hot_io.md @@ -0,0 +1,5 @@ +## 2. 热 I/O + +紧密循环里的 `puts` / `print` 会刷新。门控调试输出或做缓冲。 + +Lint: `rb-puts-hot`(默认 off) diff --git a/packages/rubyperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/rubyperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..d4f9941 --- /dev/null +++ b/packages/rubyperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,7 @@ +## 3. 编译与解析 + +`Regexp.new` 每次调用都编译。正则字面量会被缓存。对固定 payload 的 `JSON.parse` / `generate` 会重复解析。变化的 payload 不能外提。这些 lint 是 hint,并有微基准。`Date.parse` / `Time.parse` / `DateTime.parse` 已移除,原因与固定字符串解析相同:after 侧仅靠不解析才赢。 + +对同一路径 `File.read` 约 346×(相对读一次)。`File.open` 属同一 lint。每轮都变的路径不能外提。`Process.clock_gettime` 约 2.3×,`send` 约 1.9×;已检查但未给规则。 + +Lint: `rb-regexp-new-hot`, `rb-json-hot`, `rb-file-read-hot`(默认 hint) diff --git a/packages/rubyperf/docs/guide-src/zh-CN/99_tail.md b/packages/rubyperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..2655713 --- /dev/null +++ b/packages/rubyperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,15 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| rb-string-plus-hot | 循环内字符串 `+=` | off | +| rb-array-plus-rebind | 循环内 Array `+` / concat 重绑定 | off | +| rb-puts-hot | 循环内 `puts` / `print` | off | +| rb-regexp-new-hot | 循环内 `Regexp.new` | hint(有测量;未 corpus 分级) | +| rb-json-hot | 循环内 `JSON.parse` / `generate` | hint(有测量;未 corpus 分级) | + +用 `--severity =hint` 启用 `off` 规则。升为 `error` 仅在有测量的 `evidence/rb-*.jsonl`——见 [evidence-policy.md](evidence-policy.md)。`rb-date-parse-in-loop` 与 `rb-hash-merge-in-loop` 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/rubyperf/docs/ruby-performance-guide.md b/packages/rubyperf/docs/ruby-performance-guide.md index 35ee6f6..f82df79 100644 --- a/packages/rubyperf/docs/ruby-performance-guide.md +++ b/packages/rubyperf/docs/ruby-performance-guide.md @@ -2,19 +2,15 @@ Fewer quadratic builders, quieter hot paths. -> **Scope: MRI Ruby 3+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: MRI Ruby 3+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (`benchmark`, stackprof). -2. Treat each chapter as a checklist. -3. JRuby / TruffleRuby differ — re-bench. +Profile first (`benchmark`, stackprof). JRuby and TruffleRuby differ — re-bench. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Strings, arrays, and hashes](#1-strings-arrays-and-hashes) - [2. Hot I/O](#2-hot-io) +- [3. Compile and parse](#3-compile-and-parse) - [Lint map](#lint-map) ## 1. Strings, arrays, and hashes @@ -23,21 +19,22 @@ Fewer quadratic builders, quieter hot paths. `Hash#merge` in a loop was removed. On Ruby 4.0.8 the suggested literal build was slower than `merge` (~0.78×, and again ~0.65×) and allocated about twice as many objects. See `evidence/_removed/rb-hash-merge-in-loop.REMOVED.md`. -Companion lint: **rb-string-plus-hot**, **rb-array-plus-rebind**. +Lint: `rb-string-plus-hot`, `rb-array-plus-rebind` (default off). ## 2. Hot I/O `puts` / `print` in a tight loop flushes. Gate debug output or buffer. -Companion lint: **rb-puts-hot**. +Lint: `rb-puts-hot` (default off). ## 3. Compile and parse -`Regexp.new` compiles on every call. A regexp literal is cached. `JSON.parse` / `generate` on a fixed payload repeats the parse. A changing payload cannot be hoisted. These lints are **hint** and have a microbench. `Date.parse` / `Time.parse` / `DateTime.parse` was removed for the same reason as a fixed-string parse: the after side only wins by not parsing. +`Regexp.new` compiles on every call. A regexp literal is cached. `JSON.parse` / `generate` on a fixed payload repeats the parse. A changing payload cannot be hoisted. These lints are hint and have a microbench. `Date.parse` / `Time.parse` / `DateTime.parse` was removed for the same reason as a fixed-string parse: the after side only wins by not parsing. `File.read` of one path was about 346× versus reading once. `File.open` is the same lint. A path that changes every iteration cannot be hoisted. `Process.clock_gettime` was about 2.3× and `send` about 1.9×; those were checked and not given a rule. -Companion lint: **rb-regexp-new-hot**, **rb-json-hot**, **rb-file-read-hot**. +Lint: `rb-regexp-new-hot`, `rb-json-hot`, `rb-file-read-hot` (default hint). + ## Lint map | ID | Smell | Default | diff --git a/packages/rubyperf/docs/zh-CN/corpus.md b/packages/rubyperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..c75b732 --- /dev/null +++ b/packages/rubyperf/docs/zh-CN/corpus.md @@ -0,0 +1,43 @@ +# rubyperf 语料库目标 + +大型树木**不会**被转售,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +```sh +ruby packages/rubyperf/rubyperf-lint/rubyperf-lint.rb / --format json --no-fail +``` + +检查器:`rubyperf-lint`。2026-10-22 的分类覆盖了 rb-string-plus-hot–rb-hash-merge-in-loop。`rb-hash-merge-in-loop` 于 2026-10-23 被移除。 + +## 固定目标 + +上游默认分支的浅克隆;提交未被记录。 + +| Tree | URL | +|------|-----| +| Rails(ActiveSupport + ActiveRecord)| https://github.com/rails/rails | +| jekyll | https://github.com/jekyll/jekyll | +| sidekiq | https://github.com/sidekiq/sidekiq | + +## Lint 计数 — 之后 → r2 → r3(重新设计) + +| 树 | 纤维绒(之后 → r2 → **r3**) | +|------|------------------------------| +| **rails** AS+AR | **4 → 1 → 2** (rb-hash-merge-in-loop) | +| **jekyll** | **0 → 0 → 0** | +| **sidekiq** | **5 → 0 → 2** (rb-hash-merge-在循环中) | + +## G3 TP/FP 分诊(2026-10-22 亚洲/上海) + +| 阶段 | n 样本 | 真阳性 | 假阳性+文档+跳过 | 语料库准备好? | +|-------|----------|----|-------------|----------------| +| after/r1 | 9 | 0% | 33.3% | **no** | +| r2 | 1 | 0% | 0% | **no** (OK-idiom) | +| **r3** (rb-hash-merge-in-loop) | **4** | **75%** | **0%** | **是** | + +r2→r3:将 rb-string-plus-hot–rb-puts-hot 降级 → **关闭**;添加 **rb-hash-merge-in-loop**(提示)。测量的 G4 证据:见 [evidence-policy.md](evidence-policy.md)。 + +## 2026-10-23 — 提示规则不在 2026-10-22 判决范围内 + +2026-10-22 的语料库准备判决不包括以下子弹头。它们仍然保持 **提示** 状态,尚未经过 G2/G3。 + +- `rb-regexp-new-hot`、`rb-json-hot` — 磁盘上的 JSONL;仍然是提示 diff --git a/packages/rubyperf/docs/zh-CN/evidence-policy.md b/packages/rubyperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..43376fd --- /dev/null +++ b/packages/rubyperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,27 @@ +# rubyperf 证据政策 + +## 门槛 + +只有当以下两者都存在于 `packages/rubyperf/evidence/` 下时,规则才可被视为 **`error`**: + +1. `.md` — 声明,如何测量,工具链版本,限制 +2. `.jsonl` — ≥1 条前后行,包含 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts` + +否则规则保持 **`hint`**(或 **`off`**)。 + +仅测量的 JSONL 本身不会升级一个规则:被标记的问题还必须在 G3 语料库分类中成立(参见 [corpus.md](corpus.md))。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| rb-string-plus-hot | 关闭 | **是** (`rb-string-plus-hot.md` + `.jsonl`, ~32×) | 循环中的字符串 `+=`(建议使用 `<<`) | +| rb-array-plus-rebind | 关闭 | **是** (`rb-array-plus-rebind.md` + `.jsonl`, ~181×) | 循环中数组 `+` / concat 重新绑定 | +| rb-puts-hot | 关闭 | **是** (`rb-puts-hot.md` + `.jsonl`, ~5×; I/O 依赖于环境) | 循环中的 `puts` / `print` | +| rb-regexp-new-hot | 提示 | 已测量(`rb-regexp-new-hot.md` + `rb-regexp-new-hot.jsonl`);保持提示 | 循环中的 `Regexp.new`; 未进行语料库分流 | +| rb-json-hot | 提示 | 测量(`rb-json-hot.md` + `rb-json-hot.jsonl`);保留提示 | 循环中使用 `JSON.parse` / `generate`;未经过语料库筛选 | +| rb-file-read-hot | 提示 | 测量 (`rb-file-read-hot.md` + `rb-file-read-hot.jsonl`);保持提示 | 循环中使用 `File.read` / `File.open`;仅相同路径;未经过语料排查 | + +没有规则是 `错误`:这些比率是真的,但在 G3 OK-idiom 分类后,三个 `off` 规则被降级。`rb-hash-merge-in-loop` 被移除了:在 Ruby 4.0.8 上,字面量构建比 `merge` 慢,并且分配更多。 diff --git a/packages/rubyperf/docs/zh-CN/ruby-performance-guide.md b/packages/rubyperf/docs/zh-CN/ruby-performance-guide.md new file mode 100644 index 0000000..085441e --- /dev/null +++ b/packages/rubyperf/docs/zh-CN/ruby-performance-guide.md @@ -0,0 +1,49 @@ +# Ruby 性能指南(v0) + +少做平方级 builder,让热路径更安静。 + +> **范围:MRI Ruby 3+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`benchmark`、stackprof)。JRuby 与 TruffleRuby 不同——请重测。 + +## 目录 + +- [1. 字符串、数组与哈希](#1-字符串数组与哈希) +- [2. 热 I/O](#2-热-io) +- [3. 编译与解析](#3-编译与解析) +- [Lint 对照](#lint-对照) + +## 1. 字符串、数组与哈希 + +`s += piece` 每次分配新字符串;优先 `s << piece`(或缓冲区)。`arr = arr + [x]` 会拷贝;优先 `<<` / `push`。 + +循环里的 `Hash#merge` 已移除。在 Ruby 4.0.8 上,建议的字面量构建比 `merge` 更慢(约 0.78×,再次约 0.65×),并大约多分配一倍对象。见 `evidence/_removed/rb-hash-merge-in-loop.REMOVED.md`。 + +Lint: `rb-string-plus-hot`, `rb-array-plus-rebind`(默认 off) +## 2. 热 I/O + +紧密循环里的 `puts` / `print` 会刷新。门控调试输出或做缓冲。 + +Lint: `rb-puts-hot`(默认 off) +## 3. 编译与解析 + +`Regexp.new` 每次调用都编译。正则字面量会被缓存。对固定 payload 的 `JSON.parse` / `generate` 会重复解析。变化的 payload 不能外提。这些 lint 是 hint,并有微基准。`Date.parse` / `Time.parse` / `DateTime.parse` 已移除,原因与固定字符串解析相同:after 侧仅靠不解析才赢。 + +对同一路径 `File.read` 约 346×(相对读一次)。`File.open` 属同一 lint。每轮都变的路径不能外提。`Process.clock_gettime` 约 2.3×,`send` 约 1.9×;已检查但未给规则。 + +Lint: `rb-regexp-new-hot`, `rb-json-hot`, `rb-file-read-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| rb-string-plus-hot | 循环内字符串 `+=` | off | +| rb-array-plus-rebind | 循环内 Array `+` / concat 重绑定 | off | +| rb-puts-hot | 循环内 `puts` / `print` | off | +| rb-regexp-new-hot | 循环内 `Regexp.new` | hint(有测量;未 corpus 分级) | +| rb-json-hot | 循环内 `JSON.parse` / `generate` | hint(有测量;未 corpus 分级) | + +用 `--severity =hint` 启用 `off` 规则。升为 `error` 仅在有测量的 `evidence/rb-*.jsonl`——见 [evidence-policy.md](evidence-policy.md)。`rb-date-parse-in-loop` 与 `rb-hash-merge-in-loop` 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/rubyperf/docs/zh-CN/rule-ids.md b/packages/rubyperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..9b63f7f --- /dev/null +++ b/packages/rubyperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,31 @@ +# rubyperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `rb-string-plus-hot` | +| `rb-array-plus-rebind` | +| `rb-puts-hot` | +| `rb-regexp-new-hot` | +| `rb-json-hot` | +| `rb-file-read-hot` | + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `rb-file-read-in-loop` | `rb-file-read-hot` | +| `rb-json-in-loop` | `rb-json-hot` | +| `rb-puts-in-loop` | `rb-puts-hot` | +| `rb-regexp-new-in-loop` | `rb-regexp-new-hot` | +| `rb-string-plus-in-loop` | `rb-string-plus-hot` | + +## Removed (tombstone; never reuse) + +- `rb-date-parse-in-loop` — see `evidence/_removed/` +- `rb-hash-merge-in-loop` — see `evidence/_removed/` + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 diff --git a/packages/rubyperf/evidence/zh-CN/README.md b/packages/rubyperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..f65f689 --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/README.md @@ -0,0 +1,12 @@ +# rubyperf 证据 + +每条规则都有一个索赔文件和从 `examples/bench/evidence_rb.rb` 测量的 JSONL: + +- `rb-string-plus-hot` — `.md` + `.jsonl` +- `rb-array-plus-rebind` — `.md` + `.jsonl` +- `rb-puts-hot` — `.md` + `.jsonl` +- `rb-hash-merge-in-loop` 已被移除;请参见 `_removed/` + +`rb-regexp-new-hot` 和 `rb-json-hot` 拥有 JSONL 并保持 **提示**。`rb-date-parse-in-loop` 已被移除;请参见 `_removed/`。 + +默认值和升级规则:[`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-array-plus-rebind.md b/packages/rubyperf/evidence/zh-CN/rb-array-plus-rebind.md new file mode 100644 index 0000000..7d42570 --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-array-plus-rebind.md @@ -0,0 +1,35 @@ +# rb-array-plus-rebind — a = a + [x] 与 << + +## 主张 + +在这台机器上测量:主要热案例 **`array_plus_rebind`** 的前/后比约为 **~131.34×**(之后更快)。默认严重性保持 **`off`**。仅凭这一优化不要升级为 `error` — 比例虽然很高,但策略/默认设置以及 G3 OK-idiom 历史保持严重性不变。 + +## 如何测量 + +- 测试套件:`packages/rubyperf/examples/bench/evidence_rb.rb` +- 协议:warm=3;9轮的中位数;在适用的情况下进行GC/收集 +- 原始文件:[`rb-array-plus-rebind.jsonl`](../rb-array-plus-rebind.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **Ruby 4.0.8** | +| 使用 (`shovel`) 后的中位数 | 0.441000 毫秒 (操作=20000,比率=1.000000) | +| `array_plus_rebind` 之前的中位数 | 57.921000 毫秒 (操作次数=20000, 比率=131.340176) | +| (`small_shovel`) 后的中位数 | 0.003000 毫秒 (操作=100,比率=1.000000) | +| 中位数之前 (`small_array_plus`) | 0.005000 毫秒 (操作=100, 比率=1.666615) | +| **主要比率 前/后** | **~131.34×** (`array_plus_rebind`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试套件。 + +## 与 lint 的对应 + +`rubyperf-lint` **rb-array-plus-rebind** 默认保持 **`off`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能分析。 +- 小样本行数可能接近计时器噪声;对于比例声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)会丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-file-read-hot.md b/packages/rubyperf/evidence/zh-CN/rb-file-read-hot.md new file mode 100644 index 0000000..e827997 --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-file-read-hot.md @@ -0,0 +1,19 @@ +# rb-file-read-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[rb-file-read-hot.jsonl](../rb-file-read-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +一个单字节文件的 `File.read`,测量 2000 次,与只读取一次相比。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在…之后 | `read_once` | 0.076000 | 2000 | 1 | +| 之前 | `read_each_iter` | 18.897000 | 2000 | 248.644 | + +Ruby 4.0.8,2026-10-23。温度 3,9 的中位数。 + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅凭此文件将默认值设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍需进行语料库分流。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-json-hot.md b/packages/rubyperf/evidence/zh-CN/rb-json-hot.md new file mode 100644 index 0000000..ece90cf --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-json-hot.md @@ -0,0 +1,22 @@ +# rb-json-hot — 测量过的,仍有暗示 + +## 状态 + +**提示**。实时时钟 JSONL:[rb-json-hot.jsonl](../rb-json-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识符。 + +## 测量 + +固定 JSON 对象解析 20000 次。解析一次后。形状改变的有效负载之后不能使用此方法。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `parse_once` | 0.781000 | 20000 | 1 | +| 之前 | `parse_each_iter` | 6.116000 | 20000 | 7.831 | + +Ruby 4.0.8,2026-10-23。温度 3,9 的中位数。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-puts-hot.md b/packages/rubyperf/evidence/zh-CN/rb-puts-hot.md new file mode 100644 index 0000000..650abfb --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-puts-hot.md @@ -0,0 +1,33 @@ +# rb-puts-hot — 每次用 puts 与 用 join 一次 + +## 主张 + +在这台机器上测量:主要热案例 **`puts_each`** 比率前/后约 **3.58×**(之后更快)。默认严重性保持 **`off`**。仅凭此填充不要升级到 `error`。 + +## 如何测量 + +- 测试工具:`packages/rubyperf/examples/bench/evidence_rb.rb` +- 协议:预热=3;9轮的中位数;在适用的地方进行垃圾收集/收集 +- 原始数据行:[`rb-puts-hot.jsonl`](../rb-puts-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL 时间戳是 UTC) | +| Toolchain | **Ruby 4.0.8** | +| 中位数(`join_once` 之后) | 1.149000 毫秒(操作=20000,比率=1.000000) | +| 中位数之前 (`puts_each`) | 4.110000 毫秒 (操作=20000, 比率=3.577023) | +| **前/后主要比率** | **~3.58×** (`puts_each`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试环境。 + +## 与 lint 的对应 + +`rubyperf-lint` **rb-puts-hot** 默认保持 **`关闭`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序配置文件。 +- 小 N 行可能接近计时器噪声;对于比例声明,更喜欢热(非小)情况。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 依赖环境。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-regexp-new-hot.md b/packages/rubyperf/evidence/zh-CN/rb-regexp-new-hot.md new file mode 100644 index 0000000..681d510 --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-regexp-new-hot.md @@ -0,0 +1,22 @@ +# rb-regexp-new-hot — 已测量,仍然提示 + +## 状态 + +**提示**。实时时钟 JSONL:[rb-regexp-new-hot.jsonl](../rb-regexp-new-hot.jsonl)。未进行语料库分拣。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 + +5000 次固定字符串的匹配。在每次迭代前使用 Regexp.new;之后只编译一次。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在…之后 | `regexp_once` | 0.565000 | 5000 | 1 | +| 之前 | `regexp_each_iter` | 2.041000 | 5000 | 3.612 | + +Ruby 4.0.8,2026-10-23。温度 3,9 的中位数。 + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/rubyperf/evidence/zh-CN/rb-string-plus-hot.md b/packages/rubyperf/evidence/zh-CN/rb-string-plus-hot.md new file mode 100644 index 0000000..5f5ac3d --- /dev/null +++ b/packages/rubyperf/evidence/zh-CN/rb-string-plus-hot.md @@ -0,0 +1,35 @@ +# rb-string-plus-hot — string += 与 << + +## 主张 + +在这台机器上测量:主热案例 **`str_plus`** 比例前/后约 **~53.79×**(之后更快)。默认严重性保持 **`off`**。单凭此填充不要升级为 `error`——比例很高,但策略/默认设置和 G3 OK-惯用历史保持严重性不变。 + +## 如何测量 + +- 测试工具:`packages/rubyperf/examples/bench/evidence_rb.rb` +- 协议:预热=3;9轮的中位数;在适用的地方进行垃圾收集/收集 +- 原始数据行:[`rb-string-plus-hot.jsonl`](../rb-string-plus-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL 时间戳为 UTC) | +| Toolchain | **Ruby 4.0.8** | +| 使用 (`shovel`) 后的中位数 | 1.164000 毫秒 (操作=50000,比率=1.000000) | +| 中位数之前 (`str_plus`) | 62.611000 毫秒 (操作=50000, 比率=53.789518) | +| 使用 (`small_shovel`) 后的中位数 | 0.003000 毫秒 (操作=100,比率=1.000000) | +| 中位数之前 (`small_str_plus`) | 0.005000 毫秒 (操作=100, 比率=1.666718) | +| **主要比率 前/后** | **~53.79×** (`str_plus`) | + +方向和绝对毫秒数对机器/工具链/选项敏感。在工具链或机器更改后,请重新运行测试套件。 + +## 与 lint 的对应 + +`rubyperf-lint` **rb-string-plus-hot** 默认保持 **`关闭`**。推荐升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能概况。 +- 小行数可能接近计时器噪声;对于比例声明,建议使用热(非小)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null` — 取决于环境。 diff --git a/packages/rubyperf/zh-CN/CONTRIBUTING.md b/packages/rubyperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/rubyperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/rubyperf/zh-CN/README.md b/packages/rubyperf/zh-CN/README.md new file mode 100644 index 0000000..a439c57 --- /dev/null +++ b/packages/rubyperf/zh-CN/README.md @@ -0,0 +1,37 @@ +# rubyperf + +Ruby 性能笔记、小示例,以及 `rubyperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。lint **corpus-ready**(启发式)。默认:rb-string-plus-hot、rb-array-plus-rebind、rb-puts-hot **off**;regexp / JSON / file-read **hint**。`rb-hash-merge-in-loop` 已移除。指南 v0。 + +## 目录 + +- [英文指南(v0)](../docs/zh-CN/ruby-performance-guide.md) — 来自 `docs/guide-src/` +- [`rubyperf-lint`](../rubyperf-lint/README.md) — rb-string-plus-hot–rb-file-read-hot +- [语料固定目标](../docs/zh-CN/corpus.md) + +## 关注点 + +优先 `<<` 而非字符串 `+=`,避免循环里 Array `+` 重绑定,门控热路径 `puts`/`print`。 + +## 运行 lint + +```sh +cd packages/rubyperf +ruby rubyperf-lint/rubyperf-lint.rb path/to/sources +bash scripts/smoke-lint.sh +bash scripts/self-lint.sh +``` + +仅 **error** 级发现时退出 `1`(hint 退出 `0`,除非 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ 章节源 +docs/ 组装后的指南 + corpus.md + evidence-policy.md +evidence/ 每规则主张 + 实测 JSONL(全部四条规则) +examples/ 小演示 +rubyperf-lint/ 检查器 + 夹具 +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/rustperf/docs/guide-src/00_head.md b/packages/rustperf/docs/guide-src/00_head.md index c68febd..f0238ab 100644 --- a/packages/rustperf/docs/guide-src/00_head.md +++ b/packages/rustperf/docs/guide-src/00_head.md @@ -2,18 +2,14 @@ Less allocator chatter, fewer surprise string / print stalls. -> **Scope: Rust edition 2021+.** Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Rust edition 2021+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (`cargo bench`, `criterion`, `perf`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. LLVM optimizations change costs — re-bench after toolchain bumps. +Profile first (`cargo bench`, `criterion`, `perf`). LLVM optimizations change costs — re-bench after toolchain bumps. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Allocation and clone](#1-allocation-and-clone) - [2. Strings and hot I/O](#2-strings-and-hot-io) +- [3. Regex](#3-regex) - [Lint map](#lint-map) diff --git a/packages/rustperf/docs/guide-src/01_allocation.md b/packages/rustperf/docs/guide-src/01_allocation.md index 89a15d7..e85fddf 100644 --- a/packages/rustperf/docs/guide-src/01_allocation.md +++ b/packages/rustperf/docs/guide-src/01_allocation.md @@ -8,3 +8,5 @@ let mut v = Vec::with_capacity(n); for i in 0..n { v.push(i); } ``` +Lint: `rs-alloc-hot` (default off after G3; G4 measured). + diff --git a/packages/rustperf/docs/guide-src/02_strings.md b/packages/rustperf/docs/guide-src/02_strings.md index 59c1c59..3d59011 100644 --- a/packages/rustperf/docs/guide-src/02_strings.md +++ b/packages/rustperf/docs/guide-src/02_strings.md @@ -1,6 +1,8 @@ ## 2. Strings and hot I/O -`format!("{s}…")` / `s = format!(…)` accumulation in a loop is alloc-heavy (copies the growing prefix) — **rs-format-rebind-hot**. Prefer `with_capacity` + `write!`, or collect then join. Plain `+=` / `push_str` on one growing `String` was about 1× versus `with_capacity` on a large fill, so that lint was removed. Keep `println!` / `eprintln!` out of tight loops (gate or buffer). +`format!("{s}…")` / `s = format!(…)` accumulation in a loop is alloc-heavy (copies the growing prefix). Prefer `with_capacity` + `write!`, or collect then join. Plain `+=` / `push_str` on one growing `String` was about 1× versus `with_capacity` on a large fill, so that lint was removed. Keep `println!` / `eprintln!` out of tight loops (gate or buffer). -A large `Vec::new` plus `push` was about 1× versus `with_capacity`, so that lint was removed. Avoid `.collect::>()` inside per-item loops when a reused buffer or streaming count will do (**rs-collect-vec-hot**). +A large `Vec::new` plus `push` was about 1× versus `with_capacity`, so that lint was removed. Avoid `.collect::>()` inside per-item loops when a reused buffer or streaming count will do. + +Lint: `rs-format-rebind-hot` (default error; G4: ~99× vs `write!`); `rs-println-hot`, `rs-collect-vec-hot` (default hint). diff --git a/packages/rustperf/docs/guide-src/03_hot_calls.md b/packages/rustperf/docs/guide-src/03_hot_calls.md index 4be6462..dc16130 100644 --- a/packages/rustperf/docs/guide-src/03_hot_calls.md +++ b/packages/rustperf/docs/guide-src/03_hot_calls.md @@ -1,7 +1,8 @@ ## 3. Regex -`Regex::new` inside a loop recompiles the pattern. On `regex` 1.13.1 and rustc 1.99, constructing every iteration and matching a short haystack was about 963× versus constructing once (both sides still match). This lint is **hint**. `HashMap::new` plus one insert, 20000 times, was about 1.5× versus clearing one map on two quiet rustc 1.99 runs (`rs-map-new-hot`, hint). +`Regex::new` inside a loop recompiles the pattern. On `regex` 1.13.1 and rustc 1.99, constructing every iteration and matching a short haystack was about 963× versus constructing once (both sides still match). This lint is hint. `HashMap::new` plus one insert, 20000 times, was about 1.5× versus clearing one map on two quiet rustc 1.99 runs (`rs-map-new-hot`, hint). `File::open` of one path was about 416× on rustc 1.99 versus opening once. `Instant::now` was about 20× on rustc 1.99 versus one sample. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. -Companion lint: **rs-regex-new-hot**, **rs-file-open-hot**, **rs-clock-hot**. +Lint: `rs-regex-new-hot`, `rs-file-open-hot`, `rs-clock-hot` (default hint). + diff --git a/packages/rustperf/docs/guide-src/99_tail.md b/packages/rustperf/docs/guide-src/99_tail.md index 0a96169..65e9511 100644 --- a/packages/rustperf/docs/guide-src/99_tail.md +++ b/packages/rustperf/docs/guide-src/99_tail.md @@ -4,11 +4,11 @@ |----|-------|---------| | rs-alloc-hot | alloc/clone in loop | off (G3 OK-idiom; G4 measured) | | rs-println-hot | println! / eprintln! in loop | hint | -| rs-format-rebind-hot | `format!` rebind accum in loop | **error** (G4: ~99× vs `write!`) | +| rs-format-rebind-hot | `format!` rebind accum in loop | error (G4: ~99× vs `write!`) | | rs-collect-vec-hot | collect:: in loop | hint | | rs-regex-new-hot | `Regex::new` in loop | hint (JSONL about 963× on a short haystack) | -See [evidence-policy.md](evidence-policy.md). Demote rs-format-rebind-hot with `--severity rs-format-rebind-hot=hint` if one-shot wraps dominate. Amortized `+=` and large `Vec::new` plus push were removed. +See [evidence-policy.md](evidence-policy.md). Demote `rs-format-rebind-hot` with `--severity rs-format-rebind-hot=hint` if one-shot wraps dominate. Amortized `+=` and large `Vec::new` plus push were removed. ## References diff --git a/packages/rustperf/docs/guide-src/zh-CN/00_head.md b/packages/rustperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..17796d4 --- /dev/null +++ b/packages/rustperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Rust 性能指南(v0) + +少一点分配器噪声,少一点意外的字符串 / 打印停顿。 + +> **范围:Rust edition 2021+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`cargo bench`、`criterion`、`perf`)。LLVM 优化会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. 分配与 clone](#1-分配与-clone) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [3. Regex](#3-regex) +- [Lint 对照](#lint-对照) + diff --git a/packages/rustperf/docs/guide-src/zh-CN/01_allocation.md b/packages/rustperf/docs/guide-src/zh-CN/01_allocation.md new file mode 100644 index 0000000..13ff22e --- /dev/null +++ b/packages/rustperf/docs/guide-src/zh-CN/01_allocation.md @@ -0,0 +1,11 @@ +## 1. 分配与 clone + +画像显示有压力时,热循环里优先复用与容量,而不是 `Vec::new` / `.clone()` / `.to_string()`。 + +```rust +// prefer: pre-sized or reused buffer +let mut v = Vec::with_capacity(n); +for i in 0..n { v.push(i); } +``` + +Lint: `rs-alloc-hot`(默认 off,G3 后;G4 有测量) diff --git a/packages/rustperf/docs/guide-src/zh-CN/02_strings.md b/packages/rustperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..5bb297e --- /dev/null +++ b/packages/rustperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,7 @@ +## 2. 字符串与热 I/O + +循环里用 `format!("{s}…")` / `s = format!(…)` 累加分配很重(会拷贝不断变长的前缀)——**rs-format-rebind-hot**。优先 `with_capacity` + `write!`,或先收集再 join。对单一增长的 `String` 做普通 `+=` / `push_str`,在大填充上相对 `with_capacity` 约 1×,因此该 lint 已移除。不要把 `println!` / `eprintln!` 放进紧密循环(门控或缓冲)。 + +大 `Vec::new` 再 `push` 相对 `with_capacity` 约 1×,该 lint 已移除。逐项循环里避免 `.collect::>()`,若复用缓冲区或流式计数即可(**rs-collect-vec-hot**)。 + +Lint: `rs-format-rebind-hot`(default error; G4: ~99× vs `write!`); `rs-println-hot`, `rs-collect-vec-hot`(默认 hint) diff --git a/packages/rustperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/rustperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..8ed4122 --- /dev/null +++ b/packages/rustperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,7 @@ +## 3. Regex + +循环里的 `Regex::new` 会反复编译模式。在 `regex` 1.13.1 与 rustc 1.99 上,每轮构造再匹配短 haystack,相对构造一次约 963×(两侧仍匹配)。该 lint 为 hint。`HashMap::new` 加一次 insert,重复 20000 次,相对清空同一 map 约 1.5×(两次安静的 rustc 1.99 运行;`rs-map-new-hot`,hint)。 + +对同一路径 `File::open` 在 rustc 1.99 上约 416×(相对打开一次)。`Instant::now` 约 20×(相对采一次样)。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +Lint: `rs-regex-new-hot`, `rs-file-open-hot`, `rs-clock-hot`(默认 hint) diff --git a/packages/rustperf/docs/guide-src/zh-CN/99_tail.md b/packages/rustperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..34c22d9 --- /dev/null +++ b/packages/rustperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,15 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| rs-alloc-hot | 循环内 alloc/clone | off(G3 OK-idiom;G4 有测量) | +| rs-println-hot | 循环内 println! / eprintln! | hint | +| rs-format-rebind-hot | 循环内 `format!` 重绑定累加 | error(G4:相对 `write!` 约 99×) | +| rs-collect-vec-hot | 循环内 collect:: | hint | +| rs-regex-new-hot | 循环内 `Regex::new` | hint(短 haystack 上 JSONL 约 963×) | + +见 [evidence-policy.md](evidence-policy.md)。若一次性包装占主导,可用 `--severity rs-format-rebind-hot=hint` 降级。摊销的 `+=` 与大 `Vec::new` + push 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/rustperf/docs/rust-performance-guide.md b/packages/rustperf/docs/rust-performance-guide.md index 99537fe..805ec65 100644 --- a/packages/rustperf/docs/rust-performance-guide.md +++ b/packages/rustperf/docs/rust-performance-guide.md @@ -2,19 +2,15 @@ Less allocator chatter, fewer surprise string / print stalls. -> **Scope: Rust edition 2021+.** Claims that assert speedups name a local example, an upstream source, or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Rust edition 2021+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (`cargo bench`, `criterion`, `perf`). -2. Treat each chapter as a checklist, not a rewrite mandate. -3. LLVM optimizations change costs — re-bench after toolchain bumps. +Profile first (`cargo bench`, `criterion`, `perf`). LLVM optimizations change costs — re-bench after toolchain bumps. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Allocation and clone](#1-allocation-and-clone) - [2. Strings and hot I/O](#2-strings-and-hot-io) +- [3. Regex](#3-regex) - [Lint map](#lint-map) ## 1. Allocation and clone @@ -27,30 +23,35 @@ let mut v = Vec::with_capacity(n); for i in 0..n { v.push(i); } ``` +Lint: `rs-alloc-hot` (default off after G3; G4 measured). + ## 2. Strings and hot I/O -`format!("{s}…")` / `s = format!(…)` accumulation in a loop is alloc-heavy (copies the growing prefix) — **rs-format-rebind-hot**. Prefer `with_capacity` + `write!`, or collect then join. Plain `+=` / `push_str` on one growing `String` was about 1× versus `with_capacity` on a large fill, so that lint was removed. Keep `println!` / `eprintln!` out of tight loops (gate or buffer). +`format!("{s}…")` / `s = format!(…)` accumulation in a loop is alloc-heavy (copies the growing prefix). Prefer `with_capacity` + `write!`, or collect then join. Plain `+=` / `push_str` on one growing `String` was about 1× versus `with_capacity` on a large fill, so that lint was removed. Keep `println!` / `eprintln!` out of tight loops (gate or buffer). -A large `Vec::new` plus `push` was about 1× versus `with_capacity`, so that lint was removed. Avoid `.collect::>()` inside per-item loops when a reused buffer or streaming count will do (**rs-collect-vec-hot**). +A large `Vec::new` plus `push` was about 1× versus `with_capacity`, so that lint was removed. Avoid `.collect::>()` inside per-item loops when a reused buffer or streaming count will do. + +Lint: `rs-format-rebind-hot` (default error; G4: ~99× vs `write!`); `rs-println-hot`, `rs-collect-vec-hot` (default hint). ## 3. Regex -`Regex::new` inside a loop recompiles the pattern. On `regex` 1.13.1 and rustc 1.99, constructing every iteration and matching a short haystack was about 963× versus constructing once (both sides still match). This lint is **hint**. `HashMap::new` plus one insert, 20000 times, was about 1.5× versus clearing one map on two quiet rustc 1.99 runs (`rs-map-new-hot`, hint). +`Regex::new` inside a loop recompiles the pattern. On `regex` 1.13.1 and rustc 1.99, constructing every iteration and matching a short haystack was about 963× versus constructing once (both sides still match). This lint is hint. `HashMap::new` plus one insert, 20000 times, was about 1.5× versus clearing one map on two quiet rustc 1.99 runs (`rs-map-new-hot`, hint). `File::open` of one path was about 416× on rustc 1.99 versus opening once. `Instant::now` was about 20× on rustc 1.99 versus one sample. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist. -Companion lint: **rs-regex-new-hot**, **rs-file-open-hot**, **rs-clock-hot**. +Lint: `rs-regex-new-hot`, `rs-file-open-hot`, `rs-clock-hot` (default hint). + ## Lint map | ID | Smell | Default | |----|-------|---------| | rs-alloc-hot | alloc/clone in loop | off (G3 OK-idiom; G4 measured) | | rs-println-hot | println! / eprintln! in loop | hint | -| rs-format-rebind-hot | `format!` rebind accum in loop | **error** (G4: ~99× vs `write!`) | +| rs-format-rebind-hot | `format!` rebind accum in loop | error (G4: ~99× vs `write!`) | | rs-collect-vec-hot | collect:: in loop | hint | | rs-regex-new-hot | `Regex::new` in loop | hint (JSONL about 963× on a short haystack) | -See [evidence-policy.md](evidence-policy.md). Demote rs-format-rebind-hot with `--severity rs-format-rebind-hot=hint` if one-shot wraps dominate. Amortized `+=` and large `Vec::new` plus push were removed. +See [evidence-policy.md](evidence-policy.md). Demote `rs-format-rebind-hot` with `--severity rs-format-rebind-hot=hint` if one-shot wraps dominate. Amortized `+=` and large `Vec::new` plus push were removed. ## References diff --git a/packages/rustperf/docs/zh-CN/corpus.md b/packages/rustperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..0af1ce7 --- /dev/null +++ b/packages/rustperf/docs/zh-CN/corpus.md @@ -0,0 +1,49 @@ +# rustperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任意本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +| 树 | 链接 | 提交 | 大小 | Lint 范围 | +|------|-----|--------|------|------------| +| serde | https://github.com/serde-rs/serde.git | `0db9b3b504bcb0a7fa9cf001429869519afd3e9a` | 约3.1M | 全部树 | +| tokio | https://github.com/tokio-rs/tokio.git | `cffdc7601134598cebc45d03af0358dedfcb3ff1` | ~7.9M 稀疏 | `tokio/` 锥 | +| 拍手 | https://github.com/clap-rs/clap.git | `ccc89b83875474399e9d9d70841163f6319d1a41` | ~6.5M | 整个树 | + +浅克隆(`--depth 1`)。Tokio 仅对 `tokio/` crate 使用稀疏检出。 + +## Dogfood + G3(通过 r4 重新设计,2026-10-22 亚洲/上海) + +| 树 | 原始 | r1 | r2 | r3-分裂 | r3(rs-alloc-hot 关闭) | **r4(rs-vec-no-capacity/rs-collect-vec-hot)** | +|------|-----|----|----|----------|----------------|----------------------| +| serde | **64** | 43 | 29 | 11 | **0** | **0** | +| tokio | **148** | 15 | 3 | 3 | **0** | **0** | +| 拍手 | **287** | 124 | 78 | 69 | **5** | **6** (rs-collect-vec-hot:2 rs-vec-no-capacity:3 rs-string-plus-in-loop:1) | + +规则顺序:rs-alloc-hot / rs-string-plus-in-loop / rs-println-hot / rs-format-rebind-hot / **rs-vec-no-capacity** / **rs-collect-vec-hot**。 + +**语料库准备就绪: 是** — r4 n=6 全部 **TP**(拍手包裹收集 + 大小为 `Vec::new` + push + 值名 `push_str`);FP+DOC+SKIP **0%**;在默认设置下 serde/tokio 干净。 + +### 反向优化(`rustperf-lint`) + +1. `-----路径 ' :** 使基线垃圾场无法逆向选择。 +2. **Path跳过:** ' tests/` ' tests/`benches/`... + **` tests.rs`** + inline **`# [cfg(test)]'/`# [test]`**. +3. ** " 环头 " :** 真正的 " 仅用于 " / " 同时 " / " 仅用于 " 类型 " 或 " ``` ' a>` " 的X。 +4. **rs-alloc-hot 默认 ' off`** - id/`to own'/syn clones 占主导地位;`-severity rs-alloc-hot=hint ' 供审查。 +5. **rs-string-plus-in-loop:** / 'push str ' only(**hint**);跳过贝壳/clap complete/mangen;跳过错误/帮助/样式/调试;微秒;跳过`mid string'/`sc names'种子在`command.rs'中。 +6. **rs-format-rebind-hot:**`dent = format! (.)' 重新组合再利用(**error**,G4~99×);跳过一发`'``dident''`括号内包装。 +7. **rs-println-hot:** 跳过例/demo bins,`cfg!(debug asserments)',`counts.rs'. +8. **rs-vec-no-容量:**空的`Vec ' /`String ' /`vec! ' 然后以**尺寸-inted** 环(**hint**);跳跃衍生箱+冷CLI。 +9. **rs-collect-vec-hot:** `collect:在真实的循环内(**hint**);保留文本包装/样式 str;跳过冷的CLI模板/壳。 + +### 残余(非速度声明) + +- r4 仅拍手:wrap-path 收集 + 有大小的冲突/子命令 vec 构建器 + 参数 value-name `push_str` — 均已分类 **TP**。 +- rs-vec-no-capacity/rs-collect-vec-hot 保持 **提示**:测量的 G4 比例适中(见 [evidence-policy.md](evidence-policy.md))。 +- 不得编造比例。 + +不要将语料库来源提交到 `packages/rustperf`。 + +## 2026-10-23 — 新提示规则,未分类 + +下面的 slug 是 **提示**。它们有配对的固定装置和待处理的证据备注。它们 **没有** JSONL,并且 **没有** 经过 G2/G3。2026-10-22 准备语料库的裁决不涵盖它们。 diff --git a/packages/rustperf/docs/zh-CN/evidence-policy.md b/packages/rustperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..92d15d3 --- /dev/null +++ b/packages/rustperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,29 @@ +# rustperf 证据政策 + +## 门槛 + +只有当以下两者都存在于 `packages/rustperf/evidence/` 下时,规则才可被视为 **`error`**: + +1. `.md` — 声明,如何测量,工具链版本,限制 +2. `.jsonl` — ≥1 条前/后行,包含 `rule`、`case`、`median_ms`、`ops`、`ratio`、`ts` + +否则规则保持 **`hint`**。 + +仅仅使用 JSONL 不够:只有当 **标记的气味** 本身显示出较大且稳定的速度提升时才升级(如 rs-format-rebind-hot 所示)。同时命中约 ~1× 均摊模式的混合检测器保持 **hint**。 + +## 当前映射 + +截至2026年10月22日。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| rs-alloc-hot | 关闭 | **是** (`rs-alloc-hot.md` + `rs-alloc-hot.jsonl`) | 循环中的分配/克隆 — Vec::new ~2.7× / fixture 克隆 ~1.1× / to_string ~1.7×;保持 **关闭**(G3 OK-惯用法;没有窄分支导致升级);使用 `--severity rs-alloc-hot=hint` 进行查看 | +| rs-println-hot | 提示 | **是** (`rs-println-hot.md` + `rs-println-hot.jsonl`) | 循环中的 println! / eprintln! (~12.6× 缓冲 vs 每行;I/O,依赖环境) | +| rs-format-rebind-hot | **错误** | **是** (`rs-format-rebind-hot.md` + `rs-format-rebind-hot.jsonl`,来自 rs-string-plus-in-loop G4 格式的别名!配对) | `format!` 在循环中重新绑定 accum(约 99 倍于 `write!`);跳过花括号包裹 | +| rs-collect-vec-hot | 提示 | **是** (`rs-collect-vec-hot.md` + `.jsonl`) | 循环中 collect:: (~1.8×) | +| rs-map-new-hot | 提示 | 测量过的 (`rs-map-new-hot.md` + `rs-map-new-hot.jsonl`); 保持提示 | 循环中的 `HashMap::new`; 两次安静运行约 1.5 倍; 未经过语料库分类 | +| rs-regex-new-hot | 提示 | 已测量(`rs-regex-new-hot.md` + `rs-regex-new-hot.jsonl`);保持提示 | 循环中的 `Regex::new`;未进行语料库分类 | +| rs-file-open-hot | 提示 | 已测量 (`rs-file-open-hot.md` + `rs-file-open-hot.jsonl`); 保持提示 | 循环中的 `File::open`; 仅相同路径; 未经过语料库筛选 | +| rs-clock-hot | 提示 | 测量 (`rs-clock-hot.md` + `rs-clock-hot.jsonl`); 保持提示 | 循环中的 `Instant::now`; 未经语料库分类 | + +rs-alloc-hot G4 2026-10-22:仅测得适度的收益;**不要**大规模升级(OK-idiom 支配 G3)。`rs-string-plus-in-loop` 在大填充保持约 1× 后被移除。Hot `s = format!("{s}…")` 保持在 rs-format-rebind-hot (**错误**)。`rs-vec-no-capacity` 在大填充保持约 1× 后被移除。 diff --git a/packages/rustperf/docs/zh-CN/rule-ids.md b/packages/rustperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..fbc6df3 --- /dev/null +++ b/packages/rustperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,36 @@ +# rustperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `rs-alloc-hot` | +| `rs-println-hot` | +| `rs-format-rebind-hot` | +| `rs-collect-vec-hot` | +| `rs-map-new-hot` | +| `rs-regex-new-hot` | +| `rs-file-open-hot` | +| `rs-clock-hot` | + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `rs-alloc-in-loop` | `rs-alloc-hot` | +| `rs-clock-in-loop` | `rs-clock-hot` | +| `rs-collect-vec-in-loop` | `rs-collect-vec-hot` | +| `rs-file-open-in-loop` | `rs-file-open-hot` | +| `rs-format-rebind-in-loop` | `rs-format-rebind-hot` | +| `rs-println-in-loop` | `rs-println-hot` | +| `rs-regex-new-in-loop` | `rs-regex-new-hot` | +| `rs-map-new-hot` | `rs-map-new-hot` | + +## Removed + +- `rs-string-plus-in-loop` — large fill about 1.00×; see `evidence/_removed/` +- `rs-vec-no-capacity` — large fill about 1.00× and about 1.05× bytes; see `evidence/_removed/` diff --git a/packages/rustperf/docs/zh-CN/rust-performance-guide.md b/packages/rustperf/docs/zh-CN/rust-performance-guide.md new file mode 100644 index 0000000..41f4277 --- /dev/null +++ b/packages/rustperf/docs/zh-CN/rust-performance-guide.md @@ -0,0 +1,55 @@ +# Rust 性能指南(v0) + +少一点分配器噪声,少一点意外的字符串 / 打印停顿。 + +> **范围:Rust edition 2021+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(`cargo bench`、`criterion`、`perf`)。LLVM 优化会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. 分配与 clone](#1-分配与-clone) +- [2. 字符串与热 I/O](#2-字符串与热-io) +- [3. Regex](#3-regex) +- [Lint 对照](#lint-对照) + +## 1. 分配与 clone + +画像显示有压力时,热循环里优先复用与容量,而不是 `Vec::new` / `.clone()` / `.to_string()`。 + +```rust +// prefer: pre-sized or reused buffer +let mut v = Vec::with_capacity(n); +for i in 0..n { v.push(i); } +``` + +Lint: `rs-alloc-hot`(默认 off,G3 后;G4 有测量) +## 2. 字符串与热 I/O + +循环里用 `format!("{s}…")` / `s = format!(…)` 累加分配很重(会拷贝不断变长的前缀)——**rs-format-rebind-hot**。优先 `with_capacity` + `write!`,或先收集再 join。对单一增长的 `String` 做普通 `+=` / `push_str`,在大填充上相对 `with_capacity` 约 1×,因此该 lint 已移除。不要把 `println!` / `eprintln!` 放进紧密循环(门控或缓冲)。 + +大 `Vec::new` 再 `push` 相对 `with_capacity` 约 1×,该 lint 已移除。逐项循环里避免 `.collect::>()`,若复用缓冲区或流式计数即可(**rs-collect-vec-hot**)。 + +Lint: `rs-format-rebind-hot`(default error; G4: ~99× vs `write!`); `rs-println-hot`, `rs-collect-vec-hot`(默认 hint) +## 3. Regex + +循环里的 `Regex::new` 会反复编译模式。在 `regex` 1.13.1 与 rustc 1.99 上,每轮构造再匹配短 haystack,相对构造一次约 963×(两侧仍匹配)。该 lint 为 hint。`HashMap::new` 加一次 insert,重复 20000 次,相对清空同一 map 约 1.5×(两次安静的 rustc 1.99 运行;`rs-map-new-hot`,hint)。 + +对同一路径 `File::open` 在 rustc 1.99 上约 416×(相对打开一次)。`Instant::now` 约 20×(相对采一次样)。变化的路径不能外提;需要新鲜时间戳的循环也没有可外提的东西。 + +Lint: `rs-regex-new-hot`, `rs-file-open-hot`, `rs-clock-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| rs-alloc-hot | 循环内 alloc/clone | off(G3 OK-idiom;G4 有测量) | +| rs-println-hot | 循环内 println! / eprintln! | hint | +| rs-format-rebind-hot | 循环内 `format!` 重绑定累加 | error(G4:相对 `write!` 约 99×) | +| rs-collect-vec-hot | 循环内 collect:: | hint | +| rs-regex-new-hot | 循环内 `Regex::new` | hint(短 haystack 上 JSONL 约 963×) | + +见 [evidence-policy.md](evidence-policy.md)。若一次性包装占主导,可用 `--severity rs-format-rebind-hot=hint` 降级。摊销的 `+=` 与大 `Vec::new` + push 已移除。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/rustperf/evidence/zh-CN/README.md b/packages/rustperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..a0d2d25 --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/README.md @@ -0,0 +1,10 @@ +# rustperf 证据 + +所有六条规则都有一个声明文件和测量过的 JSONL(挂载在 [`../examples/bench`](../../examples/bench)): + +- **rs-alloc-hot** — `rs-alloc-hot.md` + `.jsonl`; Vec::new 重用约 2.7× / fixture 克隆约 1.1× / to_string 约 1.7×;默认 **关闭**(比例适中,G3 OK-idiom 占主导)。 +- **rs-println-hot** — `rs-println-hot.md` + `.jsonl`; 缓冲写入约 12.6×(I/O、环境相关);默认 **提示**。 +- **rs-format-rebind-hot** — `rs-format-rebind-hot.md` + `.jsonl`; `format!` 重新绑定约 99×(别名来自 rs-string-plus-in-loop G4 的 `format!` 配对);默认 **错误**。 +- **rs-collect-vec-hot** — `rs-collect-vec-hot.md` + `.jsonl`; 约 1.8×;默认 **提示**。 + +请参阅 [`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md) 和根目录下的 [`docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/rustperf/evidence/zh-CN/rs-alloc-hot.md b/packages/rustperf/evidence/zh-CN/rs-alloc-hot.md new file mode 100644 index 0000000..9966897 --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-alloc-hot.md @@ -0,0 +1,62 @@ +# rs-alloc-hot — 在循环中分配/克隆(Vec::new / .clone / to_string / to_owned) + +## 主张 + +在一个高频循环中(构建缓冲区、丢弃、重复)使用新的 `Vec::new()` 比使用 `clear`/`with_capacity` 重用在这台机器上 rustc `-C opt-level=3` 下只慢约 **~2.7×**。定型的固定 `seed.clone()` 到一个 `Vec` 中,在有无 `Vec::with_capacity` 的情况下大约是 **~1.1×**(克隆成本占主导)。每步增长 `String::clone` 与 `clear`+`push_str` 重用相比,大约慢 **~2.2×**。`i.to_string()` 与重用的 `clear`+`write!` 相比,大约慢 **~1.7×**。小 N 的重复短期 `Vec::new` 可能显示约 **~9×**,但这仍远低于 lu-string-concat-hot / rs-format-rebind-hot 类型的性能提升,并且探测器也会识别 G3 OK-idiom Id/`to_owned` 构建器。**不要**将整个 rs-alloc-hot 升级为 `error`;**不要**在没有探测器排除 OK-idiom 的情况下发明子规则升级它。 + +## 如何测量 + +- 利用:`包/壳/样本/箱/证据 rs001.rs' +- 构件:`rustc-C opt-level=3 证据 rs001.rs-o out/证据 rs001' +- 计时器:`std:时间:即时 ' 墙钟;温暖=3;中位数为9;`std:hint::black box ' +- A对:外=50000×内=64推 +- ** 之前:** `vic: new()' 每一个外延 +- ** 后面:** 1个`系数:具有 容量(内)`+`清晰'每个外部 +- 配对B(配对 'rustperf-lint/fixtures/bad/rs-alloc-hot clone loop.rs ' 容量意图):N=200000,种子=64 +- ** 之前:**`威奇:新'+`种子.clone()'推 +- ** 之后:** `系数:具有 容量(N)' + 相同的克隆 +- C对(生长型克隆人):N=8000 +- ** 在此之前:** `ac ' 增加一个字符;`ac.clone()' 每一步骤 +- ** 之后:** 相同的生长;`buf.clear();buf.push str(和acc)'再利用 +- D对数:N=200000 +- ** 在**`i.to string()'之前 +- ** 之后:** 重新使用`String' +`clear' + `write!' 后:** 重复使用`String' +`clear' +`write! ' +- E对(小N界限):外部=100×内部=16×20000 计时层外代表 +- 原始线路: [rs-alloc-hot.jsonl] (rs-alloc-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| rustc | **1.99.1** | +| 选项 | **优化级别=3** (`-C opt-level=3`) | +| `vec_new_each_iter` 之前的中位数 | 4.955 毫秒 | +| (`vec_clear_reuse`) 之后的中位数 | 1.810 毫秒 | +| **ratio Vec::new each** | **~6.11×** | +| 中值之前 (`clone_vec_new`) | 5.979 毫秒 | +| (`clone_with_capacity`) 后的中位数 | 5.398 毫秒 | +| **比率夹具克隆** | **~1.08×** | +| (`grow_clone_each`) 之前的中位数 | 0.550 毫秒 | +| 调用 (`grow_clear_push_str`) 后的中位数 | 0.247 毫秒 | +| **克隆比例增长** | **~1.81×** | +| 中位数之前 (`to_string_each_iter`) | 4.342 毫秒 | +| (`to_string_clear_write`) 之后的中位数 | 2.488 毫秒 | +| **ratio to_string** | **~1.56×** | +| 调整前中位数 (`small_n_vec_new_each`) | 178.412 毫秒 | +| (`small_n_vec_clear_reuse`) 之后的中位数 | 19.802 毫秒 | +| **ratio small N** | **~11.95×** | + +说明:在新的 `Vec::new` 上,重用/`clear` 略微胜出;测试夹具 `clone` 约为 1×;这里没有任何内容能匹配 rs-format-rebind-hot 的 ~99×。绝对毫秒数和比率取决于机器/优化。rustc 或机器更改后,请重新运行测试。 + +## 与 lint 的对应 + +`rustperf-lint` **rs-alloc-hot** 会标记真实的 `for`/`while`/`loop` 循环窗口内的 `Vec::new` / `.to_string()` / `.to_owned()` /(逆向优化)类似缓冲的 `.clone()`。默认严重性为 **`off`**(使用 `--severity rs-alloc-hot=hint` 来查看):JSONL 存在,但比率适中(约 1–9×),G3 clap/serde 残留大多为 OK-惯用 Id/`to_owned` 构建器。**不要**大规模升级。未来的精细拆分(例如仅循环体内的 `Vec::new`,不包括一次性构建器)仍然需要在设为 `error` 前单独设定检测器和证据。 + +## 限制 + +- 微基准测试;不是应用程序配置文件。 +- 配对 B 仅容量后不会移除克隆分配 — 预期约为 1×。 +- 配对 C 重用仍会复制字节;主要优势是避免每步创建新的 `String` 分配。 +- 配对 E 为定时器分辨率乘以小构建器;`ops` 是每批单元(100)的外部构建次数,而不是总的 push 次数。 +- 历史注:早期 G3 使用了大量 OK-惯用法;套件后来通过 rs-vec-no-capacity/rs-collect-vec-hot 翻转为 **语料库准备好**(参见 docs/corpus.md)。rs-println-hot 自那时起已被测量(提示)。 diff --git a/packages/rustperf/evidence/zh-CN/rs-clock-hot.md b/packages/rustperf/evidence/zh-CN/rs-clock-hot.md new file mode 100644 index 0000000..6b0168f --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-clock-hot.md @@ -0,0 +1,17 @@ +# rs-clock-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[rs-clock-hot.jsonl](../rs-clock-hot.jsonl)。未进行语料库分类。2026-10-22 的语料库裁决不涵盖此标识。 + +## 测量 +每次迭代使用 `Instant::now` 与只进行一次采样。rustc **1.99.1**,`-C opt-level=3`,Linux x64,负载 0.23。之前的 438× 配对之后是 0.008 毫秒。这次重新运行保持循环使用 `black_box`。一次采样是 0.205 毫秒;其余的在 0.137–0.145 毫秒之间。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `sample_once` | 0.137 | 100000 | 1 | +| 之前 | `sample_each_iter` | 2.721 | 100000 | 19.899 | + +在采样一次之后,并且不会在循环内部读取时钟。需要每次迭代获取新时间戳的代码没有可以提升的内容。默认保持 **hint**。这个比率不是普遍的加速方式。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在任何默认更改之前,仍然需要进行语料库分类处理。 diff --git a/packages/rustperf/evidence/zh-CN/rs-collect-vec-hot.md b/packages/rustperf/evidence/zh-CN/rs-collect-vec-hot.md new file mode 100644 index 0000000..a857544 --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-collect-vec-hot.md @@ -0,0 +1,37 @@ +# rs-collect-vec-hot — 在循环中使用 collect::> 与 .count() + +## 主张 + +在这台机器上测量:主要热案例 **`collect_in_loop`** 比例前/后 **~2.14×**(之后更快)。默认严重性保持 **`hint`**。仅凭此填充 **不要** 升级为 `error`。 + +## 如何测量 + +- 测试框架:`packages/rustperf/examples/bench/evidence_rs003_rs005_rs006.rs` +- 协议:warm=3;取9轮的中位数 +- 优化:`opt-level=3` +- 原始行:[`rs-collect-vec-hot.jsonl`](../rs-collect-vec-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL 时间戳是 UTC) | +| Toolchain | **1.85.1** | +| 选项 | **优化级别=3** | +| (`count_no_collect`) 之后的中位数 | 0.333875 毫秒 (操作数=5000,比例=1.000000) | +| 循环中收集前的中位数 (`collect_in_loop`) | 0.714542 毫秒 (操作次数=5000,比例=2.140148) | +| (`small_count`) 后的中位数 | 7.044250 毫秒 (操作=50,比例=1.000000) | +| `small_collect` 之前的中位数 | 13.768250 毫秒 (操作次数=50,比率=1.954537) | +| **主要比率 前/后** | **~2.14×** (`collect_in_loop`) | + +方向和绝对毫秒数对机器/光学敏感。在工具链或机器更改后,请重新运行测试夹具。 + +## 与 lint 的对应 + +`rustperf-lint` **rs-collect-vec-hot** 默认保持 **`hint`**。建议升级:**否**。 + +## 限制 + +- 微基准;不是应用程序性能概况。 +- 小行数的测试结果可能接近计时器噪声;在进行比率声明时,最好使用热点(非小规模)案例。 +- I/O 规则(print/echo/puts/writeln)丢弃到 `/dev/null`——依赖环境。 diff --git a/packages/rustperf/evidence/zh-CN/rs-file-open-hot.md b/packages/rustperf/evidence/zh-CN/rs-file-open-hot.md new file mode 100644 index 0000000..052b41b --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-file-open-hot.md @@ -0,0 +1,17 @@ +# rs-file-open-hot — 已测量,仍有提示 + +## 状态 +**提示**。挂钟 JSONL: [rs-file-open-hot.jsonl](../rs-file-open-hot.jsonl)。未进行语料库分级。2026-10-22 的语料库裁决不涵盖此条目。 + +## 测量 +`File::open` 加上对一个一字节文件的短读取,共 2000 次,与只打开一次进行比较。rustc **1.99.1**,`-C opt-level=3`,Linux x64,负载 0.59。早期的 2750× 对比是 rustc 1.94(16.615 毫秒 vs 0.006 毫秒)。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `open_once` | 0.00478 | 2000 | 1 | +| 之前 | `open_each_iter` | 1.987266 | 2000 | 415.746 | + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。变化的输入、冷路径或另一个 ABI 都可能抹去差异。在进行任何默认更改之前,仍然需要语料库分流。 diff --git a/packages/rustperf/evidence/zh-CN/rs-format-rebind-hot.md b/packages/rustperf/evidence/zh-CN/rs-format-rebind-hot.md new file mode 100644 index 0000000..a7afd25 --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-format-rebind-hot.md @@ -0,0 +1,41 @@ +# rs-format-rebind-hot — 在循环中重新绑定格式化累积 + +## 主张 + +在 Rust 中,使用热循环 `s = format!("{s}…")` / `s = format!("{}{}", s, …)` 累积字符串的速度明显比 `String::with_capacity` + `write!` / `push_str` 慢,因为每次 `format!` 都会分配一个新的 `String` 并复制不断增长的前缀(大约呈二次方增长)。 + +## 如何测量 + +与以前的 rs-string-plus-in-loop 格式相同的挂载和运行!配对(不是重新发明的): + +- 测试工具:`packages/rustperf/examples/bench/evidence_rs002.rs`(Pair C) +- 构建:`rustc -C opt-level=3 evidence_rs002.rs -o out/evidence_rs002` +- 计时器:`std::time::Instant` 墙钟时间;预热=3;取9次的中位数;`std::hint::black_box` +- Pair C(匹配测试用例 `rustperf-lint/fixtures/bad/rs-format-rebind-hot_format_rebind.rs` / 良好 `rs-format-rebind-hot_write.rs`):N=20000 + - **之前:** 循环中 `s = format!("{s}{i}")` + - **之后:** `with_capacity` + `write!(s, "{i}")` +- 原始数据行:[`rs-format-rebind-hot.jsonl`](../rs-format-rebind-hot.jsonl) — **来源别名** 自 2026-10-22 rs-string-plus-in-loop G4 JSONL(`rule` 字段设为 `rs-format-rebind-hot`; `aliased_from=rs-string-plus-in-loop`; `median_ms` / `ops` / `ts` 相同) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| rustc | **1.99.1** | +| 选项 | **优化级别=3** (`-C opt-level=3`) | +| 前中位数 (`digits_format_accum`) | 28.652 毫秒 | +| (`digits_write`) 之后的中位数 | 0.288 毫秒 | +| **比率格式!前/后** | **~100×** | + +方向:`format!` 重新绑定累积对 **flagged** 异味是一个大而稳定的胜利。绝对毫秒数和比率取决于机器/优化。 + +## 与 lint 的对应 + +`rustperf-lint` **rs-format-rebind-hot** 仅在循环行包含 `ident = format!(…)`(**不是** `let ident = format!(…)`)且格式体/参数重用 `ident`(`{ident}` 或位置标记)时触发。裸露的 `format!(…)` 表达式和新的 let 绑定不会触发警告。根据 [evidence-policy](../../docs/zh-CN/evidence-policy.md),默认严重性为 **`error`**:被标记的代码味道本身约为 99×。路径跳过镜像 `rs-string-plus-in-loop`(适用于 shell / 冷 CLI 格式化器)。摊销的 `+=` / `push_str` 保持在 **rs-string-plus-in-loop**(提示)。 + +## 限制 + +- 微型基准;不是应用程序配置文件。 +- 行启发式仍可能触发一次性换行,这种换行在每次外部迭代中会重复使用左侧表达式(参见 G3 剩余注释)——如有需要,可使用 `--severity rs-format-rebind-hot=off` / `hint` 覆盖。 +- 不测量 `Write` 特征对象、`itoa` 或 SSO。 +- 历史说明:编写于 r4 重设计之前;现在该工具包已**适用于语料库**(参见 docs/corpus.md)。 diff --git a/packages/rustperf/evidence/zh-CN/rs-map-new-hot.md b/packages/rustperf/evidence/zh-CN/rs-map-new-hot.md new file mode 100644 index 0000000..2aed417 --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-map-new-hot.md @@ -0,0 +1,20 @@ +# rs-map-new-hot — 已测量,提示 + +## 状态 + +**提示**。挂钟 JSONL:[rs-map-new-hot.jsonl](../rs-map-new-hot.jsonl)。未经过语料库筛选。该规则在较旧的 2.07× 配对之后被移除,然后又被放回:两次安静的 rustc 1.99.1 在 Linux x64 上的运行(负载 0.03 和 0.11)分别为 **1.533×** 和 **1.510×**。 + +## 测量 + +对一个键进行20000次插入。在每次迭代前使用`HashMap::new`;之后清除用`with_capacity(1)`创建的一个映射。双方都读取插入的值。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `map_reused` | 0.392946 | 20000 | 1 | +| 之前 | `map_new_each_iter` | 0.602298 | 20000 | 1.533 | + +第二次运行为 0.374781 毫秒,而第一次为 0.565840 毫秒(1.510×)。两次运行的样本都集中在一个窄区间。默认保持 **hint**。这个比率并不是一般性的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。更改的密钥集或冷路径可能会抹掉差异。 diff --git a/packages/rustperf/evidence/zh-CN/rs-println-hot.md b/packages/rustperf/evidence/zh-CN/rs-println-hot.md new file mode 100644 index 0000000..365c3df --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-println-hot.md @@ -0,0 +1,39 @@ +# rs-println-hot — 每次迭代使用 writeln/println 与缓冲区比较然后写入 + +## 主张 + +在每次循环迭代中向 `/dev/null` 写一行,比将内容缓冲到 `String` 中的 `writeln!` 然后一次性 `write_all` 慢 **约 64.9 倍**(rustc 1.99.1 `-C opt-level=3`,本机测试)。与一个安静的计算循环相比,这个差距非常大(受 I/O 限制)——该示例仅用于参考,**不得**因此提高设置。默认仍为 **`hint`**(取决于环境/I/O)。 + +## 如何测量 + +- 测试框架: `packages/rustperf/examples/bench/evidence_rs003_rs005_rs006.rs` +- 构建: `rustc -C opt-level=3` +- 计时器: `std::time::Instant`; 预热=3; 取9次的中位数; `black_box` +- 组合 A: N=20000 + - **之前:** 每次迭代 `writeln!(null, "{i}")` + - **之后:** 缓冲行然后一次性写入 `/dev/null` +- 组合 B (上下文): 同样的之前 vs 安静算术 (无 I/O) +- 原始行: [`rs-println-hot.jsonl`](../rs-println-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-22 亚洲/上海(机器时钟;JSONL `ts` 为 UTC) | +| rustc | **1.99.1** | +| 选项 | **优化级别=3** | +| `writeln_each_iter` 之前的中位数 | 6.582 毫秒 | +| (`buffer_then_write`) 后的中位数 | 0.524 毫秒 | +| **比率缓冲对** | **~64.85×** | +| (`quiet_compute`) 后的中位数 | ~0.00005 毫秒 | +| writeln 与 quiet 的比率 | 巨大(有 I/O 与无 I/O — 不升级基础) | + +## 与 lint 的对应 + +`rustperf-lint` **rs-println-hot** 默认保持 **`hint`**。建议升级:**否**(依赖 I/O 环境,与 lu-print-hot 和 cp-printf-hot 属于同一类)。 + +## 限制 + +- 微基准测试;标准输出被丢弃到 `/dev/null`。 +- 实际 TTY/标准错误竞争可能会显著改变绝对毫秒数。 +- 不会单独测量 `println!` 宏格式化与写入系统调用。 diff --git a/packages/rustperf/evidence/zh-CN/rs-regex-new-hot.md b/packages/rustperf/evidence/zh-CN/rs-regex-new-hot.md new file mode 100644 index 0000000..82c78ea --- /dev/null +++ b/packages/rustperf/evidence/zh-CN/rs-regex-new-hot.md @@ -0,0 +1,15 @@ +# rs-regex-new-hot — 已测量,仍在提示 + +## 状态 +**提示**。挂钟 JSONL:[rs-regex-new-hot.jsonl](../rs-regex-new-hot.jsonl)。未经过语料库分类。2026-10-22 的语料库裁决不涵盖此 slug。Crate `regex` 1.13.1,rustc 1.99.1,`-C opt-level=3`,Linux x64,负载 0.44。双方仍然在 `id-2026` 上调用 `is_match`。之前的 2968× 配对是 rustc 1.94 和 `regex` 1.11.1。 + +## 测量 +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在 | `Regex::new` 一次之后 | 0.133901 | 2000 | 1 | +| 之前 | `Regex::new` 每次迭代 | 128.901984 | 2000 | 962.666 | + +模式是 `([A-Za-z]+)-([0-9]{2,4})`。默认保持提示。 + +## 解除阻塞 +不要仅仅从此文件将默认值设置为 `error`。其他输入或冷路径可能会抹去差异。任何默认更改前仍需进行语料筛选。 diff --git a/packages/rustperf/zh-CN/CONTRIBUTING.md b/packages/rustperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/rustperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/rustperf/zh-CN/README.md b/packages/rustperf/zh-CN/README.md new file mode 100644 index 0000000..fbcd23d --- /dev/null +++ b/packages/rustperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# rustperf + +> **范围说明:** **rustperf** 专做 **Rust**(edition 2021+)。仅启发式 lint;升级前用 `criterion` / `cargo bench` 测量。 + +Rust 性能笔记、小示例,以及 `rustperf-lint`。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。lint 为启发式(**corpus-ready: 是** — r4 在 clap 上 6×TP;**rs-alloc-hot=off**;**rs-format-rebind-hot=error** ~99×;**rs-println-hot/rs-collect-vec-hot=hint**;六条规则均有实测 JSONL)。指南 v0。 + +## 目录 + +- [英文指南(v0)](../docs/zh-CN/rust-performance-guide.md) — 来自 `docs/guide-src/` +- [`rustperf-lint`](../rustperf-lint/README.md) — rs-alloc-hot–rs-collect-vec-hot +- [语料固定目标](../docs/zh-CN/corpus.md) + +## 关注点 + +避免热循环里的 alloc/clone,有界 push 循环前优先 `with_capacity`,避免每轮 `collect::>`,优先 `write!` 而非 `format!` 重绑定,紧循环里不要放 `println!`。 + +## 运行 lint + +```sh +cd packages/rustperf/rustperf-lint && cargo build --release +./target/release/rustperf-lint path/to/sources +bash ../scripts/smoke-lint.sh +bash ../scripts/self-lint.sh +``` + +仅 **error** 级发现时退出 `1`(hint 退出 `0`,除非 `--strict-hints`)。 + +## 布局 + +```text +docs/guide-src/ 章节源 +docs/ 组装后的指南 + corpus.md + evidence-policy.md +evidence/ 每规则主张 + 实测 JSONL;rs-format-rebind-hot error(~99×),其余 off/hint +examples/ 小演示 +rustperf-lint/ 检查器 + 夹具 +scripts/ smoke-lint.sh, self-lint.sh +``` diff --git a/packages/swiftperf/docs/guide-src/00_head.md b/packages/swiftperf/docs/guide-src/00_head.md index 710b7d5..c963027 100644 --- a/packages/swiftperf/docs/guide-src/00_head.md +++ b/packages/swiftperf/docs/guide-src/00_head.md @@ -2,17 +2,12 @@ Fewer force-unwraps on hot paths, fewer quadratic string builders. -> **Scope: Swift 5.9+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Swift 5.9+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (Instruments, `swift package benchmark` when available). -2. Treat each chapter as a checklist. -3. ARC and specialization change costs — re-bench after toolchain bumps. +Profile first (Instruments, `swift package benchmark` when available). ARC and specialization change costs — re-bench after toolchain bumps. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Optionals and hot paths](#1-optionals-and-hot-paths) - [2. Strings, collections, and printing](#2-strings-collections-and-printing) - [3. Formatters, JSON, and regular expressions](#3-formatters-json-and-regular-expressions) diff --git a/packages/swiftperf/docs/guide-src/01_optionals.md b/packages/swiftperf/docs/guide-src/01_optionals.md index e4aec24..0cadba8 100644 --- a/packages/swiftperf/docs/guide-src/01_optionals.md +++ b/packages/swiftperf/docs/guide-src/01_optionals.md @@ -3,3 +3,4 @@ Force-unwrap (`!`) in a tight loop crashes on nil and skips safer control flow. Prefer `guard let` / `if let` once per iteration or hoist. Force-unwrap and `if let` took the same time on a non-nil array, so there is no lint for it. + diff --git a/packages/swiftperf/docs/guide-src/02_strings.md b/packages/swiftperf/docs/guide-src/02_strings.md index 640dc36..1d4ac45 100644 --- a/packages/swiftperf/docs/guide-src/02_strings.md +++ b/packages/swiftperf/docs/guide-src/02_strings.md @@ -2,5 +2,5 @@ Repeated `s += piece` can be costly; prefer reserved capacity / builders when measured. Hot `print` is I/O-bound — gate it. Appending to an empty `Array` in a sized loop reallocates as it grows; call `reserveCapacity` first when the final count is known. -Companion lint: **sp-print-hot** (off; about 10× a `String` when stdout is discarded), **sp-array-no-reserve** (hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. +Lint: `sp-print-hot` (default off; about 10× a `String` when stdout is discarded); `sp-array-no-reserve` (default hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. diff --git a/packages/swiftperf/docs/guide-src/03_hot_calls.md b/packages/swiftperf/docs/guide-src/03_hot_calls.md index 4502f18..99977ab 100644 --- a/packages/swiftperf/docs/guide-src/03_hot_calls.md +++ b/packages/swiftperf/docs/guide-src/03_hot_calls.md @@ -1,6 +1,6 @@ ## 3. Formatters, JSON, and regular expressions -`DateFormatter`, `JSONDecoder`, and `NSRegularExpression` do heavy setup. Building one on every iteration of a hot loop repeats that work; create it once and reuse it. `JSONSerialization.jsonObject` belongs with the JSON case. A Swift `Regex` value is a different API and is not flagged. These lints are **hint**. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. +`DateFormatter`, `JSONDecoder`, and `NSRegularExpression` do heavy setup. Building one on every iteration of a hot loop repeats that work; create it once and reuse it. `JSONSerialization.jsonObject` belongs with the JSON case. A Swift `Regex` value is a different API and is not flagged. These lints are hint. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. `String(contentsOfFile:)` of one path was about 414× versus reading once. A path that changes every iteration cannot be hoisted. @@ -8,4 +8,5 @@ `value(forKey:)` every iteration was about 35× versus reading the property. A name that changes every iteration cannot become a direct property. This is the same family as Go `FieldByName`. -Companion lint: **sp-dateformatter-hot**, **sp-json-decode-hot**, **sp-regex-hot**, **sp-file-read-hot**, **sp-clock-hot**, **sp-value-for-key-hot**. +Lint: `sp-dateformatter-hot`, `sp-json-decode-hot`, `sp-regex-hot`, `sp-file-read-hot`, `sp-clock-hot`, `sp-value-for-key-hot` (default hint). + diff --git a/packages/swiftperf/docs/guide-src/99_tail.md b/packages/swiftperf/docs/guide-src/99_tail.md index 6c5eb5c..7a78b99 100644 --- a/packages/swiftperf/docs/guide-src/99_tail.md +++ b/packages/swiftperf/docs/guide-src/99_tail.md @@ -8,7 +8,7 @@ | sp-json-decode-hot | JSON decoder or jsonObject in a loop | hint (JSONL present; not corpus-triaged) | | sp-regex-hot | regular expression constructed in a loop | hint (JSONL present; not corpus-triaged) | -Enable an `off` rule with `--severity =hint`. The five rules from 2026-10-22 still have no JSONL. The three rows added 2026-10-23 have a JSONL microbench and stay **hint**. They are not part of the 2026-10-22 corpus-ready verdict. See [evidence-policy.md](evidence-policy.md). +Enable an `off` rule with `--severity =hint`. The five rules from 2026-10-22 still have no JSONL. The three rows added 2026-10-23 have a JSONL microbench and stay hint. They are not part of the 2026-10-22 corpus-ready verdict. See [evidence-policy.md](evidence-policy.md). ## References diff --git a/packages/swiftperf/docs/guide-src/zh-CN/00_head.md b/packages/swiftperf/docs/guide-src/zh-CN/00_head.md new file mode 100644 index 0000000..91c7e74 --- /dev/null +++ b/packages/swiftperf/docs/guide-src/zh-CN/00_head.md @@ -0,0 +1,15 @@ +# Swift 性能指南(v0) + +热路径少强制解包,少做平方级字符串 builder。 + +> **范围:Swift 5.9+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(Instruments;可用时用 `swift package benchmark`)。ARC 与特化会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. Optional 与热路径](#1-optional-与热路径) +- [2. 字符串、集合与打印](#2-字符串集合与打印) +- [3. Formatter、JSON 与正则](#3-formatterjson-与正则) +- [Lint 对照](#lint-对照) + diff --git a/packages/swiftperf/docs/guide-src/zh-CN/01_optionals.md b/packages/swiftperf/docs/guide-src/zh-CN/01_optionals.md new file mode 100644 index 0000000..55b4b04 --- /dev/null +++ b/packages/swiftperf/docs/guide-src/zh-CN/01_optionals.md @@ -0,0 +1,5 @@ +## 1. Optional 与热路径 + +紧密循环里的强制解包(`!`)在 nil 时会崩,并跳过更安全的控制流。优先每轮一次 `guard let` / `if let`,或外提。 + +在非 nil 数组上,强制解包与 `if let` 耗时相同,因此没有对应 lint。 diff --git a/packages/swiftperf/docs/guide-src/zh-CN/02_strings.md b/packages/swiftperf/docs/guide-src/zh-CN/02_strings.md new file mode 100644 index 0000000..dd4dd37 --- /dev/null +++ b/packages/swiftperf/docs/guide-src/zh-CN/02_strings.md @@ -0,0 +1,5 @@ +## 2. 字符串、集合与打印 + +反复 `s += piece` 可能昂贵;测得需要时优先预留容量 / builder。热路径 `print` 受 I/O 限制——请门控。在有大小的循环里向空 `Array` 追加会边长边扩容;已知最终计数时先 `reserveCapacity`。 + +Lint: `sp-print-hot`(默认 off; about 10× a `String` when stdout is discarded); `sp-array-no-reserve`(默认 hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. diff --git a/packages/swiftperf/docs/guide-src/zh-CN/03_hot_calls.md b/packages/swiftperf/docs/guide-src/zh-CN/03_hot_calls.md new file mode 100644 index 0000000..4219a90 --- /dev/null +++ b/packages/swiftperf/docs/guide-src/zh-CN/03_hot_calls.md @@ -0,0 +1,11 @@ +## 3. Formatter、JSON 与正则 + +`DateFormatter`、`JSONDecoder`、`NSRegularExpression` 准备很重。热循环每轮新建会重复该工作;只建一次并复用。`JSONSerialization.jsonObject` 归入 JSON 情形。Swift `Regex` 值是另一套 API,不标记。这些 lint 是 hint。各自有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。 + +对同一路径 `String(contentsOfFile:)` 约 414×(相对读一次)。每轮都变的路径不能外提。 + +每轮 `Date()` 相对采一次样约 244×。需要新鲜时间戳的循环没有可外提的东西。`DateFormatter` 仍是独立规则。 + +每轮 `value(forKey:)` 相对读属性约 35×。每轮都变的名字不能变成直接属性。这与 Go 的 `FieldByName` 同族。 + +Lint: `sp-dateformatter-hot`, `sp-json-decode-hot`, `sp-regex-hot`, `sp-file-read-hot`, `sp-clock-hot`, `sp-value-for-key-hot`(默认 hint) diff --git a/packages/swiftperf/docs/guide-src/zh-CN/99_tail.md b/packages/swiftperf/docs/guide-src/zh-CN/99_tail.md new file mode 100644 index 0000000..f7decf5 --- /dev/null +++ b/packages/swiftperf/docs/guide-src/zh-CN/99_tail.md @@ -0,0 +1,15 @@ +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| sp-print-hot | 循环内 `print(` | off(相对 String 约 10×;丢弃 stdout) | +| sp-array-no-reserve | 空 Array 追加且无 `reserveCapacity` | hint(小约 5×,一次大填充约 1.6×) | +| sp-dateformatter-hot | 循环内构造 date formatter | hint(有 JSONL;未 corpus 分级) | +| sp-json-decode-hot | 循环内 JSON decoder 或 jsonObject | hint(有 JSONL;未 corpus 分级) | +| sp-regex-hot | 循环内构造正则 | hint(有 JSONL;未 corpus 分级) | + +用 `--severity =hint` 启用 `off` 规则。2026-10-22 的五条规则仍无 JSONL。2026-10-23 新增三行有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/swiftperf/docs/swift-performance-guide.md b/packages/swiftperf/docs/swift-performance-guide.md index cf2837c..e7eb1ea 100644 --- a/packages/swiftperf/docs/swift-performance-guide.md +++ b/packages/swiftperf/docs/swift-performance-guide.md @@ -2,17 +2,12 @@ Fewer force-unwraps on hot paths, fewer quadratic string builders. -> **Scope: Swift 5.9+.** Claims that assert speedups name a local example or are marked provisional. **Corpus lint counts are heuristic dogfood**, not benchmarks. +> **Scope: Swift 5.9+.** Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. -## 0. Scope and method - -1. **Measure first** (Instruments, `swift package benchmark` when available). -2. Treat each chapter as a checklist. -3. ARC and specialization change costs — re-bench after toolchain bumps. +Profile first (Instruments, `swift package benchmark` when available). ARC and specialization change costs — re-bench after toolchain bumps. ## Contents -- [0. Scope and method](#0-scope-and-method) - [1. Optionals and hot paths](#1-optionals-and-hot-paths) - [2. Strings, collections, and printing](#2-strings-collections-and-printing) - [3. Formatters, JSON, and regular expressions](#3-formatters-json-and-regular-expressions) @@ -23,15 +18,16 @@ Fewer force-unwraps on hot paths, fewer quadratic string builders. Force-unwrap (`!`) in a tight loop crashes on nil and skips safer control flow. Prefer `guard let` / `if let` once per iteration or hoist. Force-unwrap and `if let` took the same time on a non-nil array, so there is no lint for it. + ## 2. Strings, collections, and printing Repeated `s += piece` can be costly; prefer reserved capacity / builders when measured. Hot `print` is I/O-bound — gate it. Appending to an empty `Array` in a sized loop reallocates as it grows; call `reserveCapacity` first when the final count is known. -Companion lint: **sp-print-hot** (off; about 10× a `String` when stdout is discarded), **sp-array-no-reserve** (hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. +Lint: `sp-print-hot` (default off; about 10× a `String` when stdout is discarded); `sp-array-no-reserve` (default hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. ## 3. Formatters, JSON, and regular expressions -`DateFormatter`, `JSONDecoder`, and `NSRegularExpression` do heavy setup. Building one on every iteration of a hot loop repeats that work; create it once and reuse it. `JSONSerialization.jsonObject` belongs with the JSON case. A Swift `Regex` value is a different API and is not flagged. These lints are **hint**. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. +`DateFormatter`, `JSONDecoder`, and `NSRegularExpression` do heavy setup. Building one on every iteration of a hot loop repeats that work; create it once and reuse it. `JSONSerialization.jsonObject` belongs with the JSON case. A Swift `Regex` value is a different API and is not flagged. These lints are hint. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict. `String(contentsOfFile:)` of one path was about 414× versus reading once. A path that changes every iteration cannot be hoisted. @@ -39,7 +35,8 @@ Companion lint: **sp-print-hot** (off; about 10× a `String` when stdout is disc `value(forKey:)` every iteration was about 35× versus reading the property. A name that changes every iteration cannot become a direct property. This is the same family as Go `FieldByName`. -Companion lint: **sp-dateformatter-hot**, **sp-json-decode-hot**, **sp-regex-hot**, **sp-file-read-hot**, **sp-clock-hot**, **sp-value-for-key-hot**. +Lint: `sp-dateformatter-hot`, `sp-json-decode-hot`, `sp-regex-hot`, `sp-file-read-hot`, `sp-clock-hot`, `sp-value-for-key-hot` (default hint). + ## Lint map | ID | Smell | Default | @@ -50,7 +47,7 @@ Companion lint: **sp-dateformatter-hot**, **sp-json-decode-hot**, **sp-regex-hot | sp-json-decode-hot | JSON decoder or jsonObject in a loop | hint (JSONL present; not corpus-triaged) | | sp-regex-hot | regular expression constructed in a loop | hint (JSONL present; not corpus-triaged) | -Enable an `off` rule with `--severity =hint`. The five rules from 2026-10-22 still have no JSONL. The three rows added 2026-10-23 have a JSONL microbench and stay **hint**. They are not part of the 2026-10-22 corpus-ready verdict. See [evidence-policy.md](evidence-policy.md). +Enable an `off` rule with `--severity =hint`. The five rules from 2026-10-22 still have no JSONL. The three rows added 2026-10-23 have a JSONL microbench and stay hint. They are not part of the 2026-10-22 corpus-ready verdict. See [evidence-policy.md](evidence-policy.md). ## References diff --git a/packages/swiftperf/docs/zh-CN/corpus.md b/packages/swiftperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..f77b8fc --- /dev/null +++ b/packages/swiftperf/docs/zh-CN/corpus.md @@ -0,0 +1,25 @@ +# swiftperf 语料库目标 + +大型树木**不是**经过供应的,并且语料库不会随仓库一起分发:将其克隆到仓库之外的任何本地目录,并将该路径传递给代码检查器。 + +## 固定目标 + +浅层克隆,2026-10-22 亚洲/上海。 + +| 树 | 提交 | Lint r3 → r4 重新设计 | 备注 | +|------|--------|----------------------|-------| +| **swift-nio** | `108daca` `https://github.com/apple/swift-nio.git` | 16 → **20** | sp-array-no-reserve/sp-map-joined-chain | +| **Alamofire** | `bda9ed5` `https://github.com/Alamofire/Alamofire.git` | 1 → **6** | ParameterEncoding sp-map-joined-chain TP | +| **swift-argument-parser** | `efd239f` `https://github.com/apple/swift-argument-parser.git` | 21 → **6** | 使用/工具/自动完成已跳过 | + +**注意(2026-10-22):** 没有 `swift` 工具链。G0–G3 使用 [`scripts/text-lint.cjs`](../../scripts/text-lint.cjs)(与 SPM 源代码启发式一致)。安装 Swift 5.9+ 以构建 `swiftperf-lint` 并重新验证。 + +**语料库就绪: 是** (r4 重新设计;文本校验一致性)。默认值: sp-array-no-reserve/sp-map-joined-chain **提示**;sp-force-unwrap-in-loop–sp-print-hot **关闭**。无 G4。 + +不要将语料库来源提交到 `packages/swiftperf`。 + +## 2026-10-23 — 新提示规则,未分类 + +下面的 slug 是 **提示**。每个都有一个 JSONL 微基准测试,并且尚未经过 G2/G3。2026-10-22 可用于语料库的裁定不包括它们。 + +- `sp-date for matter-hot ' 、`sp-json-decode-hot ' 、`sp-regex-hot ' diff --git a/packages/swiftperf/docs/zh-CN/evidence-policy.md b/packages/swiftperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..ee9c2b9 --- /dev/null +++ b/packages/swiftperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,22 @@ +# swiftperf 证据政策 + +## 门槛 + +一条规则只有在有 `evidence/.md` + `.jsonl` **以及**明确的政策决定的情况下才可以被视为 **`error`**。仅测量的 JSONL 并不会自动升级。批次 B(2026-10-22)保持了软件包默认设置。 + +## 当前映射 + +截至2026-10-22,第B批次G4。 + +| 规则 | 严重性(默认) | 证据 | 异味 | +|------|--------------------|----------|-------| +| sp-print-hot | 关闭 | [sp-print-hot.md](../../evidence/zh-CN/sp-print-hot.md) | 大约是 String 的 10 倍,标准输出到 /dev/null;保持关闭 | +| sp-array-no-reserve | 提示 | [sp-array-no-reserve.md](../../evidence/zh-CN/sp-array-no-reserve.md) | 对许多小数组约快 5 倍;对一个大填充约快 1.6 倍;保持提示 | +| sp-dateformatter-hot | 提示 | JSONL 存在(`sp-dateformatter-hot.md` + `.jsonl`);保持提示 | 在循环中构建的日期格式化器;未经过语料库分类 | +| sp-json-decode-hot | 提示 | JSONL 存在(`sp-json-decode-hot.md` + `.jsonl`);保持提示 | JSON 解码器或 jsonObject 在循环中;未经过语料库整理 | +| sp-regex-hot | 提示 | JSONL 存在(`sp-regex-hot.md` + `.jsonl`);保持提示 | 在循环中构建的正则表达式;未经过语料库筛选 | +| sp-file-read-hot | 提示 | 已测量 (`sp-file-read-hot.md` + `sp-file-read-hot.jsonl`); 保持提示 | 循环中的 `contentsOfFile`; 仅相同路径; 非语料库分类 | +| sp-clock-hot | 提示 | 已测量(`sp-clock-hot.md` + `sp-clock-hot.jsonl`);停留提示 | 循环中的 `Date()`;未进行语料库分类 | +| sp-value-for-key-hot | 提示 | 测量过的(`sp-value-for-key-hot.md` + `sp-value-for-key-hot.jsonl`);保持提示 | 循环中的 `value(forKey:)`;未经过语料筛选 | + +2026-10-23 添加的三行保持 **hint**。每行现在都有一个 JSONL 微基准。它们在 2026-10-22 的语料库准备好的判决之外,并且比例是一个形状,而不是一般的加速。 diff --git a/packages/swiftperf/docs/zh-CN/rule-ids.md b/packages/swiftperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..1004e7e --- /dev/null +++ b/packages/swiftperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,36 @@ +# swiftperf 规则 ID(简写) + +规范 ID 仅为 **slug**。旧地图中的数字是历史数据,**不**被接受。 + +| Slug | +|------| +| `sp-print-hot` | +| `sp-array-no-reserve` | +| `sp-dateformatter-hot` | +| `sp-json-decode-hot` | +| `sp-regex-hot` | +| `sp-文件-读取-热` | +| `sp-clock-hot` | +| `sp-针对键的值-hot` | + +另请参见 [root rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 重命名 2026-10-27(旧墓碑;永不重复使用) + +默认 **`--scope all`**(或相应的工具包选项)。旧的 `*-in-loop` ID 会误导工具/模型;它们已在 `evidence/_removed/` 下作废。 + +| 旧别名 | 新别名 | +|----------|----------| +| `sp-clock-in-loop` | `sp-clock-hot` | +| `sp-dateformatter-in-loop` | `sp-dateformatter-hot` | +| `sp-file-read-in-loop` | `sp-file-read-hot` | +| `sp-json-decode-in-loop` | `sp-json-decode-hot` | +| `sp-print-in-loop` | `sp-print-hot` | +| `sp-regex-in-loop` | `sp-regex-hot` | +| `循环中键的sp值` | `键快捷的sp值` | + +## 已移除 + +- `sp-string-plus-in-loop` — `reserveCapacity` 更慢;参见 `evidence/_removed/` +- `sp-map-joined-chain` — 大约 1.2 倍;参见 `evidence/_removed/` +- `sp-force-unwrap-in-loop` — 与 `if let` 同样时间;参见 `evidence/_removed/` diff --git a/packages/swiftperf/docs/zh-CN/swift-performance-guide.md b/packages/swiftperf/docs/zh-CN/swift-performance-guide.md new file mode 100644 index 0000000..15fc5da --- /dev/null +++ b/packages/swiftperf/docs/zh-CN/swift-performance-guide.md @@ -0,0 +1,51 @@ +# Swift 性能指南(v0) + +热路径少强制解包,少做平方级字符串 builder。 + +> **范围:Swift 5.9+。** 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。 + +先画像(Instruments;可用时用 `swift package benchmark`)。ARC 与特化会改变成本——工具链升级后请重测。 + +## 目录 + +- [1. Optional 与热路径](#1-optional-与热路径) +- [2. 字符串、集合与打印](#2-字符串集合与打印) +- [3. Formatter、JSON 与正则](#3-formatterjson-与正则) +- [Lint 对照](#lint-对照) + +## 1. Optional 与热路径 + +紧密循环里的强制解包(`!`)在 nil 时会崩,并跳过更安全的控制流。优先每轮一次 `guard let` / `if let`,或外提。 + +在非 nil 数组上,强制解包与 `if let` 耗时相同,因此没有对应 lint。 +## 2. 字符串、集合与打印 + +反复 `s += piece` 可能昂贵;测得需要时优先预留容量 / builder。热路径 `print` 受 I/O 限制——请门控。在有大小的循环里向空 `Array` 追加会边长边扩容;已知最终计数时先 `reserveCapacity`。 + +Lint: `sp-print-hot`(默认 off; about 10× a `String` when stdout is discarded); `sp-array-no-reserve`(默认 hint; about 5× on many small arrays, about 1.6× on one large fill). String `+=` with `reserveCapacity` was slower, and `map` then `joined` was about 1.2×, so those lints were removed. +## 3. Formatter、JSON 与正则 + +`DateFormatter`、`JSONDecoder`、`NSRegularExpression` 准备很重。热循环每轮新建会重复该工作;只建一次并复用。`JSONSerialization.jsonObject` 归入 JSON 情形。Swift `Regex` 值是另一套 API,不标记。这些 lint 是 hint。各自有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。 + +对同一路径 `String(contentsOfFile:)` 约 414×(相对读一次)。每轮都变的路径不能外提。 + +每轮 `Date()` 相对采一次样约 244×。需要新鲜时间戳的循环没有可外提的东西。`DateFormatter` 仍是独立规则。 + +每轮 `value(forKey:)` 相对读属性约 35×。每轮都变的名字不能变成直接属性。这与 Go 的 `FieldByName` 同族。 + +Lint: `sp-dateformatter-hot`, `sp-json-decode-hot`, `sp-regex-hot`, `sp-file-read-hot`, `sp-clock-hot`, `sp-value-for-key-hot`(默认 hint) +## Lint 对照 + +| ID | Smell | 默认 | +|----|-------|------| +| sp-print-hot | 循环内 `print(` | off(相对 String 约 10×;丢弃 stdout) | +| sp-array-no-reserve | 空 Array 追加且无 `reserveCapacity` | hint(小约 5×,一次大填充约 1.6×) | +| sp-dateformatter-hot | 循环内构造 date formatter | hint(有 JSONL;未 corpus 分级) | +| sp-json-decode-hot | 循环内 JSON decoder 或 jsonObject | hint(有 JSONL;未 corpus 分级) | +| sp-regex-hot | 循环内构造正则 | hint(有 JSONL;未 corpus 分级) | + +用 `--severity =hint` 启用 `off` 规则。2026-10-22 的五条规则仍无 JSONL。2026-10-23 新增三行有 JSONL 微基准并保持 hint。它们不属于 2026-10-22 corpus-ready 裁决。见 [evidence-policy.md](evidence-policy.md)。 + +## 参考资料 + +- [XPerf playbook](../../../../docs/zh-CN/playbook.md) diff --git a/packages/swiftperf/evidence/zh-CN/README.md b/packages/swiftperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..8dcef88 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/README.md @@ -0,0 +1,5 @@ +# swiftperf 证据 + +截至 2026-10-22 亚洲/上海(B 批次 G4 填充)。 + +原始的五条规则被**阻止**:在 2026-10-22 的测量环境中禁止使用 `swiftc`,因此没有 JSONL(没有被创建)。每条规则都有一个 `sp-*.md` 的阻止存根。2026-10-23 新增的三条规则(`sp-dateformatter-hot`、`sp-json-decode-hot`、`sp-regex-hot`)现在有 JSONL 微基准,并保持 **提示**。未进行语料库分类。政策:[`../docs/evidence-policy.md`](../../docs/zh-CN/evidence-policy.md)。根矩阵:[`../../../docs/rule-regression.md`](../../../../docs/zh-CN/rule-regression.md)。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-array-no-reserve.md b/packages/swiftperf/evidence/zh-CN/sp-array-no-reserve.md new file mode 100644 index 0000000..ad5c950 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-array-no-reserve.md @@ -0,0 +1,16 @@ +# sp-array-no-reserve — 已测量,仍然提示 + +**状态:**已测量。默认保持**提示**。2026-10-22语料库的裁定不包含此JSONL。 + +## 主张 + +一个包含 200000 个 `Int` 的 `Array` 在没有使用 `reserveCapacity` 的情况下追加,耗时约为已预留容量的 **1.6×**(0.293 毫秒对比 0.180 毫秒)。32 元素的数组重复 4000 次,耗时约为 **5.1×**(1.133 毫秒对比 0.221 毫秒)。计数一致。 + +## 如何测量 + +- `swiftc -O`,Apple Swift 6.3.2 +- 原始:[`sp-array-no-reserve.jsonl`](../sp-array-no-reserve.jsonl) + +## 限制 + +大型填充是适度的。匹配器也报告了该形状。不要升级。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-clock-hot.md b/packages/swiftperf/evidence/zh-CN/sp-clock-hot.md new file mode 100644 index 0000000..073f4f0 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-clock-hot.md @@ -0,0 +1,17 @@ +# sp-clock-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[sp-clock-hot.jsonl](../sp-clock-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`Date()` 每次迭代与一个样本。结果被保留,所以循环结束后不会被删除。`DateFormatter` 是一个不同的规则。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `sample_once` | 0.008 | 100000 | 1 | +| 之前 | `sample_each_iter` | 1.946 | 100000 | 243.642 | + +在采样一次之后,并且不会在循环内部构建日期。需要每次迭代都获取新时间戳的代码没有可提升的部分。默认保持 **hint**。这个比例不是普遍的加速方式。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-dateformatter-hot.md b/packages/swiftperf/evidence/zh-CN/sp-dateformatter-hot.md new file mode 100644 index 0000000..d8a44d3 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-dateformatter-hot.md @@ -0,0 +1,20 @@ +# sp-dateformatter-hot — 已测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[sp-dateformatter-hot.jsonl](../sp-dateformatter-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +一种日期的2000种格式。之前每次迭代都会构建DateFormatter;之后重用一个。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `formatter_once` | 1.482010 | 2000 | 1 | +| 之前 | `formatter_each_iter` | 44.153929 | 2000 | 29.793 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-file-read-hot.md b/packages/swiftperf/evidence/zh-CN/sp-file-read-hot.md new file mode 100644 index 0000000..32ad50c --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-file-read-hot.md @@ -0,0 +1,17 @@ +# sp-file-read-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[sp-file-read-hot.jsonl](../sp-file-read-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +一个单字节文件的 `String(contentsOfFile:)`,读 500 次,与读一次相比。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 后 | `read_once` | 0.014 | 500 | 1 | +| 前 | `read_each_iter` | 5.819 | 500 | 413.669 | + +该基准重新打开一个单字节文件。每次迭代都会变化的路径无法提升;测量的开销是打开和读取。默认保持 **hint**。这个比例不是普遍的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-json-decode-hot.md b/packages/swiftperf/evidence/zh-CN/sp-json-decode-hot.md new file mode 100644 index 0000000..28f12e9 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-json-decode-hot.md @@ -0,0 +1,20 @@ +# sp-json-decode-hot — 已测量,仍有提示 + +## 状态 + +**提示**。挂钟 JSONL:[sp-json-decode-hot.jsonl](../sp-json-decode-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +对一个固定的 JSON 对象进行 20000 次解码。解码一次后再次读取该字段。变化的负载在形状之后不能使用此方法。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `decode_once` | 0.011086 | 20000 | 1 | +| 之前 | `decode_each_iter` | 21.706939 | 20000 | 1957.968 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料库分流。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-print-hot.md b/packages/swiftperf/evidence/zh-CN/sp-print-hot.md new file mode 100644 index 0000000..27983fd --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-print-hot.md @@ -0,0 +1,16 @@ +# sp-print-hot — 已测量,保持关闭 + +**状态:** 已测量。默认保持 **关闭**。 + +## 主张 + +`print` 4000 个整数,并且将 stdout 指向 `/dev/null`,大约是用 `String` 构建相同的行的 **10 倍**(1.475 毫秒 对比 0.148 毫秒)。 + +## 如何测量 + +- `swiftc -O`,Apple Swift 6.3.2 +- 原始文件:[ `sp-print-hot.jsonl`](../sp-print-hot.jsonl) + +## 限制 + +后端不会锁定标准输出。必须打印的日志行不是这个比较。保持默认关闭。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-regex-hot.md b/packages/swiftperf/evidence/zh-CN/sp-regex-hot.md new file mode 100644 index 0000000..57c5c5e --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-regex-hot.md @@ -0,0 +1,20 @@ +# sp-regex-hot — 测量过的,仍然提示 + +## 状态 + +**提示**。挂钟 JSONL:[sp-regex-hot.jsonl](../sp-regex-hot.jsonl)。未进行语料分类。2026-10-22 的语料裁定不涵盖此标识。 + +## 测量 + +2000 次固定字符串的匹配。在之前,每次迭代都会构建 NSRegularExpression;之后只构建一次。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +|------|------|----------:|----:|------:| +| 在...之后 | `regex_once` | 0.307918 | 2000 | 1 | +| 之前 | `regex_each_iter` | 5.050063 | 2000 | 16.401 | + +微基准显示了这个形状的成本。默认保持 **hint** 直到形状经过语料库分流。这个比例不是一般的加速。 + +## 解阻条件 + +不要仅从此文件将默认设置为 `error`。可变输入、冷路径或其他 ABI 都可能抹去差异。在做任何默认更改之前,仍然需要进行语料筛选。 diff --git a/packages/swiftperf/evidence/zh-CN/sp-value-for-key-hot.md b/packages/swiftperf/evidence/zh-CN/sp-value-for-key-hot.md new file mode 100644 index 0000000..191cca0 --- /dev/null +++ b/packages/swiftperf/evidence/zh-CN/sp-value-for-key-hot.md @@ -0,0 +1,17 @@ +# sp-value-for-key-hot — 已测量,仍然提示 + +## 状态 +**提示**。实时时钟 JSONL:[sp-value-for-key-hot.jsonl](../sp-value-for-key-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`value(forKey:)` 每次迭代与读取属性。200000 次操作。结果被保留。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `direct_field` | 0.661 | 200000 | 1 | +| 之前 | `value_for_key_each` | 23.258 | 200000 | 35.185 | + +每次迭代都会变化的名称无法成为直接字段。默认保持 **提示**。这一比率不是一般的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/swiftperf/zh-CN/CONTRIBUTING.md b/packages/swiftperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..3b9f506 --- /dev/null +++ b/packages/swiftperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +本 kit 本地说明(构建、fixtures、证据):见本 package [README.md](README.md)。 diff --git a/packages/swiftperf/zh-CN/README.md b/packages/swiftperf/zh-CN/README.md new file mode 100644 index 0000000..c01f773 --- /dev/null +++ b/packages/swiftperf/zh-CN/README.md @@ -0,0 +1,39 @@ +# swiftperf + +Swift 性能笔记、小示例,以及 `swiftperf-lint`(Swift Package)。 + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。针对 2026-10-22 规则(AGENT.md G3)lint **corpus-ready**。默认:sp-array-no-reserve **hint**;sp-print-hot **off**。强制解包、字符串 `+=` 与 `map`/`joined` 在本地 `swiftc -O` 跑后已移除。三条较晚 hint(`sp-dateformatter-hot`、`sp-json-decode-hot`、`sp-regex-hot`)在该裁决之外。指南 v0 脚手架;无 Swift SDK 时有 text-lint 对等。 + +> **工具链:** 你运行的机器上可能没有 `swift`。源码 + `Package.swift` 仍随仓库提供;有 Swift 5.9+ 时再构建。夹具与文档在不编译时也完整。 + +## 目录 + +- [英文指南(v0)](../docs/zh-CN/swift-performance-guide.md) — 来自 `docs/guide-src/` +- [`swiftperf-lint`](../swiftperf-lint/README.md) — 从 sp-array-no-reserve 到热调用 hint(sp-array-no-reserve 与 2026-10-23 热调用规则为 hint;print 为 off) +- [语料固定目标](../docs/zh-CN/corpus.md) + +## 关注点 + +热循环里的强制解包、字符串拼接增长,以及热路径 `print`。没有本地基准的主张保持暂定。 + +## 运行 lint + +```sh +cd packages/swiftperf/swiftperf-lint +swift build -c release +"$(swift build -c release --show-bin-path)/swiftperf-lint" path/to/sources +bash ../scripts/smoke-lint.sh +``` + +无 Swift 时:`scripts/smoke-lint.sh` 经 [`scripts/text-lint.cjs`](../scripts/text-lint.cjs) 跑 G0(与 SPM 源启发式对等)。语料 G2/G3 用同一回退 — 见 [docs/corpus.md](../docs/zh-CN/corpus.md)。**corpus-ready: 是**(text-lint 对等;有 Swift 工具链时再用 SPM 复核)。 + +## 布局 + +```text +docs/guide-src/ 章节源 +docs/ 组装后的指南 + corpus.md + evidence-policy.md +evidence/ 阻塞 stub(有 Swift 工具链前无 JSONL) +examples/ 小 Swift 演示 +swiftperf-lint/ Package.swift + Sources + 夹具 +scripts/ smoke-lint.sh, self-lint.sh, text-lint.cjs(无 swift 时的回退) +``` diff --git a/packages/zperf/docs/guide-src/00_head.md b/packages/zperf/docs/guide-src/00_head.md index b8f7d58..eaaefdf 100644 --- a/packages/zperf/docs/guide-src/00_head.md +++ b/packages/zperf/docs/guide-src/00_head.md @@ -3,7 +3,7 @@ Less memory, more throughput. > **Target: Zig 0.17.0.** Examples under `examples/` are checked with Zig 0.17.0 on x86_64 Linux (`zig test` Debug and `-O fast`, or `zig build-exe`). Reproduce with `cd examples && zig build test`. -> Numbers come from release notes, original PRs, or are labeled as local measurements. Nothing is invented. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. Numbers come from release notes, original PRs, or are labeled as local measurements. Nothing is invented. > > Companion: `zperf-lint/` — static checks for performance and memory anti-patterns (Chapter 9). diff --git a/packages/zperf/docs/guide-src/01_method.md b/packages/zperf/docs/guide-src/01_method.md index 9e59d6d..dc0bcd1 100644 --- a/packages/zperf/docs/guide-src/01_method.md +++ b/packages/zperf/docs/guide-src/01_method.md @@ -2,12 +2,12 @@ TigerBeetle's TigerStyle order: -1. **Estimate at design time.** Bandwidth and latency for network, disk, memory, and CPU. The largest reported gains (up to 1000×) come from design changes, before profiling is possible. -2. **Fix the slowest resource first** (network > disk > memory > CPU), weighted by frequency. A cache miss repeated often enough can cost as much as an `fsync`. -3. **Batch.** Amortize network, disk, memory, and CPU across batches; give the CPU large, predictable units of work. -4. **Make hot work explicit.** e.g. a hot loop as a standalone function with raw parameters instead of `self` (see §5.6). +1. Estimate at design time. Bandwidth and latency for network, disk, memory, and CPU. The largest reported gains (up to 1000×) come from design changes, before profiling is possible. +2. Fix the slowest resource first (network > disk > memory > CPU), weighted by frequency. A cache miss repeated often enough can cost as much as an `fsync`. +3. Batch. Amortize network, disk, memory, and CPU across batches; give the CPU large, predictable units of work. +4. Make hot work explicit. e.g. a hot loop as a standalone function with raw parameters instead of `self` (see §5.6). -Andrew Kelley's *Practical Data-Oriented Design*: **less memory can mean faster code** — fewer allocations and better cache use. This guide starts with memory, then computation. +Andrew Kelley's *Practical Data-Oriented Design*: less memory can mean faster code — fewer allocations and better cache use. This guide starts with memory, then computation. Sources: [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) · [Practical Data-Oriented Design (Handmade Seattle 2021)](https://handmade-seattle.com/media-sub/practical-data-oriented-design) · [Zig PR #7920](https://github.com/ziglang/zig/pull/7920) diff --git a/packages/zperf/docs/guide-src/02_memory.md b/packages/zperf/docs/guide-src/02_memory.md index 8d00ed2..f503d28 100644 --- a/packages/zperf/docs/guide-src/02_memory.md +++ b/packages/zperf/docs/guide-src/02_memory.md @@ -286,7 +286,7 @@ test "buffer pool tryAlloc / release" { ### 2.5 Static allocation (TigerStyle) -TigerBeetle's rule: **allocate everything at startup; after init, do not allocate or free.** That removes allocation latency jitter and use-after-free from the steady state. Fix limits for queues, loops, and connection counts. +TigerBeetle's rule: allocate everything at startup; after init, do not allocate or free. That removes allocation latency jitter and use-after-free from the steady state. Fix limits for queues, loops, and connection counts. > Example: `examples/m11_static.zig` (checked with Zig 0.17.0 using `zig test`). @@ -414,7 +414,7 @@ test "phase allocator: alloc only in init" { #### Size the object / message pool from concurrency -Static allocation only works if the pool is **large enough to always make progress**. TigerBeetle's `MessagePool` sizes from worst-case concurrent holders: journal read/write IOPS, pipeline depth, connection send queues, grid repair reads, plus a small burst (+1) so a parse or ping cannot deadlock the replica. For any fixed-size frame pool: +Static allocation only works if the pool is large enough to always make progress. TigerBeetle's `MessagePool` sizes from worst-case concurrent holders: journal read/write IOPS, pipeline depth, connection send queues, grid repair reads, plus a small burst (+1) so a parse or ping cannot deadlock the replica. For any fixed-size frame pool: 1. List every concurrent holder (connections × in-flight, disk IOPS, pipelines, loops). 2. Add one for “holding a message while allocating the next”. @@ -1109,9 +1109,9 @@ test "delta + varint" { ### 2.16 Fragmentation, huge pages, and other notes -- **Fragmentation:** Mixing many small objects with different sizes and lifetimes in one general-purpose heap can fragment memory. Group allocations by lifetime (arena), by size (`MemoryPool` / size-class pools), or use static allocation. -- **Huge pages:** TLB misses can limit large, randomly accessed data such as hash tables and arrays. On Linux, `madvise(MADV_HUGEPAGE)` can request transparent huge pages for an `mmap` region. Measure the result with `perf stat -e dTLB-load-misses`. -- **ArrayList growth:** Since 0.14, the Allocator has `remap`, and `page_allocator` supports `mremap`. Large ArrayList growth may therefore remap pages instead of copying them. +- Fragmentation: Mixing many small objects with different sizes and lifetimes in one general-purpose heap can fragment memory. Group allocations by lifetime (arena), by size (`MemoryPool` / size-class pools), or use static allocation. +- Huge pages: TLB misses can limit large, randomly accessed data such as hash tables and arrays. On Linux, `madvise(MADV_HUGEPAGE)` can request transparent huge pages for an `mmap` region. Measure the result with `perf stat -e dTLB-load-misses`. +- ArrayList growth: Since 0.14, the Allocator has `remap`, and `page_allocator` supports `mremap`. Large ArrayList growth may therefore remap pages instead of copying them. - **`Deque` + `remap`:** `std.Deque` grows with `Allocator.remap` when the ring can stay in place; only then splits head/tail for a moving grow (`lib/std/deque.zig`). Prefer remap-friendly allocators behind large rings. ### 2.17 Relocatable slabs and idle physical reclaim (advanced) diff --git a/packages/zperf/docs/guide-src/05_compute.md b/packages/zperf/docs/guide-src/05_compute.md index 8a5082f..9c085a5 100644 --- a/packages/zperf/docs/guide-src/05_compute.md +++ b/packages/zperf/docs/guide-src/05_compute.md @@ -2,8 +2,8 @@ ### 5.1 `comptime`: move work to compile time -- **Lookup tables:** Build them in a `comptime` block so they live in read-only data and require no runtime initialization. For large loops, raise the compile-time branch quota with `@setEvalBranchQuota`. -- **Generic specialization:** A `comptime N: usize` parameter generates specialized code for each N. `inline for` unrolls the loop at compile time. +- Lookup tables: Build them in a `comptime` block so they live in read-only data and require no runtime initialization. For large loops, raise the compile-time branch quota with `@setEvalBranchQuota`. +- Generic specialization: A `comptime N: usize` parameter generates specialized code for each N. `inline for` unrolls the loop at compile time. - Assert relationships between constants at compile time with `comptime std.debug.assert(...)`, as TigerStyle recommends. > Example: `examples/p01_comptime_lut.zig` (checked with Zig 0.17.0 using `zig test`). @@ -279,7 +279,7 @@ Sources: [0.14 `@branchHint`](https://ziglang.org/download/0.14.0/release-notes. - **Use multiple accumulators or unroll loops** when measurements show a dependency chain is limiting throughput. `inline for` can create a fixed number of independent accumulators. Unrolling increases code size and instruction-cache pressure. - **`inline fn` is a language guarantee, not a hint.** The language reference calls it “semantic inlining”; compile-time-known arguments are compile-time-known inside the function. It can increase binary size and compile time, and can even hurt runtime performance. Usually let the compiler decide; use it when compile-time propagation or a specific stack-frame shape is required. - Pass large structs as `*const T` when they exceed 16 bytes and copying is not intended (TigerStyle). “Push ifs up, fors down”: move branches outside loops and move loops into batch-processing functions. -- **Initialize large structs in place:** `fn init(self: *Big) void { self.* = .{...}; }` avoids constructing a temporary on the stack and copying it (TigerStyle). +- Initialize large structs in place: `fn init(self: *Big) void { self.* = .{...}; }` avoids constructing a temporary on the stack and copying it (TigerStyle). > Example: `examples/p09_hot_loop.zig` (checked with Zig 0.17.0 using `zig test`). diff --git a/packages/zperf/docs/guide-src/07_build.md b/packages/zperf/docs/guide-src/07_build.md index 4a70638..9166cd5 100644 --- a/packages/zperf/docs/guide-src/07_build.md +++ b/packages/zperf/docs/guide-src/07_build.md @@ -53,7 +53,7 @@ pub fn build(b: *std.Build) void { ### 7.3 PGO -Zig does **not** support PGO for user programs as of this writing. [#15137](https://github.com/ziglang/zig/issues/15137) is “not planned”; research [#237](https://github.com/ziglang/zig/issues/237) on test-driven PGO is still open. Fallbacks: branch hints, hot/cold separation, data layout. +Zig does not support PGO for user programs as of this writing. [#15137](https://github.com/ziglang/zig/issues/15137) is “not planned”; research [#237](https://github.com/ziglang/zig/issues/237) on test-driven PGO is still open. Fallbacks: branch hints, hot/cold separation, data layout. Sources: `zig build-exe --help` (0.17.0) · [0.17 rename of lang.OptimizeMode to lang.Optimize](https://ziglang.org/download/0.17.0/release-notes.html) · [0.16 Lazy Field Analysis and loop safety-check code generation](https://ziglang.org/download/0.16.0/release-notes.html) · `lib/std/Build/Step/Compile.zig` @@ -67,10 +67,10 @@ Useful when building tools that look like compilers (parsers, LSs, bundlers). Fu | SoA + `extra` + `string_bytes` | `lib/std/zig/Zir.zig` | Dense IR; rare ops out of line (§2.8) | | Sharded intern + tid indices | `src/InternPool.zig` | Concurrent intern without a global lock (§2.10) | | Labeled-switch tokenizer | `lib/std/zig/tokenizer.zig` | Same as §5.4; keywords via `StaticStringMap` | -| Optional tracy hooks | `src/tracy.zig` | Instrument self-host builds when chasing compile time (**U**) | +| Optional tracy hooks | `src/tracy.zig` | Instrument self-host builds when chasing compile time (U) | | Content-addressed build cache | `lib/std/Build/Cache.zig` | SipHash manifests for incremental artifacts (§8.4 adjacent) | -Do not cargo-cult compiler internals into small apps. Steal the **shape** (indices, SoA, shard, ensure heuristics) after measuring. +Do not copy compiler internals into small apps. Steal the shape (indices, SoA, shard, growth heuristics) after measuring. --- diff --git a/packages/zperf/docs/guide-src/08_measure.md b/packages/zperf/docs/guide-src/08_measure.md index 560f655..6022b9b 100644 --- a/packages/zperf/docs/guide-src/08_measure.md +++ b/packages/zperf/docs/guide-src/08_measure.md @@ -2,9 +2,9 @@ ### 8.1 Microbenchmarks -Since 0.16, `std.time.Timer` is gone. Time through `Io`: `std.Io.Clock.Timestamp.now(io, .awake)` and `.untilNow(io)`. Use `std.mem.doNotOptimizeAway` so the compiler cannot delete the work. Run several trials; keep the **best** (or report a distribution) to cut interrupt and cold-cache noise. +Since 0.16, `std.time.Timer` is gone. Time through `Io`: `std.Io.Clock.Timestamp.now(io, .awake)` and `.untilNow(io)`. Use `std.mem.doNotOptimizeAway` so the compiler cannot delete the work. Run several trials; keep the best (or report a distribution) to cut interrupt and cold-cache noise. -Record Zig version, optimize mode, OS, and CPU model so another machine can compare runs. The example prints one **JSONL** line per result. +Record Zig version, optimize mode, OS, and CPU model so another machine can compare runs. The example prints one JSONL line per result. > Example: `examples/p12_bench.zig` (checked with Zig 0.17.0 using `zig run -O fast`). diff --git a/packages/zperf/docs/guide-src/10_cheat.md b/packages/zperf/docs/guide-src/10_cheat.md index a7bcf35..93b194e 100644 --- a/packages/zperf/docs/guide-src/10_cheat.md +++ b/packages/zperf/docs/guide-src/10_cheat.md @@ -23,7 +23,7 @@ | Read a large file | `File.createMemoryMap` + zero-copy slices | | Growable ring | `Deque` (uses `Allocator.remap` when possible) | -**Compute faster** +Compute faster | Goal | Use | |---|---| diff --git a/packages/zperf/docs/zh-CN/community-tooling.md b/packages/zperf/docs/zh-CN/community-tooling.md new file mode 100644 index 0000000..5b265aa --- /dev/null +++ b/packages/zperf/docs/zh-CN/community-tooling.md @@ -0,0 +1,18 @@ +# Zig 代码检查工具:范围与比较 + +有关相关 Zig Linter 的说明以及 `zperf-lint` 的位置。版本会变化;在依赖此表之前请重新检查上游。 + +## `zperf-lint` 的作用 + +它会寻找编译器很少标记的性能和内存模式:循环内分配、移动队列、可能的填充、空 I/O 缓冲区以及类似的源代码信号。检查是基于语法的启发式方法。它们不能替代编译、分析、基准测试或常规 lint 检查。 + +CI 使用社区工具运行它,命令为 `zig run scripts/repo.zig -- lint-all`:正确性工具会阻止执行;样式或版本不匹配的检查仅作建议。总的 CI 配置位于仓库根目录 [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml)(任务 `zperf`)。嵌套包工作流已被移除;参见 [`ci/README.md`](../../ci/README.md)。 + +## 设计边界 + +- 仅在源模式有明确的性能后果时添加规则。 +- 记录误报风险和抑制路径。 +- 除非编译器 API 可靠地暴露类型分析,否则不要进行类型分析。 +- 将发现视为测量提示——Lint 命中并不等于测量出的加速。 + +规则和限制:[``zperf-lint``](../../zperf-lint/README.md)。持续集成(CI):[根目录 `ci.yml`](../../../../.github/workflows/ci.yml) · [`ci/README.md`](../../ci/README.md)。 diff --git a/packages/zperf/docs/zh-CN/corpus.md b/packages/zperf/docs/zh-CN/corpus.md new file mode 100644 index 0000000..4831b97 --- /dev/null +++ b/packages/zperf/docs/zh-CN/corpus.md @@ -0,0 +1,64 @@ +# zperf 语料库目标 + +大型树木**不会**被供应商化,并且语料库不会随仓库一起发布:将其克隆到仓库外的任意本地目录,并将该路径传递给代码检查器。使用以下命令进行检查: + +```sh +cd packages/zperf/zperf-lint && zig build -Doptimize=fast +./zig-out/bin/zperf-lint --no-fail -f json / +``` + +## 固定目标 + +克隆于 2026-10-22(亚洲/上海)。 + +| 树 | 链接 | 提交 | 大小 | `.zig` | +|------|-----|--------|------|-------:| +| **zig** (`lib/std`, `src`) | https://codeberg.org/ziglang/zig.git | `8f0c6ce407f9` | 总计 312M 的一部分 | 1072 个遍历树 | +| **tigerbeetle** (`src/`) | https://github.com/tigerbeetle/tigerbeetle.git | `0450c32b2c3c` | 14M | 248 | +| **ghostty** | https://github.com/ghostty-org/ghostty.git | `67edd20a0292` | 63M | 603 | +| **http.zig**(次要) | https://github.com/karlseguin/http.zig.git | `e8272bfd5727` | 1.1M | 33 | + +主要 G2 套装:**zig(标准库 + 编译器)** + **tigerbeetle** + **ghostty**(≥3 个大型生产树)。 +http.zig 是一个精简的辅助套装。 + +检查器是 **Zig**,`std.zig.Ast`,同语言。默认路径跳过:tests / Build / benchmarks / **fuzz** / scripts / `compiler_rt`(参见 [`zperf-lint/README.md`](../../zperf-lint/README.md))。 + +## G3/r4 结果(2026-10-22) + +在默认规则映射下,每次运行的计数:r1 → r3 → **r4**。 + +| Tree | r1 → r3 → **r4** (defaults) | +|------|------------------------------| +| **zig** `lib/std` | **403 → 391 → 12** | +| **zig** `src` (编译器) | **859 → 856 → 3** | +| **tigerbeetle** `src/` | **252 → 184 → 1** | +| **ghostty** | **441 → 425 → 9** | +| **http.zig**(次要) | **25 → 24 → 1** | + +**G3/r4 之后默认关闭**(成熟 Zig 上的 OK-习惯集合):zp-alloc-hot、zp-append-without-reserve、zp-debug-allocator、zp-large-struct-by-value、zp-print-hot、zp-unbuffered-io、zp-missing-free、zp-packable-bools、zp-usize-index-field、zp-aos-single-field、zp-catch-unreachable-alloc、zp-oversized-enum-tag、zp-optional-scalar-field、zp-bool-array、zp-runtime-div-hot、zp-per-packet-alloc、zp-map-put-without-ensure、zp-catch-unreachable-io。CLI 不会报告 `off` 规则(请参见 [`zperf-lint/README.md`](../../zperf-lint/README.md#rules))。 + +**默认开启:** zp-page-allocator-small, zp-broad-runtime-safety, zp-static-string-map, zp-queue-ordered-remove, zp-shift-on-read, zp-invariant-float-div, zp-arena-reset-free-all, zp-create-destroy-hot, zp-fixed-size-alloc-hot. + +### 反向优化 + 降级 + +- 路径跳过:`*fuzz*.zig`,`**/fuzz/**`(合并到 `--include-tests` 中) +- zp-page-allocator-small:精确匹配 `page_allocator` / `*.page_allocator`(不包括 `huge_page_allocator`) +- zp-debug-allocator:仅匹配 `DebugAllocator(` / `GPA(` 实例化;跳过重新导出和测试上下文 +- zp-print-hot:跳过 `if (log)` / `if (logging)` 诊断门控 +- **r4:** 将几乎所有 OK-idiom 规则降级为默认 **`off`**;保留带有 TP 的规则(上面默认开启列表)在默认映射中开启 + +### 语料库准备:**是** + +G3 + r4 降级完成。全 r4 剩余样本 **n=26**:TP **73.1%**,OK-idiom **26.9%**,FP+DOC+SKIP-path **0%**。在 zp-queue-ordered-remove / zp-shift-on-read / zp-broad-runtime-safety / zp-fixed-size-alloc-hot 上真实 TP。没有 G4 不会大规模升级。 + +## 库存 + +```text +/ + zig/ # symlink → zig-corpus/zig + tigerbeetle/ # symlink + ghostty/ # symlink + http.zig/ # symlink (secondary) +``` + +不要将语料来源提交到 `packages/zperf`。 diff --git a/packages/zperf/docs/zh-CN/evidence-policy.md b/packages/zperf/docs/zh-CN/evidence-policy.md new file mode 100644 index 0000000..a12c0df --- /dev/null +++ b/packages/zperf/docs/zh-CN/evidence-policy.md @@ -0,0 +1,73 @@ +# zperf 证据政策 + +## 门槛 + +只有在 `packages/zperf/evidence/` 下同时存在以下两项时,zperf 规则才可以被记录/视为 **速度 `错误`**(cperf/jsperf 风格升级): + +1. `.md` — 声明、利用、Zig 版本 / 优化模式、限制 +2. `.jsonl` — ≥1 个前/后行,包含字段 `rule`、`case`、`role`、`zig`、`optimize`、`median_ms`、`ops`、`ratio`、`ts` + +否则保持 **rules.zig** 的严重性(`warning` / `info` / `hint` / `off`),并将其视为评论提示,而不是“必须修复以提高速度”的 CI 门控。 + +拥有 JSONL 是**必要的,但不充分的**。只有当实际时间明显同意**并且**故障模式符合规则时才升级。适度的比例、依赖环境的 I/O 或优化器省略的基准仍然保持非错误。 + +今天的 CLI 仍可能因任何发现而使流程失败;此政策是 **保留 / 升级** 合同,直到预设生效。 + +## 当前映射 + +截至2026-10-22:使用 Zig **0.17.0** `-Ofast` 测量,取 9 次运行的中位数,测试工具 `examples/bench/evidence_fill.zig`。 + +**G3/r4 (2026-10-22):** 在对成熟的 Zig 语料库进行 OK-idiom 大规模测试后,若干规则降级为默认 **`关闭`**(参见 [corpus.md](corpus.md);门控定义见 [AGENT.md](../../../../zh-CN/AGENT.md))。这些规则的证据 JSONL 文件仍然有效;在未对默认开启集合重新验证 G4 之前,**不要**将其升级为 `error`。 + +| 规则 | rules.zig sev | 证据 | 测量比率 | 升级? | +|------|---------------|----------|-----------|-----------| +| zp-alloc-hot | 关闭 | [zp-alloc-hot](../../evidence/zh-CN/zp-alloc-hot.md) | 相对于 arena 保留约 **1.4×** GPA | **否**(适中;对于固定大小更推荐使用 zp-create-destroy-hot / zp-fixed-size-alloc-hot) | +| zp-append-without-reserve | 关闭 | [zp-append-without-reserve](../../evidence/zh-CN/zp-append-without-reserve.md) | ~**2.1×** | 否(信息) | +| zp-page-allocator-small | 警告 | [zp-page-allocator-small](../../evidence/zh-CN/zp-page-allocator-small.md) | ~**341×** 页面 vs GPA | **候选错误** | +| zp-print-hot | 关闭 | [zp-print-hot](../../evidence/zh-CN/zp-print-hot.md) | ~**6e4×** 打印对比安静 | 否 — 环境 I/O(如 jsperf 的 jp-console-hot) | +| zp-unbuffered-io | 关闭 | [zp-unbuffered-io](../../evidence/zh-CN/zp-unbuffered-io.md) | ~**49×** 空 vs 4KiB | **候选错误** | +| zp-aos-single-field | 关闭 | [zp-aos-single-field](../../evidence/zh-CN/zp-aos-single-field.md) | ~**6.6×** AoS 对比 SoA | 否(审查提示) | +| zp-bool-array | 关闭 | [zp-bool-array](../../evidence/zh-CN/zp-bool-array.md) | ~**30×** bool[] 对比 bitset | 否(信息) | +| zp-static-string-map | 信息 | [zp-static-string-map](../../evidence/zh-CN/zp-static-string-map.md) | ~**2.5×** | 否(信息) | +| zp-runtime-div-hot | 关闭 | [zp-runtime-div-hot](../../evidence/zh-CN/zp-runtime-div-hot.md) | 约 **12×** 模与掩码 | 否(审查提示) | +| zp-queue-ordered-remove | 警告 | [zp-queue-ordered-remove](../../evidence/zh-CN/zp-queue-ordered-remove.md) | ~**466×** orderedRemove 与 ring | **候选错误** | +| zp-shift-on-read | 警告 | [zp-shift-on-read](../../evidence/zh-CN/zp-shift-on-read.md) | ~**17×** | **候选错误** | +| zp-per-packet-alloc | 关闭 | [zp-per-packet-alloc](../../evidence/zh-CN/zp-per-packet-alloc.md) | 共享 zp-alloc-hot ~1.4× | 否(与 zp-alloc-hot 相同) | +| zp-不变浮点除法 | 提示 | [zp-不变浮点除法](../../evidence/zh-CN/zp-invariant-float-div.md) | ~**2.0×** | 保持提示 | +| zp-map-put-without-ensure | 关闭 | [zp-map-put-without-ensure](../../evidence/zh-CN/zp-map-put-without-ensure.md) | ~**2.8×** | 否(信息) | +| zp-arena-reset-free-all | 警告 | [zp-arena-reset-free-all](../../evidence/zh-CN/zp-arena-reset-free-all.md) | ~**3.2×** free_all 与 retain | 临界警告 | +| zp-create-destroy-hot | 信息 | [zp-create-destroy-hot](../../evidence/zh-CN/zp-create-destroy-hot.md) | ~**11×** | **候选错误** | +| zp-fixed-size-alloc-hot | 信息 | [zp-fixed-size-alloc-hot](../../evidence/zh-CN/zp-fixed-size-alloc-hot.md) | ~**8.5×** | **候选错误** | +| zp-open-file-hot | 提示 | [zp-open-file-hot](../../evidence/zh-CN/zp-open-file-hot.md) | ~**2134次** 打开一次 | 否(提示;路径可能会改变) | +| zp-clock-hot | 提示 | [zp-clock-hot](../../evidence/zh-CN/zp-clock-hot.md) | ~**42×** 样本一次 | 否(提示;新的时间戳没有可提升的内容) | +| zp-json-hot | 提示 | [zp-json-hot](../../evidence/zh-CN/zp-json-hot.md) | ~**2683×** 解析一次 | 否(提示;不可提升可变的有效负载) | +| zp-alloc-print-hot | 提示 | [zp-alloc-print-hot](../../evidence/zh-CN/zp-alloc-print-hot.md) | ~**50×** GPA 打印一次;arena ~**12×** | 否(提示;一个变化的值不能被提升) | + +### 本次未测量 / 不适用 + +| 规则 | 状态 | 原因 | +|------|--------|-----| +| zp-debug-allocator | **不适用(仅限静态)** | 构建模式分配器选择;非单一二进制前/后 | +| zp-large-struct-by-value | **结论不明确** | ReleaseFast 通过值传递与指针微基准测试省略 (~50 ns 总计) — 不可引用 | +| zp-missing-free | **不适用(正确性)** | 缺少 free/deinit — 导致内存泄漏/未定义行为,不影响速度 | +| zp-broad-runtime-safety | **待定措施** | 广泛的 `@setRuntimeSafety(false)` —— 仅在 ReleaseSafe 下有意义 | +| zp-packable-bools | **待测量** | 可打包布尔值——密度声明;尚无 wall JSONL | +| zp-usize-index-field | **待定措施** | usize 索引字段 — 密度;尚无 wall JSONL | +| zp-catch-unreachable-alloc | **不适用(正确性)** | 在 alloc 上使用 `catch unreachable` — OOM 时未定义行为 | +| zp-oversized-enum-tag | **不适用(静态大小)** | 超大枚举标签 — ABI/大小;无墙 JSONL | +| zp-optional-scalar-field | **待定测量** | 可选标量填充——密度;尚无 wall JSONL | +| zp-catch-unreachable-io | **不适用(正确性)** | 在 I/O 上 `catch unreachable` | + +## 再生 + +```sh +bash packages/zperf/scripts/regen-evidence.sh +``` + +在 Zig 次要升级后重新运行;如果比率发生显著变化,更新匹配的 `.md` 中位数。 + +## 候选错误(策略,目前还不是 CLI 预设) + +这些测量结果高度一致:**zp-page-allocator-small, zp-queue-ordered-remove, zp-shift-on-read, zp-create-destroy-hot, zp-fixed-size-alloc-hot**(zp-unbuffered-io 在 G3 OK-idiom 后被降级关闭)。 + +在 `--preset` 到达之前保持为警告/信息;在没有 G4 + preset 的 CI 中不要生成 `error`。r4 之后默认开启:zp-page-allocator-small、zp-broad-runtime-safety、zp-static-string-map、zp-queue-ordered-remove、zp-shift-on-read、zp-invariant-float-div、zp-arena-reset-free-all、zp-create-destroy-hot、zp-fixed-size-alloc-hot。 diff --git a/packages/zperf/docs/zh-CN/rule-ids.md b/packages/zperf/docs/zh-CN/rule-ids.md new file mode 100644 index 0000000..c83a74a --- /dev/null +++ b/packages/zperf/docs/zh-CN/rule-ids.md @@ -0,0 +1,59 @@ +# zperf 规则 ID(slug) + +Canonical ids are **slugs only**. Numerics from the old map are historical and **not** accepted. + +| Slug | +|------| +| `zp-alloc-hot` | +| `zp-append-without-reserve` | +| `zp-page-allocator-small` | +| `zp-debug-allocator` | +| `zp-large-struct-by-value` | +| `zp-print-hot` | +| `zp-unbuffered-io` | +| `zp-missing-free` | +| `zp-broad-runtime-safety` | +| `zp-packable-bools` | +| `zp-usize-index-field` | +| `zp-aos-single-field` | +| `zp-catch-unreachable-alloc` | +| `zp-oversized-enum-tag` | +| `zp-optional-scalar-field` | +| `zp-bool-array` | +| `zp-static-string-map` | +| `zp-runtime-div-hot` | +| `zp-queue-ordered-remove` | +| `zp-shift-on-read` | +| `zp-per-packet-alloc` | +| `zp-invariant-float-div` | +| `zp-map-put-without-ensure` | +| `zp-catch-unreachable-io` | +| `zp-arena-reset-free-all` | +| `zp-create-destroy-hot` | +| `zp-fixed-size-alloc-hot` | +| `zp-open-file-hot` | +| `zp-clock-hot` | +| `zp-json-hot` | +| `zp-alloc-print-hot` | + +另见 [根目录 rule-id-map](../../../../docs/zh-CN/rule-id-map.md)。 + +## 已重命名 2026-10-27 (墓碑化旧 ID;永不复用) + +Default **`--scope all`** (or kit-equivalent). 旧的 `*-in-loop` ID 会误导工具/模型;它们被墓碑化在 `evidence/_removed/` 下。 + +| 旧 slug | 新 slug | +|----------|----------| +| `zp-alloc-in-loop` | `zp-alloc-hot` | +| `zp-alloc-print-in-loop` | `zp-alloc-print-hot` | +| `zp-clock-in-loop` | `zp-clock-hot` | +| `zp-create-destroy-in-loop` | `zp-create-destroy-hot` | +| `zp-fixed-size-alloc-in-loop` | `zp-fixed-size-alloc-hot` | +| `zp-json-in-loop` | `zp-json-hot` | +| `zp-open-file-in-loop` | `zp-open-file-hot` | +| `zp-print-in-loop` | `zp-print-hot` | +| `zp-runtime-div-in-loop` | `zp-runtime-div-hot` | + +## Removed + +- `zp-extern-struct-padding` — walk about 1.21×, 24 bytes versus 16; see `evidence/_removed/` diff --git a/packages/zperf/docs/zh-CN/zig-performance-guide.md b/packages/zperf/docs/zh-CN/zig-performance-guide.md index dfed36e..c143b3e 100644 --- a/packages/zperf/docs/zh-CN/zig-performance-guide.md +++ b/packages/zperf/docs/zh-CN/zig-performance-guide.md @@ -3,7 +3,7 @@ 少占内存,提高吞吐。 > **目标版本:Zig 0.17.0。** 示例在 `examples/`,于 **Zig 0.17.0 / x86_64-linux** 上用 `zig test`(Debug 与 `-O fast`)或 `zig build-exe` 校验(`cd examples && zig build test` 可复现)。 -> 性能数字来自官方 release notes、原始 PR,或标注为本地实测;没有编造的基准。 +> 加速数字会标明本地 bench、上游来源,或标为暂定。语料库残留计数是 lint 命中次数,不是墙钟耗时节省。数字来自发布说明、原始 PR,或标明为本机测量。不编造数字。 > > 配套:`zperf-lint/` —— 性能/内存反模式静态检查(第 9 章)。 @@ -59,12 +59,12 @@ Zig 未到 1.0,版本可能破坏 API。面向 0.11–0.13 的教程常常编 TigerBeetle TigerStyle 的顺序: -1. **设计阶段估算**:网络、磁盘、内存、CPU 的带宽与延迟。最大收益(他们说的“1000×”)往往出现在还无法 profile 的设计阶段。 -2. **先处理最慢的资源**(网络 > 磁盘 > 内存 > CPU),并加权频率:频繁的 cache miss 可以贵到接近一次 `fsync`。 -3. **批处理**:把网络、磁盘、内存、CPU 成本摊到批次上;给 CPU 大块、可预测的工作。 -4. **热点写清楚**:例如把热循环抽成只收原始参数、不带 `self` 的函数(见 §5.6)。 +1. 设计阶段估算:网络、磁盘、内存、CPU 的带宽与延迟。最大收益(他们说的“1000×”)往往出现在还无法 profile 的设计阶段。 +2. 先处理最慢的资源(网络 > 磁盘 > 内存 > CPU),并加权频率:频繁的 cache miss 可以贵到接近一次 `fsync`。 +3. 批处理:把网络、磁盘、内存、CPU 成本摊到批次上;给 CPU 大块、可预测的工作。 +4. 热点写清楚:例如把热循环抽成只收原始参数、不带 `self` 的函数(见 §5.6)。 -Andrew Kelley *Practical Data-Oriented Design*:**少用内存可以更快**——分配更少、缓存更友好。本文先讲内存,再讲运算。 +Andrew Kelley *Practical Data-Oriented Design*:少用内存可以更快——分配更少、缓存更友好。本文先讲内存,再讲运算。 来源:[TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) · [Practical DOD(Handmade Seattle 2021)](https://handmade-seattle.com/media-sub/practical-data-oriented-design) · [Zig PR #7920](https://github.com/ziglang/zig/pull/7920) @@ -350,7 +350,7 @@ test "buffer pool tryAlloc / release" { ### 2.5 静态分配(TigerStyle) -TigerBeetle 规则:**启动时分配全部内存;初始化之后不再分配或释放。** 去掉稳态分配延迟抖动与 use-after-free。队列、循环、连接数都设固定上限。 +TigerBeetle 规则:启动时分配全部内存;初始化之后不再分配或释放。 去掉稳态分配延迟抖动与 use-after-free。队列、循环、连接数都设固定上限。 > 📄 `examples/m11_static.zig`(Zig 0.17.0 下 `zig test` 通过) @@ -476,7 +476,7 @@ test "phase allocator: alloc only in init" { #### 按并发给对象 / 消息池定容 -静态分配只有在池**大到总能推进**时才安全。TigerBeetle 的 `MessagePool` 按最坏并发占用来计:journal 读写 IOPS、pipeline 深度、连接发送队列、grid 修复读,再加一小段突发(+1),避免 parse/ping 卡死副本。定长帧池可按此素描: +静态分配只有在池大到总能推进时才安全。TigerBeetle 的 `MessagePool` 按最坏并发占用来计:journal 读写 IOPS、pipeline 深度、连接发送队列、grid 修复读,再加一小段突发(+1),避免 parse/ping 卡死副本。定长帧池可按此素描: 1. 列出每个并发持有者(连接 × 在途、磁盘 IOPS、pipeline、循环)。 2. 再加一:持有一条消息的同时申请下一条。 @@ -646,7 +646,6 @@ test "out-of-band bool + hot/cold split" { 来源:[Practical DOD](https://handmade-seattle.com/media-sub/practical-data-oriented-design) · [演讲文字稿](https://josherich.me/podcast/andrew-kelley-practical-data-oriented-design-dod) · [PR #7920 AST 内存布局重做](https://github.com/ziglang/zig/pull/7920) · [Mitchell Hashimoto: Zig Parser](https://mitchellh.com/zig/parser) · `lib/std/multi_array_list.zig` - **证据(本地):** `examples/p15_soa_bandwidth.zig` — 对 N=4e6 个元素累加一个热 `u32` 字段(4 遍,best-of-7,`-O fast`):AoS vs `MultiArrayList.items(.hp)` vs 并行 `[]u32`。JSONL:`zig-perf-research/bench-results/p15_soa_bandwidth.jsonl`(引用前请重跑)。 ### 2.9 位级压缩:packed struct、任意位宽整数 @@ -878,7 +877,6 @@ test "NaN-boxing" { 来源:[InternPool PR #15569](https://github.com/ziglang/zig/pull/15569) · [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) · [0.16 Forbid Pointers in Packed Structs](https://ziglang.org/download/0.16.0/release-notes.html) - 当“数组”是一块可重定位、内部混排类型的 mmap slab 时,优先用**相对该基址的字节偏移**,而不是裸指针——见 §2.17。 ### 2.11 字符串驻留与小字符串优化 @@ -1172,9 +1170,9 @@ test "delta + varint" { ### 2.16 碎片、大页与其他 -- **碎片**:大量不同大小、不同寿命的小对象混在一个通用堆里最容易产生碎片。按生命周期分组(arena)、按大小分组(MemoryPool)、或者静态分配,都能从根上避免。 -- **大页(huge pages)**:大块、随机访问的内存(哈希表、大数组)上 TLB miss 可能是瓶颈。Linux 上可以对 mmap 出来的区域 `madvise(MADV_HUGEPAGE)` 使用透明大页;是否有收益必须实测(`perf stat -e dTLB-load-misses`)。 -- **ArrayList 扩容**:0.14 起 Allocator 有 `remap`,`page_allocator` 支持 `mremap`,所以大 ArrayList 扩容时可能由内核重映射页而不是 memcpy。 +- 碎片:大量不同大小、不同寿命的小对象混在一个通用堆里最容易产生碎片。按生命周期分组(arena)、按大小分组(MemoryPool)、或者静态分配,都能从根上避免。 +- 大页(huge pages):大块、随机访问的内存(哈希表、大数组)上 TLB miss 可能是瓶颈。Linux 上可以对 mmap 出来的区域 `madvise(MADV_HUGEPAGE)` 使用透明大页;是否有收益必须实测(`perf stat -e dTLB-load-misses`)。 +- ArrayList 扩容:0.14 起 Allocator 有 `remap`,`page_allocator` 支持 `mremap`,所以大 ArrayList 扩容时可能由内核重映射页而不是 memcpy。 - **`Deque` + `remap`:** `std.Deque` 扩容尽量走 `Allocator.remap`,环能原地长大才拆 head/tail 搬迁(`lib/std/deque.zig`)。大环后面用支持 remap 的分配器更合适。 --- @@ -1526,8 +1524,8 @@ test "car case study" { ### 5.1 comptime:把计算挪到编译期 -- **查找表**:用 `comptime` 块生成,表直接进只读数据段,运行时零初始化成本。循环次数多时用 `@setEvalBranchQuota` 提高编译期分支配额。 -- **泛型特化**:`comptime N: usize` 参数让每个 N 生成一份专用代码;`inline for` 在编译期完全展开。 +- 查找表:用 `comptime` 块生成,表直接进只读数据段,运行时零初始化成本。循环次数多时用 `@setEvalBranchQuota` 提高编译期分支配额。 +- 泛型特化:`comptime N: usize` 参数让每个 N 生成一份专用代码;`inline for` 在编译期完全展开。 - 编译期断言常量之间的关系(TigerStyle 强烈推荐):`comptime std.debug.assert(...)`。 > 📄 `examples/p01_comptime_lut.zig`(Zig 0.17.0 下 `zig test` 通过) @@ -1804,8 +1802,8 @@ test "hints" { - **热循环抽成独立函数,只接受原始参数,不带 `self`**(TigerStyle):编译器不需要证明 `self.field` 没被别名修改,就能把它放进寄存器;读者也更容易看出冗余计算。 - **多路累加器 / 循环展开**:`inline for` 生成固定数量的独立累加器,打破加法依赖链。展开会增大代码,i-cache 压力是真实成本。 - **`inline fn` 不是提示**:语言参考明确说它是"语义上的内联",编译期已知的实参在被调函数内也视为编译期参数;代价是可能伤害二进制大小、编译速度,甚至运行时性能。一般让编译器自己决定,只在需要编译期传播或确保栈帧形态时才用。 -- **大结构体按 `*const T` 传**(TigerStyle:>16 字节且无意拷贝时),以及"push ifs up, fors down"——把分支提到循环外,把循环下推到批处理函数里。 -- 大结构体**原地初始化**:`fn init(self: *Big) void { self.* = .{...}; }`,避免先在栈上构造再拷贝(TigerStyle)。 +- 大结构体按 `*const T` 传(TigerStyle:>16 字节且无意拷贝时),以及"push ifs up, fors down"——把分支提到循环外,把循环下推到批处理函数里。 +- 大结构体原地初始化:`fn init(self: *Big) void { self.* = .{...}; }`,避免先在栈上构造再拷贝(TigerStyle)。 > 📄 `examples/p09_hot_loop.zig`(Zig 0.17.0 下 `zig test` 通过) @@ -2188,7 +2186,7 @@ pub fn build(b: *std.Build) void { ### 7.3 PGO -截至本文,Zig **不支持**对用户程序做 profile-guided optimization:相关请求 [#15137](https://github.com/ziglang/zig/issues/15137) 已以 "not planned" 关闭;"基于测试用例的自动 PGO" 的研究性 issue [#237](https://github.com/ziglang/zig/issues/237) 仍开着。替代办法是手动:`@branchHint`、冷热函数分离、布局调整。 +截至本文,Zig 不支持对用户程序做 profile-guided optimization:相关请求 [#15137](https://github.com/ziglang/zig/issues/15137) 已以 "not planned" 关闭;"基于测试用例的自动 PGO" 的研究性 issue [#237](https://github.com/ziglang/zig/issues/237) 仍开着。替代办法是手动:`@branchHint`、冷热函数分离、布局调整。 来源:`zig build-exe --help`(0.17.0)· [0.17 Rename lang.OptimizeMode to lang.Optimize](https://ziglang.org/download/0.17.0/release-notes.html) · [0.16 Lazy Field Analysis / For Loop Safety Checks](https://ziglang.org/download/0.16.0/release-notes.html) · `lib/std/Build/Step/Compile.zig` @@ -2205,7 +2203,7 @@ pub fn build(b: *std.Build) void { | 可选 tracy 钩子 | `src/tracy.zig` | 追编译时间时可插桩(**U**) | | 内容寻址构建缓存 | `lib/std/Build/Cache.zig` | SipHash 清单做增量产物(邻接 §8.4) | -别把编译器内部生搬进小程序。量过之后再偷**形状**(索引、SoA、分片、ensure 启发式)。 +别把编译器内部照搬进小程序。量过之后再借用形状(索引、SoA、分片、扩容启发式)。 --- @@ -2215,7 +2213,7 @@ pub fn build(b: *std.Build) void { 0.16 起 `std.time.Timer` 已移除。计时走 `Io`:`std.Io.Clock.Timestamp.now(io, .awake)` 与 `.untilNow(io)`。用 `std.mem.doNotOptimizeAway` 防止被测工作被删掉。多跑几轮,取**最佳**(或报分布),减轻中断与冷缓存噪声。 -记录 Zig 版本、优化模式、OS、CPU 型号,便于跨机对比。示例每条结果打一行 **JSONL**。 +记录 Zig 版本、优化模式、OS、CPU 型号,便于跨机对比。示例每条结果打一行 JSONL。 > 📄 `examples/p12_bench.zig`(Zig 0.17.0 下 `zig run -O fast` 通过) @@ -2387,7 +2385,7 @@ $ echo $? 另外,Zig 0.16 代码里的 `.{x} ** N` 会先按等长替换再解析(0.17 分词器已去掉 `**`)。 -**自举时学到的一条**:`Ast.tokenLocation(0, tok)` 每次都从文件头扫描,是 O(文件长度)。对每条发现、每个函数都调用它,就会变成 O(n²):zperf-lint v0.3 的规则阶段曾是解析阶段的 4 倍。正确做法是解析后一次扫描,建一张行首偏移表,之后二分查找。再加上每文件 arena `reset(.retain_capacity)`(§2.2)和 `Io.Group` 并行、各写各槽(§6.2),在 Zig 0.17 std(563 个文件)上从 527 ms 降到 41 ms,峰值 RSS 从 64 MiB 降到 17 MiB,输出逐字节不变。详见 `zperf-lint/README.md` 的“自举 / dogfooding”。 +自举时学到的一条:`Ast.tokenLocation(0, tok)` 每次都从文件头扫描,是 O(文件长度)。对每条发现、每个函数都调用它,就会变成 O(n²):zperf-lint v0.3 的规则阶段曾是解析阶段的 4 倍。正确做法是解析后一次扫描,建一张行首偏移表,之后二分查找。再加上每文件 arena `reset(.retain_capacity)`(§2.2)和 `Io.Group` 并行、各写各槽(§6.2),在 Zig 0.17 std(563 个文件)上从 527 ms 降到 41 ms,峰值 RSS 从 64 MiB 降到 17 MiB,输出逐字节不变。详见 `zperf-lint/README.md` 的“自举 / dogfooding”。 **局限**:它是语法级检查,没有类型信息——结构体大小是按字段类型估算的,"是否是整数除法"无法判断,"所有权是否转移"靠 token 启发式。每条规则的误报风险都写在 README 里;把它当成 code review 的提示,而不是编译器错误。 diff --git a/packages/zperf/docs/zig-performance-guide.md b/packages/zperf/docs/zig-performance-guide.md index 56f2f14..ce787c0 100644 --- a/packages/zperf/docs/zig-performance-guide.md +++ b/packages/zperf/docs/zig-performance-guide.md @@ -3,7 +3,7 @@ Less memory, more throughput. > **Target: Zig 0.17.0.** Examples under `examples/` are checked with Zig 0.17.0 on x86_64 Linux (`zig test` Debug and `-O fast`, or `zig build-exe`). Reproduce with `cd examples && zig build test`. -> Numbers come from release notes, original PRs, or are labeled as local measurements. Nothing is invented. +> Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings. Numbers come from release notes, original PRs, or are labeled as local measurements. Nothing is invented. > > Companion: `zperf-lint/` — static checks for performance and memory anti-patterns (Chapter 9). @@ -59,12 +59,12 @@ Sources: [0.14.0 release notes](https://ziglang.org/download/0.14.0/release-note TigerBeetle's TigerStyle order: -1. **Estimate at design time.** Bandwidth and latency for network, disk, memory, and CPU. The largest reported gains (up to 1000×) come from design changes, before profiling is possible. -2. **Fix the slowest resource first** (network > disk > memory > CPU), weighted by frequency. A cache miss repeated often enough can cost as much as an `fsync`. -3. **Batch.** Amortize network, disk, memory, and CPU across batches; give the CPU large, predictable units of work. -4. **Make hot work explicit.** e.g. a hot loop as a standalone function with raw parameters instead of `self` (see §5.6). +1. Estimate at design time. Bandwidth and latency for network, disk, memory, and CPU. The largest reported gains (up to 1000×) come from design changes, before profiling is possible. +2. Fix the slowest resource first (network > disk > memory > CPU), weighted by frequency. A cache miss repeated often enough can cost as much as an `fsync`. +3. Batch. Amortize network, disk, memory, and CPU across batches; give the CPU large, predictable units of work. +4. Make hot work explicit. e.g. a hot loop as a standalone function with raw parameters instead of `self` (see §5.6). -Andrew Kelley's *Practical Data-Oriented Design*: **less memory can mean faster code** — fewer allocations and better cache use. This guide starts with memory, then computation. +Andrew Kelley's *Practical Data-Oriented Design*: less memory can mean faster code — fewer allocations and better cache use. This guide starts with memory, then computation. Sources: [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) · [Practical Data-Oriented Design (Handmade Seattle 2021)](https://handmade-seattle.com/media-sub/practical-data-oriented-design) · [Zig PR #7920](https://github.com/ziglang/zig/pull/7920) @@ -356,7 +356,7 @@ test "buffer pool tryAlloc / release" { ### 2.5 Static allocation (TigerStyle) -TigerBeetle's rule: **allocate everything at startup; after init, do not allocate or free.** That removes allocation latency jitter and use-after-free from the steady state. Fix limits for queues, loops, and connection counts. +TigerBeetle's rule: allocate everything at startup; after init, do not allocate or free. That removes allocation latency jitter and use-after-free from the steady state. Fix limits for queues, loops, and connection counts. > Example: `examples/m11_static.zig` (checked with Zig 0.17.0 using `zig test`). @@ -484,7 +484,7 @@ test "phase allocator: alloc only in init" { #### Size the object / message pool from concurrency -Static allocation only works if the pool is **large enough to always make progress**. TigerBeetle's `MessagePool` sizes from worst-case concurrent holders: journal read/write IOPS, pipeline depth, connection send queues, grid repair reads, plus a small burst (+1) so a parse or ping cannot deadlock the replica. For any fixed-size frame pool: +Static allocation only works if the pool is large enough to always make progress. TigerBeetle's `MessagePool` sizes from worst-case concurrent holders: journal read/write IOPS, pipeline depth, connection send queues, grid repair reads, plus a small burst (+1) so a parse or ping cannot deadlock the replica. For any fixed-size frame pool: 1. List every concurrent holder (connections × in-flight, disk IOPS, pipelines, loops). 2. Add one for “holding a message while allocating the next”. @@ -1179,9 +1179,9 @@ test "delta + varint" { ### 2.16 Fragmentation, huge pages, and other notes -- **Fragmentation:** Mixing many small objects with different sizes and lifetimes in one general-purpose heap can fragment memory. Group allocations by lifetime (arena), by size (`MemoryPool` / size-class pools), or use static allocation. -- **Huge pages:** TLB misses can limit large, randomly accessed data such as hash tables and arrays. On Linux, `madvise(MADV_HUGEPAGE)` can request transparent huge pages for an `mmap` region. Measure the result with `perf stat -e dTLB-load-misses`. -- **ArrayList growth:** Since 0.14, the Allocator has `remap`, and `page_allocator` supports `mremap`. Large ArrayList growth may therefore remap pages instead of copying them. +- Fragmentation: Mixing many small objects with different sizes and lifetimes in one general-purpose heap can fragment memory. Group allocations by lifetime (arena), by size (`MemoryPool` / size-class pools), or use static allocation. +- Huge pages: TLB misses can limit large, randomly accessed data such as hash tables and arrays. On Linux, `madvise(MADV_HUGEPAGE)` can request transparent huge pages for an `mmap` region. Measure the result with `perf stat -e dTLB-load-misses`. +- ArrayList growth: Since 0.14, the Allocator has `remap`, and `page_allocator` supports `mremap`. Large ArrayList growth may therefore remap pages instead of copying them. - **`Deque` + `remap`:** `std.Deque` grows with `Allocator.remap` when the ring can stay in place; only then splits head/tail for a moving grow (`lib/std/deque.zig`). Prefer remap-friendly allocators behind large rings. ### 2.17 Relocatable slabs and idle physical reclaim (advanced) @@ -1536,8 +1536,8 @@ Notes: ### 5.1 `comptime`: move work to compile time -- **Lookup tables:** Build them in a `comptime` block so they live in read-only data and require no runtime initialization. For large loops, raise the compile-time branch quota with `@setEvalBranchQuota`. -- **Generic specialization:** A `comptime N: usize` parameter generates specialized code for each N. `inline for` unrolls the loop at compile time. +- Lookup tables: Build them in a `comptime` block so they live in read-only data and require no runtime initialization. For large loops, raise the compile-time branch quota with `@setEvalBranchQuota`. +- Generic specialization: A `comptime N: usize` parameter generates specialized code for each N. `inline for` unrolls the loop at compile time. - Assert relationships between constants at compile time with `comptime std.debug.assert(...)`, as TigerStyle recommends. > Example: `examples/p01_comptime_lut.zig` (checked with Zig 0.17.0 using `zig test`). @@ -1813,7 +1813,7 @@ Sources: [0.14 `@branchHint`](https://ziglang.org/download/0.14.0/release-notes. - **Use multiple accumulators or unroll loops** when measurements show a dependency chain is limiting throughput. `inline for` can create a fixed number of independent accumulators. Unrolling increases code size and instruction-cache pressure. - **`inline fn` is a language guarantee, not a hint.** The language reference calls it “semantic inlining”; compile-time-known arguments are compile-time-known inside the function. It can increase binary size and compile time, and can even hurt runtime performance. Usually let the compiler decide; use it when compile-time propagation or a specific stack-frame shape is required. - Pass large structs as `*const T` when they exceed 16 bytes and copying is not intended (TigerStyle). “Push ifs up, fors down”: move branches outside loops and move loops into batch-processing functions. -- **Initialize large structs in place:** `fn init(self: *Big) void { self.* = .{...}; }` avoids constructing a temporary on the stack and copying it (TigerStyle). +- Initialize large structs in place: `fn init(self: *Big) void { self.* = .{...}; }` avoids constructing a temporary on the stack and copying it (TigerStyle). > Example: `examples/p09_hot_loop.zig` (checked with Zig 0.17.0 using `zig test`). @@ -2196,7 +2196,7 @@ pub fn build(b: *std.Build) void { ### 7.3 PGO -Zig does **not** support PGO for user programs as of this writing. [#15137](https://github.com/ziglang/zig/issues/15137) is “not planned”; research [#237](https://github.com/ziglang/zig/issues/237) on test-driven PGO is still open. Fallbacks: branch hints, hot/cold separation, data layout. +Zig does not support PGO for user programs as of this writing. [#15137](https://github.com/ziglang/zig/issues/15137) is “not planned”; research [#237](https://github.com/ziglang/zig/issues/237) on test-driven PGO is still open. Fallbacks: branch hints, hot/cold separation, data layout. Sources: `zig build-exe --help` (0.17.0) · [0.17 rename of lang.OptimizeMode to lang.Optimize](https://ziglang.org/download/0.17.0/release-notes.html) · [0.16 Lazy Field Analysis and loop safety-check code generation](https://ziglang.org/download/0.16.0/release-notes.html) · `lib/std/Build/Step/Compile.zig` @@ -2210,10 +2210,10 @@ Useful when building tools that look like compilers (parsers, LSs, bundlers). Fu | SoA + `extra` + `string_bytes` | `lib/std/zig/Zir.zig` | Dense IR; rare ops out of line (§2.8) | | Sharded intern + tid indices | `src/InternPool.zig` | Concurrent intern without a global lock (§2.10) | | Labeled-switch tokenizer | `lib/std/zig/tokenizer.zig` | Same as §5.4; keywords via `StaticStringMap` | -| Optional tracy hooks | `src/tracy.zig` | Instrument self-host builds when chasing compile time (**U**) | +| Optional tracy hooks | `src/tracy.zig` | Instrument self-host builds when chasing compile time (U) | | Content-addressed build cache | `lib/std/Build/Cache.zig` | SipHash manifests for incremental artifacts (§8.4 adjacent) | -Do not cargo-cult compiler internals into small apps. Steal the **shape** (indices, SoA, shard, ensure heuristics) after measuring. +Do not copy compiler internals into small apps. Steal the shape (indices, SoA, shard, growth heuristics) after measuring. --- @@ -2221,9 +2221,9 @@ Do not cargo-cult compiler internals into small apps. Steal the **shape** (indic ### 8.1 Microbenchmarks -Since 0.16, `std.time.Timer` is gone. Time through `Io`: `std.Io.Clock.Timestamp.now(io, .awake)` and `.untilNow(io)`. Use `std.mem.doNotOptimizeAway` so the compiler cannot delete the work. Run several trials; keep the **best** (or report a distribution) to cut interrupt and cold-cache noise. +Since 0.16, `std.time.Timer` is gone. Time through `Io`: `std.Io.Clock.Timestamp.now(io, .awake)` and `.untilNow(io)`. Use `std.mem.doNotOptimizeAway` so the compiler cannot delete the work. Run several trials; keep the best (or report a distribution) to cut interrupt and cold-cache noise. -Record Zig version, optimize mode, OS, and CPU model so another machine can compare runs. The example prints one **JSONL** line per result. +Record Zig version, optimize mode, OS, and CPU model so another machine can compare runs. The example prints one JSONL line per result. > Example: `examples/p12_bench.zig` (checked with Zig 0.17.0 using `zig run -O fast`). @@ -2439,7 +2439,7 @@ They overlap little: zperf-lint is performance-oriented; zbc and Zwanzig cover r | Read a large file | `File.createMemoryMap` + zero-copy slices | | Growable ring | `Deque` (uses `Allocator.remap` when possible) | -**Compute faster** +Compute faster | Goal | Use | |---|---| diff --git a/packages/zperf/evidence/zh-CN/README.md b/packages/zperf/evidence/zh-CN/README.md new file mode 100644 index 0000000..7f9003e --- /dev/null +++ b/packages/zperf/evidence/zh-CN/README.md @@ -0,0 +1,26 @@ +# zperf 证据 + +`zperf-lint` 规则的墙钟门前/门后。政策:[../docs/evidence-policy.md](../../docs/zh-CN/evidence-policy.md)。 + +## 当前 (md + jsonl) + +[zp-alloc-hot](zp-alloc-hot.md), [zp-append-without-reserve](zp-append-without-reserve.md), [zp-page-allocator-small](zp-page-allocator-small.md), [zp-print-hot](zp-print-hot.md), [zp-unbuffered-io](zp-unbuffered-io.md), [zp-aos-single-field](zp-aos-single-field.md), [zp-bool-array](zp-bool-array.md), [zp-static-string-map](zp-static-string-map.md), [zp-runtime-div-hot](zp-runtime-div-hot.md), [zp-queue-ordered-remove](zp-queue-ordered-remove.md), [zp-shift-on-read](zp-shift-on-read.md), **[zp-per-packet-alloc](zp-per-packet-alloc.md)**(共享 zp-alloc-hot 数据),[zp-invariant-float-div](zp-invariant-float-div.md), [zp-map-put-without-ensure](zp-map-put-without-ensure.md), [zp-arena-reset-free-all](zp-arena-reset-free-all.md), [zp-create-destroy-hot](zp-create-destroy-hot.md), [zp-fixed-size-alloc-hot](zp-fixed-size-alloc-hot.md) + +## 待处理措施(仅限 md 占位符 — 无 jsonl) + +- **[zp-broad-runtime-safety](zp-broad-runtime-safety.md)** — ReleaseSafe 范围内关闭安全性 +- **[zp-packable-bools](zp-packable-bools.md) / [zp-usize-index-field](zp-usize-index-field.md) / [zp-optional-scalar-field](zp-optional-scalar-field.md)** — 在此传递中利用布局密度而不使用已提交的带宽 + +## 不适用(仅限静态 / 正确性 / 静态大小)——仅 MD 存根 + +- **[zp-debug-allocator](zp-debug-allocator.md)** 构建模式下的 GPA 选择 +- **[zp-missing-free](zp-missing-free.md)** 缺失的释放 +- **[zp-catch-unreachable-alloc](zp-catch-unreachable-alloc.md)** 捕获不可达的分配 +- **[zp-oversized-enum-tag](zp-oversized-enum-tag.md)** 超大的枚举标签(大小/ABI) +- **[zp-catch-unreachable-io](zp-catch-unreachable-io.md)** 捕获不可达的 I/O + +## 不确定(仅医学存根 — 无 jsonl) + +- **[zp-large-struct-by-value](zp-large-struct-by-value.md)** large-struct-by-value — ReleaseFast 省略了微基准测试;不要伪造 JSONL + +测试工具:`../examples/bench/evidence_fill.zig` · 重新生成:`../scripts/regen-evidence.sh` diff --git a/packages/zperf/evidence/zh-CN/zp-alloc-hot.md b/packages/zperf/evidence/zh-CN/zp-alloc-hot.md new file mode 100644 index 0000000..08209da --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-alloc-hot.md @@ -0,0 +1,37 @@ +# zp-alloc-hot — 循环中分配 + +## 主张 + +在这台 Linux 机器上,使用 Zig **0.17.0** `-Ofast`,每次迭代执行 GPA alloc+free 与 ArenaAllocator.reset(.retain_capacity) 显示了明显的前后实际时间差距。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-alloc-hot` 情况) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 时间测量:`Io.Clock` 唤醒;热身=2;**9 轮的中位数** +- **之前:** `gpa_alloc_free_in_loop` +- **之后:** `arena_retain_in_loop` +- 操作次数=100000 +- 原始数据:[`zp-alloc-hot.jsonl`](../zp-alloc-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~2.012 ms | +| Median after | ~1.422 ms | +| **前后比例** | **~1.41×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-alloc-hot` / `alloc-in-loop`。`rules.zig` 中的默认严重性是 **关闭**(CLI 不会报告该规则);有关保留/升级,请参阅 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上 **一致**。 + +## 结论 + +同意 ~1.41× 但在 smp_allocator 上表现温和;FBA/池(zp-fixed-size-alloc-hot)通常更大。方向明确。 + +## 限制 + +具有微小固定大小的高频路径可能更适合使用 MemoryPool/FBA;保持 arena 是一个有效的修复方法。 diff --git a/packages/zperf/evidence/zh-CN/zp-alloc-print-hot.md b/packages/zperf/evidence/zh-CN/zp-alloc-print-hot.md new file mode 100644 index 0000000..ba671d6 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-alloc-print-hot.md @@ -0,0 +1,27 @@ +# zp-alloc-print-hot — 已测量,仍然提示 + +## 状态 +**提示**. 实时时钟 JSONL: [zp-alloc-print-hot.jsonl](../zp-alloc-print-hot.jsonl)。未进行语料库分流。2026-10-22 的语料库裁定不涵盖此段。 + +## 测量 +对一个运行时 `u64` 使用 `std.fmt.allocPrint` 20000 次,与格式化一次并重复使用切片相比。ReleaseFast,9 次的中位数。格式化的字节被混入到累加器中,因此循环结束后不会被删除。循环前后产生相同的累加器。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 之后 | `alloc_print_once` | 0.020375 | 20000 | 1 | +| 之前 | `alloc_print_each_iter` | 1.016875 | 20000 | 49.908 | +| 之后 | `arena_print_once` | 0.020333 | 20000 | 1 | +| 之前 | `arena_print_each_iter` | 0.235416 | 20000 | 11.578 | +| 之后 | `const_print_once` | 0.025834 | 20000 | 1 | +| 之前 | `const_print_each_iter` | 0.362958 | 20000 | 14.050 | + +GPA 行是每次迭代的 `std.fmt.allocPrint` 加上 `free`,与一个 `allocPrint` 对比。arena 行在每次计时调用时通过 `retain_capacity` 重置,然后在循环内进行 `allocPrint` 而不释放。constant 行是 `Allocator.print("hello", .{})`。 + +同一二进制的另外两次运行保持在同一范围:GPA 50.795×和54.930×,arena 11.799×和11.576×,常量字符串 14.128×和15.445×。 + +`std.fmt.bufPrint` / `std.mem.print` 对相同整数的速度大约是 4.8× 到 5.3×,处于平均水平,因此这个规则不会标记缓冲区格式化。对一个短的 `{s}:{d}` 对的 `bufPrint` 在不同运行中变化在 5.9× 到 9.7× 之间,所以它也没有被包含在内。 + +每次迭代都会变化的值无法被提升。检查器仅在格式字符串和每个参数都是字面量或标识符,并且此循环没有赋值、捕获或声明时才报告调用。默认保持 **hint**。这个比率不是一般性的加速。 + +## 解除阻塞 +不要仅仅从这个文件中将默认设置为 `error`。一个变化的参数、一个冷路径或另一个分配器都可能抹掉差异。在进行任何默认更改之前,仍需要对语料进行初步筛选。 diff --git a/packages/zperf/evidence/zh-CN/zp-aos-single-field.md b/packages/zperf/evidence/zh-CN/zp-aos-single-field.md new file mode 100644 index 0000000..ce8b655 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-aos-single-field.md @@ -0,0 +1,37 @@ +# zp-aos-single-field — aos-single-field + +## 主张 + +在这台 Linux 机器上,AoS sum 一个字段与并行 []u32 SoA 在 Zig **0.17.0** `-Ofast` 下显示出明显的前后实时时钟差距。 + +## 如何测量 + +- 测试套件:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-aos-single-field` 的用例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 激活;预热=2;**9 轮的中位数** +- **之前:** `aos_sum_hp` +- **之后:** `soa_sum_hp` +- 操作次数=4000000 +- 原始数据:[ `zp-aos-single-field.jsonl`](../zp-aos-single-field.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~4.801 ms | +| Median after | ~0.731 ms | +| **前/后比例** | **~6.57×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-aos-single-field` / `aos-single-field`。`rules.zig` 中的默认严重性为 **关闭**(CLI 不会报告该规则);有关保留/升级的信息,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。存在证据,并且在方向上 **一致**。 + +## 结论 + +当只有一个场热点(脂肪元素)时,同意约6.57×。 + +## 限制 + +其他循环可能会访问所有字段——lint 是一种审查提示。 diff --git a/packages/zperf/evidence/zh-CN/zp-append-without-reserve.md b/packages/zperf/evidence/zh-CN/zp-append-without-reserve.md new file mode 100644 index 0000000..766117f --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-append-without-reserve.md @@ -0,0 +1,37 @@ +# zp-append-without-reserve — 不保留地追加 + +## 主张 + +arrayList. appind in round vs sureal Total Capacity + 附加能力在 Zig 下 **0.17.0** `-Ofast'在Linux 机器上显示一个清晰的壁点后缺口. + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-append-without-reserve` 的案例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `append_no_reserve` +- **之后:** `ensure_then_assume` +- 操作数=50000 +- 原始数据:[ `zp-append-without-reserve.jsonl`](../zp-append-without-reserve.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~0.1308 ms | +| Median after | ~0.06291 ms | +| **前后比例** | **~2.08×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-append-without-reserve` / `append-without-reserve`。在 `rules.zig` 中的默认严重性为 **关闭**(CLI 不会报告此规则);有关保留/升级的详情,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。已有证据并且方向上 **一致**。 + +## 结论 + +同意 ~2.08× 对于 N=50k 从空开始的增长。 + +## 限制 + +当 N 未知时,保留可能浪费;测试/竞技场路径被 lint 跳过。 diff --git a/packages/zperf/evidence/zh-CN/zp-arena-reset-free-all.md b/packages/zperf/evidence/zh-CN/zp-arena-reset-free-all.md new file mode 100644 index 0000000..2317bab --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-arena-reset-free-all.md @@ -0,0 +1,37 @@ +# zp-arena-reset-free-all — 竞技场-重置-全部-免费 + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** 使用 `-Ofast` 时,ArenaAllocator.reset(.free_all) 每次迭代与 .retain_capacity 的对比显示了明显的前后实际时间差距。 + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-arena-reset-free-all` 用例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `arena_reset_free_all` +- **之后:** `arena_reset_retain` +- 操作次数=50000 +- 原始数据:[`zp-arena-reset-free-all.jsonl`](../zp-arena-reset-free-all.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~2.252 ms | +| Median after | ~0.7083 ms | +| **前/后比例** | **~3.18×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-arena-reset-free-all` / `arena-reset-free-all`。`rules.zig` 中的默认严重性是 **警告**;有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且方向上**一致**。 + +## 结论 + +同意 ~3.18× — free_all 每次都会返回页面。 + +## 限制 + +当 RSS 必须释放时使用 free_all;保留以便快速重用。 diff --git a/packages/zperf/evidence/zh-CN/zp-bool-array.md b/packages/zperf/evidence/zh-CN/zp-bool-array.md new file mode 100644 index 0000000..19be89b --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-bool-array.md @@ -0,0 +1,37 @@ +# zp-bool-array — 布尔数组 + +## 主张 + +在这台 Linux 机器上,[]bool count 与 bit_set.Static.count 在 Zig **0.17.0** 下使用 `-Ofast` 显示出明显的前后墙钟时间差。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-bool-array` 的案例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `bool_array_count` +- **之后:** `bitset_count` +- 操作数=524288 +- 原始数据:[ `zp-bool-array.jsonl`](../zp-bool-array.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~0.08908 ms | +| Median after | ~0.002937 ms | +| **前/后比例** | **~30.3×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-bool-array` / `bool-array`。`rules.zig` 中的默认严重性是 **关闭**(CLI 不会报告该规则);有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且**方向上**一致。 + +## 结论 + +同意 ~30.3× 的 popcount 风格遍历(并且密度高 8 倍)。 + +## 限制 + +每个元素的可寻址性 / 并发位写入可能需要 []bool。 diff --git a/packages/zperf/evidence/zh-CN/zp-broad-runtime-safety.md b/packages/zperf/evidence/zh-CN/zp-broad-runtime-safety.md new file mode 100644 index 0000000..789c6dd --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-broad-runtime-safety.md @@ -0,0 +1,17 @@ +# zp-broad-runtime-safety — 待定措施 + +## 状态 + +**待处理措施** — 尚无 `evidence/zp-broad-runtime-safety.jsonl`。 + +## 原因 + +Broad `@setRuntimeSafety(false)` 只有在 **ReleaseSafe** 下(安全检查仍在运行)才有一个有意义的实时时钟故事。本次传递的测试平台是 `-Ofast` / ReleaseFast 形状的,并未提交 ReleaseSafe 配对。 + +## 为什么没有 JSONL + +错误的优化模式会低估或虚构一个胜利。 + +## 解阻条件 + +在 `evidence_fill.zig`(或同级)中专用 ReleaseSafe 前/后,median≥9,然后添加 `.jsonl` 并更新此 md。在此之前保持警告/审查。 diff --git a/packages/zperf/evidence/zh-CN/zp-catch-unreachable-alloc.md b/packages/zperf/evidence/zh-CN/zp-catch-unreachable-alloc.md new file mode 100644 index 0000000..4990f99 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-catch-unreachable-alloc.md @@ -0,0 +1,17 @@ +# zp-catch-unreachable-alloc — 不适用(正确性) + +## 状态 + +**不适用(正确性)** — 没有 `evidence/zp-catch-unreachable-alloc.jsonl`。不要编造一个。 + +## 原因 + +`catch unreachable` 在分配时是 **OOM 下的未定义行为**,而不是吞吐量优化。 + +## 为什么没有 JSONL + +正确性/安全性,而不是速度。 + +## 解阻条件 + +对于速度门没有。该规则当前**已关闭**,因此 CLI 不会报告它。 diff --git a/packages/zperf/evidence/zh-CN/zp-catch-unreachable-io.md b/packages/zperf/evidence/zh-CN/zp-catch-unreachable-io.md new file mode 100644 index 0000000..be468e5 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-catch-unreachable-io.md @@ -0,0 +1,17 @@ +# zp-catch-unreachable-io — 不适用(正确性) + +## 状态 + +**不适用(正确性)** — 没有 `evidence/zp-catch-unreachable-io.jsonl`。不要编造一个。 + +## 原因 + +`catch unreachable` 在 I/O 上是 **未定义行为 / 出错崩溃**,而不是速度上的优势。 + +## 为什么没有 JSONL + +正确性 / 错误处理,而不是挂钟时间。 + +## 解阻条件 + +对于速度门没有。该规则当前**已关闭**,因此 CLI 不会报告它。 diff --git a/packages/zperf/evidence/zh-CN/zp-clock-hot.md b/packages/zperf/evidence/zh-CN/zp-clock-hot.md new file mode 100644 index 0000000..4710d76 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-clock-hot.md @@ -0,0 +1,17 @@ +# zp-clock-hot — 测量过的,仍有暗示 + +## 状态 +**提示**。实时时钟 JSONL:[zp-clock-hot.jsonl](../zp-clock-hot.jsonl)。未进行语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +`Io.Clock.awake.now` 每次迭代与一个样本。早期的一次运行丢弃了结果,因此未被保存。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `sample_once` | 0.051 | 100000 | 1 | +| 之前 | `sample_each_iter` | 2.128 | 100000 | 41.615 | + +每次迭代都会变化的路径无法被提升。需要新时间戳的循环没有可以提升的东西。默认保持 **hint**。这个比例并不是通用的加速。 + +## 解锁 +不要仅凭此文件将默认值设置为 `error`。变化的输入、冷路径或其他 ABI 都可能抹去差异。在进行任何默认更改之前,仍需进行语料库分流。 diff --git a/packages/zperf/evidence/zh-CN/zp-create-destroy-hot.md b/packages/zperf/evidence/zh-CN/zp-create-destroy-hot.md new file mode 100644 index 0000000..5e7df9c --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-create-destroy-hot.md @@ -0,0 +1,37 @@ +# zp-create-destroy-hot — 循环中创建和销毁 + +## 主张 + +在这台 Linux 机器上,Allocator.create/destroy(Node) 与 Zig **0.17.0** 下的 MemoryPool `-Ofast` 对比显示出明显的前后实时时钟差距。 + +## 如何测量 + +- 测试套件:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-create-destroy-hot` 的案例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;warm=2;**9 轮的中位数** +- **之前:** `create_destroy_loop` +- **之后:** `memory_pool` +- 操作次数=200000 +- 原始数据:[`zp-create-destroy-hot.jsonl`](../zp-create-destroy-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~4.026 ms | +| Median after | ~0.375 ms | +| **前/后比例** | **~10.7×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-create-destroy-hot` / `create-destroy-in-loop`。`rules.zig` 中的默认严重性是 **信息**;有关保留/升级的详情,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上**一致**。 + +## 结论 + +同意 ~10.7× 的固定大小节点更替率。 + +## 限制 + +可变大小的对象需要其他池。 diff --git a/packages/zperf/evidence/zh-CN/zp-debug-allocator.md b/packages/zperf/evidence/zh-CN/zp-debug-allocator.md new file mode 100644 index 0000000..8c7b3bf --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-debug-allocator.md @@ -0,0 +1,17 @@ +# zp-debug-allocator — 不适用(仅限静态) + +## 状态 + +**不适用(仅限静态)** — 没有 `evidence/zp-debug-allocator.jsonl`。不要自行编造。 + +## 原因 + +规则标记 GPA / arena / FBA 选择,这取决于 **构建模式**(调试 vs 发布)。不存在单一二进制的前/后实际时间对比:‘修复’是在编译时选择不同的分配器,而不是在一个可执行文件内部进行热路径重写。 + +## 为什么没有 JSONL + +实时时钟证据假设相同的优化模式和可比的操作。模式切换异味是审查提示,而不是速度限制。 + +## 解阻条件 + +速度门没有。保持为静态审核;如有需要,在指南中记录每种模式下首选的分配器。 diff --git a/packages/zperf/evidence/zh-CN/zp-fixed-size-alloc-hot.md b/packages/zperf/evidence/zh-CN/zp-fixed-size-alloc-hot.md new file mode 100644 index 0000000..bdf841e --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-fixed-size-alloc-hot.md @@ -0,0 +1,37 @@ +# zp-固定大小分配-热点 — 循环中的固定大小分配 + +## 主张 + +在这台 Linux 机器上,GPA 分配+释放 256B 与预热缓冲池命中在 Zig **0.17.0** `-Ofast` 下显示出明显的前后实际时间差距。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-fixed-size-alloc-hot` 用例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `fixed_alloc_free_loop` +- **之后:** `buffer_pool_hit` +- 操作次数=100000 +- 原始数据:[ `zp-fixed-size-alloc-hot.jsonl`](../zp-fixed-size-alloc-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~2.001 ms | +| Median after | ~0.236 ms | +| **前/后比例** | **~8.48×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-fixed-size-alloc-hot` / `fixed-size-alloc-in-loop`。`rules.zig` 中的默认严重性为 **信息**;有关保留/升级,请参阅 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上**一致**。 + +## 结论 + +同意 ~8.5×(与 p20 池相比 GPA 一致)。 + +## 限制 + +池缺仍会影响GPA;规模类别的池很重要。 diff --git a/packages/zperf/evidence/zh-CN/zp-invariant-float-div.md b/packages/zperf/evidence/zh-CN/zp-invariant-float-div.md new file mode 100644 index 0000000..35477ef --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-invariant-float-div.md @@ -0,0 +1,37 @@ +# zp-不变浮点除法 — 不变浮点除法 + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** `-Ofast` 下,浮点数 / 循环不变量与乘以倒数的对比显示了明显的前后实际时间差异。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-invariant-float-div` 情况) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `float_div_loop` +- **之后:** `mul_reciprocal` +- 操作次数=2000000 +- 原始数据:[ `zp-invariant-float-div.jsonl`](../zp-invariant-float-div.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~2.504 ms | +| Median after | ~1.25 ms | +| **前/后比例** | **~2×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-invariant-float-div` / `invariant-float-div`。`rules.zig` 中的默认严重性是 **提示**;有关保留/升级,请参阅 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上**一致**。 + +## 结论 + +同意 ~2.00× 不使用快速数学(ReleaseFast 不会启用它)。 + +## 限制 + +舍入略有不同;请验证数值。 diff --git a/packages/zperf/evidence/zh-CN/zp-json-hot.md b/packages/zperf/evidence/zh-CN/zp-json-hot.md new file mode 100644 index 0000000..057d7fd --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-json-hot.md @@ -0,0 +1,17 @@ +# zp-json-hot — 稳定,仍有暗示 + +## 状态 +**提示**。实时时钟 JSONL:[zp-json-hot.jsonl](../zp-json-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +对一个小对象进行 `json.parseFromSlice`,2000 次,与解析一次并读取字段相比。ReleaseFast。结果被保留,因此循环后不会被删除。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `parse_once` | 0.003 | 2000 | 1 | +| 之前 | `parse_each_iter` | 7.043 | 2000 | 2683.175 | + +每次迭代都会改变的有效载荷不能被提升。默认保持 **提示**。这一比率不是一般性的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/zperf/evidence/zh-CN/zp-large-struct-by-value.md b/packages/zperf/evidence/zh-CN/zp-large-struct-by-value.md new file mode 100644 index 0000000..9e3802d --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-large-struct-by-value.md @@ -0,0 +1,17 @@ +# zp-large-struct-by-value — 不确定 + +## 状态 + +**不确定** — 没有 `evidence/zp-large-struct-by-value.jsonl`。不要编造一个。 + +## 原因 + +在 Zig **0.17.0** 下,`-Ofast` / ReleaseFast 的大结构按值传递与指针微基准测试被**省略**了(总计约几十纳秒)。这些数字不能代表实际情况;引用它们将是不诚实的。 + +## 为什么没有 JSONL + +优化器删除了工作;比率是噪音。 + +## 解阻条件 + +一个在我们关心的优化模式下迫使可观察副本(不透明 / `volatile` / 黑箱汇,或优化器无法消除的真实调用边界)的工具。在此之前,该规则保持**关闭**。 diff --git a/packages/zperf/evidence/zh-CN/zp-map-put-without-ensure.md b/packages/zperf/evidence/zh-CN/zp-map-put-without-ensure.md new file mode 100644 index 0000000..d97915d --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-map-put-without-ensure.md @@ -0,0 +1,37 @@ +# zp-map-put-without-ensure — map-put-without-ensure + +## 主张 + +在这台 Linux 机器上,AutoHashMap.put 从空增长与在 Zig **0.17.0** `-Ofast` 下先 ensureTotalCapacity 再 put 显示了明显的前后墙钟时间差。 + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-map-put-without-ensure` 的案例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `map_put_grow` +- **之后:** `map_ensure_then_put` +- 操作次数=20000 +- 原始数据:[`zp-map-put-without-ensure.jsonl`](../zp-map-put-without-ensure.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~0.8296 ms | +| Median after | ~0.3001 ms | +| **前/后比例** | **~2.76×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-map-put-without-ensure` / `map-put-without-ensure`。在 `rules.zig` 中的默认严重性为 **关闭**(CLI 不会报告此规则);有关保留/升级,请参阅 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且方向上 **一致**。 + +## 结论 + +同意 ~2.76× 用于 N=20k 插入。 + +## 限制 + +未知最终大小:在批处理中确保未使用的容量。 diff --git a/packages/zperf/evidence/zh-CN/zp-missing-free.md b/packages/zperf/evidence/zh-CN/zp-missing-free.md new file mode 100644 index 0000000..e99678b --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-missing-free.md @@ -0,0 +1,17 @@ +# zp-missing-free — 不适用(正确性) + +## 状态 + +**不适用(正确性)** — 没有 `evidence/zp-missing-free.jsonl`。不要编造一个。 + +## 原因 + +缺少 `free` / `deinit` 是一种 **泄漏 / 未定义行为** 气味。收益是正确性和资源卫生,而不是可复现的实际时间改进。 + +## 为什么没有 JSONL + +规则的价值不取决于前后的速度,而取决于泄漏检测。 + +## 解阻条件 + +对于速度门没有。该规则当前**已关闭**,因此 CLI 不会报告它。 diff --git a/packages/zperf/evidence/zh-CN/zp-open-file-hot.md b/packages/zperf/evidence/zh-CN/zp-open-file-hot.md new file mode 100644 index 0000000..b022004 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-open-file-hot.md @@ -0,0 +1,17 @@ +# zp-open-file-hot — 已测量,仍有提示 + +## 状态 +**提示**。实时时钟 JSONL:[zp-open-file-hot.jsonl](../zp-open-file-hot.jsonl)。未经过语料筛选。2026-10-22 的语料判定不涵盖此标识符。 + +## 测量 +对一个单字节文件进行 `Io.Dir.openFileAbsolute`,重复 2000 次,与仅打开一次进行比较。结果被保留,以便循环结束后不会被删除。 + +| 角色 | 情况 | 中位数毫秒 | 操作次数 | 比率 | +| --- | --- | ---: | ---: | ---: | +| 在...之后 | `open_once` | 0.006 | 2000 | 1 | +| 之前 | `open_each_iter` | 13.072 | 2000 | 2134.204 | + +每次迭代都会变化的路径无法被提升。需要新时间戳的循环没有可以提升的东西。默认保持 **hint**。这个比例并不是通用的加速。 + +## 解锁 +不要仅仅从此文件中将默认设置为 `error`。变化的输入、冷路径或其他 ABI 可能会抹去差异。在任何默认更改之前,仍然需要进行语料库筛选。 diff --git a/packages/zperf/evidence/zh-CN/zp-optional-scalar-field.md b/packages/zperf/evidence/zh-CN/zp-optional-scalar-field.md new file mode 100644 index 0000000..0d37059 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-optional-scalar-field.md @@ -0,0 +1,17 @@ +# zp-可选标量字段 — 待定措施 + +## 状态 + +**待处理措施** — 尚无 `evidence/zp-optional-scalar-field.jsonl`。 + +## 原因 + +可选的标量填充/布局空洞是一个**密度**声明。没有带宽可以利用此通道。 + +## 为什么没有 JSONL + +与 zp-packable-bools/zp-usize-index-field 属于同一类——需要防止内存流量问题,而不是一个小循环。 + +## 解阻条件 + +有可测缓存效果的紧凑与填充结构数组扫描;然后是 JSONL。在此之前,该规则保持 **关闭**。 diff --git a/packages/zperf/evidence/zh-CN/zp-oversized-enum-tag.md b/packages/zperf/evidence/zh-CN/zp-oversized-enum-tag.md new file mode 100644 index 0000000..33081db --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-oversized-enum-tag.md @@ -0,0 +1,17 @@ +# zp-oversized-enum-tag — 不适用(静态大小) + +## 状态 + +**不适用(固定大小)** — 没有 `evidence/zp-oversized-enum-tag.jsonl`。不要自行编造。 + +## 原因 + +超大枚举标签是一个 **ABI / 大小** 问题的迹象(`@sizeOf`)。实时时钟是错误的门。 + +## 为什么没有 JSONL + +静态大小比较是证据;微基准测试就是表演。 + +## 解阻条件 + +可选:如果对教学有用,可在 md 中在 `@sizeOf` 前/后记录 — JSONL 速度限制仍然没有。规则保持**关闭**。 diff --git a/packages/zperf/evidence/zh-CN/zp-packable-bools.md b/packages/zperf/evidence/zh-CN/zp-packable-bools.md new file mode 100644 index 0000000..84031c8 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-packable-bools.md @@ -0,0 +1,17 @@ +# zp-packable-bools — 待定措施 + +## 状态 + +**待处理措施** — 尚无 `evidence/zp-packable-bools.jsonl`。 + +## 原因 + +可打包的布尔值/位打包是一种**布局密度**声明。此优化并没有实现一个带宽或缓存敏感的测试工具,能够真实展示仅通过打包带来的时钟时间影响。 + +## 为什么没有 JSONL + +没有专用内存流量基准的密度不是速度门。 + +## 解阻条件 + +对大型工作集进行 SoA / bitset 风格的遍历(或已知良好的 zp-bool-array 风格 bitset 框架,限定在 zp-packable-bools 的模式下),然后是 JSONL。在此之前,该规则保持**关闭**状态。 diff --git a/packages/zperf/evidence/zh-CN/zp-page-allocator-small.md b/packages/zperf/evidence/zh-CN/zp-page-allocator-small.md new file mode 100644 index 0000000..860a00e --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-page-allocator-small.md @@ -0,0 +1,37 @@ +# zp-page-allocator-small — page-allocator-small + +## 主张 + +在这台 Linux 机器上,使用 Zig **0.17.0** `-Ofast`,page_allocator 64B 分配+释放与进程 GPA(smp/c 在 ReleaseFast 下)的对比显示了明显的前后墙钟差距。 + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-page-allocator-small` 情况) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `page_allocator_64B` +- **之后:** `process_gpa_64B` +- 操作次数=20000 +- 原始数据:[`zp-page-allocator-small.jsonl`](../zp-page-allocator-small.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~137.1 ms | +| Median after | ~0.4018 ms | +| **前/后比率** | **~341×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-page-allocator-small` / `page-allocator-small`。`rules.zig` 中的默认严重性为 **警告**;有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且方向上**一致**。 + +## 结论 + +同意 ~341× — 页面分配器对每个小块付出 syscall/页面。 + +## 限制 + +大型 mmap 大小的缓冲区仍然可以故意使用页面分配器。 diff --git a/packages/zperf/evidence/zh-CN/zp-per-packet-alloc.md b/packages/zperf/evidence/zh-CN/zp-per-packet-alloc.md new file mode 100644 index 0000000..9172a1f --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-per-packet-alloc.md @@ -0,0 +1,17 @@ +# zp-per-packet-alloc — 热路径分配 + +## 主张 + +每个数据包/热点注释的函数,每次调用都会在堆上分配,其成本模型与 **zp-alloc-hot**(在热点循环中分配)相同。 + +## 证据 + +没有单独的微基准:重复使用 [`zp-alloc-hot.md`](zp-alloc-hot.md) / [`zp-alloc-hot.jsonl`](../zp-alloc-hot.jsonl)(在这台机器上 GPA 分配+释放比 arena 保留约 **1.4×**;pool/FBA 更大 — 参见 zp-create-destroy-hot/zp-fixed-size-alloc-hot)。 + +## 与 lint 的对应 + +`zperf-lint` 的 zp-per-packet-alloc 会在 alloc 出现在 hot-name / `// zperf-lint: hot` / `--hot-fn` 调用链下时触发。在 rules.zig 中,其严重性仍为 **warning**;将测量的符合性视为与 zp-alloc-hot 共享。 + +## 限制 + +热度启发式可能会产生误报;当分配是每连接一次而不是每包一次时,请忽略它。 diff --git a/packages/zperf/evidence/zh-CN/zp-print-hot.md b/packages/zperf/evidence/zh-CN/zp-print-hot.md new file mode 100644 index 0000000..e535e5f --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-print-hot.md @@ -0,0 +1,37 @@ +# zp-print-hot — 循环中打印 + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** 使用 `-Ofast` 时,循环中的 std.debug.print 与安静的计数器(相同操作)显示出明显的前后实际时间差距。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-print-hot` 情况) +- 构建:`zig build-exe -Ofast` · 优化模式=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `debug_print_in_loop` +- **之后:** `quiet_counter` +- 操作次数=2000 +- 原始数据:[`zp-print-hot.jsonl`](../zp-print-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~2.86 ms | +| Median after | ~4.5e-05 ms | +| **前/后比率** | **~6.36e+04×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-print-hot` / `print-in-loop`。`rules.zig` 中的默认严重性是 **关闭**(CLI 不会报告该规则);有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上 **一致**。 + +## 结论 + +同意 ~63566× — stderr 锁 + 格式;依赖环境,比如 jsperf 的 jp-console-hot。 + +## 限制 + +故意的诊断仍然存在;--strict-log-in-loop 用于标准日志。建议使用缓冲的 Io.Writer。 diff --git a/packages/zperf/evidence/zh-CN/zp-queue-ordered-remove.md b/packages/zperf/evidence/zh-CN/zp-queue-ordered-remove.md new file mode 100644 index 0000000..b36b6b3 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-queue-ordered-remove.md @@ -0,0 +1,37 @@ +# zp-queue-ordered-remove — 队列有序移除 + +## 主张 + +在这台 Linux 机器上,使用 Zig **0.17.0** `-Ofast` 编译,ArrayList.orderedRemove(0) 排空与环形头部前进显示出明显的前后实时时间差距。 + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-queue-ordered-remove` 用例) +- 构建:`zig build-exe -Ofast` · 优化模式=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `ordered_remove_0` +- **之后:** `ring_head_advance` +- 操作数=4000 +- 原始数据:[`zp-queue-ordered-remove.jsonl`](../zp-queue-ordered-remove.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~0.5391 ms | +| Median after | ~0.001157 ms | +| **前/后比率** | **~466×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-queue-ordered-remove` / `queue-ordered-remove`。`rules.zig` 中的默认严重性是 **警告**;有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且方向上**一致**。 + +## 结论 + +同意 ~466× 用于 N=4000 的 FIFO 排水(O(N²) 移位)。 + +## 限制 + +小型有界队列仍然可以使用有序移除;如果不要求顺序,则使用交换移除。 diff --git a/packages/zperf/evidence/zh-CN/zp-runtime-div-hot.md b/packages/zperf/evidence/zh-CN/zp-runtime-div-hot.md new file mode 100644 index 0000000..4a37da9 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-runtime-div-hot.md @@ -0,0 +1,37 @@ +# zp-runtime-div-hot — 循环中的运行时 div + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** 使用 `-Ofast` 时,i % runtime_pow2 与 i & (n-1) 显示出明显的前后墙钟时间差距。 + +## 如何测量 + +- 测试环境:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-runtime-div-hot` 的案例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9轮的中位数** +- **之前:** `runtime_mod` +- **之后:** `bitmask` +- 操作次数=2000000 +- 原始数据:[`zp-runtime-div-hot.jsonl`](../zp-runtime-div-hot.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~3.777 ms | +| Median after | ~0.3242 ms | +| **前后比例** | **~11.6×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-runtime-div-hot` / `runtime-div-in-loop`。`rules.zig` 中的默认严重性为 **关闭**(CLI 不会报告该规则);有关保留/升级的信息,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。存在证据,并且在方向上 **一致**。 + +## 结论 + +同意 ~11.6× 的二次方模数。 + +## 限制 + +Crypto/compiler_rt 路径被跳过;非 2 的幂需要其他重写。 diff --git a/packages/zperf/evidence/zh-CN/zp-shift-on-read.md b/packages/zperf/evidence/zh-CN/zp-shift-on-read.md new file mode 100644 index 0000000..49f5080 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-shift-on-read.md @@ -0,0 +1,37 @@ +# zp-shift-on-read — 读取时转换 + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** 使用 `-Ofast` 时,copyForwards compact 在前缀之后消耗与 cursor head 的对比显示出明显的前后实时时钟差距。 + +## 如何测量 + +- 测试工具:`packages/zperf/examples/bench/evidence_fill.zig` (规则 `zp-shift-on-read` 的案例) +- 构建:`zig build-exe -Ofast` · 优化模式=`fast` +- 计时:`Io.Clock` 唤醒;热身=2;**9 轮的中位数** +- **之前:** `copy_forwards_consume` +- **之后:** `cursor_head` +- 操作次数=20000 +- 原始数据:[`zp-shift-on-read.jsonl`](../zp-shift-on-read.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~0.2825 ms | +| Median after | ~0.01686 ms | +| **前后比例** | **~16.8×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-shift-on-read` / `shift-on-read`。`rules.zig` 中的默认严重性为 **警告**;有关保留/升级的信息,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在且在方向上 **一致**。 + +## 结论 + +在反复滑动剩余字节时,同意约16.8×。 + +## 限制 + +使用读取光标进行摊销;仅在需要时进行压缩。 diff --git a/packages/zperf/evidence/zh-CN/zp-static-string-map.md b/packages/zperf/evidence/zh-CN/zp-static-string-map.md new file mode 100644 index 0000000..fe89fee --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-static-string-map.md @@ -0,0 +1,37 @@ +# zp-static-string-map — 静态字符串映射 + +## 主张 + +在这台 Linux 机器上,Zig **0.17.0** 使用 `-Ofast` 时,StringHashMap.get 与 StaticStringMap.get(字面键)显示出明显的前后实时时钟差距。 + +## 如何测量 + +- 测试套件:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-static-string-map` 的用例) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `string_hashmap_get` +- **之后:** `static_string_map_get` +- 操作次数=200000 +- 原始数据:[ `zp-static-string-map.jsonl`](../zp-static-string-map.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~1.515 ms | +| Median after | ~0.5995 ms | +| **前/后比例** | **~2.53×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-static-string-map` / `static-string-map`。`rules.zig` 中的默认严重性为 **信息**;有关保留/升级,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在且在方向上 **一致**。 + +## 结论 + +同意 ~2.53× 查找;静态也没有运行时分配/初始化。 + +## 限制 + +仅当一个函数中有≥3个字面量的 put 时才触发。 diff --git a/packages/zperf/evidence/zh-CN/zp-unbuffered-io.md b/packages/zperf/evidence/zh-CN/zp-unbuffered-io.md new file mode 100644 index 0000000..73aca2d --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-unbuffered-io.md @@ -0,0 +1,37 @@ +# zp-无缓冲IO — 无缓冲IO + +## 主张 + +在这台 Linux 机器上,使用 Zig **0.17.0** `-Ofast` 对 /dev/null 写入空缓冲区 (&{{}}) 与 4KiB 缓冲区显示出明显的前后实时时钟差异。 + +## 如何测量 + +- 测试框架:`packages/zperf/examples/bench/evidence_fill.zig`(规则 `zp-unbuffered-io` 情况) +- 构建:`zig build-exe -Ofast` · optimize_mode=`fast` +- 计时:`Io.Clock` 唤醒;预热=2;**9 轮的中位数** +- **之前:** `writer_empty_buf` +- **之后:** `writer_4kib_buf` +- 操作数=50000 +- 原始数据:[ `zp-unbuffered-io.jsonl`](../zp-unbuffered-io.jsonl) + +## 运行时(本证据文件) + +| Field | Value | +|-------|--------| +| 时间 | 2026-10-21 亚洲/上海 (JSONL `ts` 为 UTC) | +| Zig | **0.17.0** · `fast` | +| Median before | ~7.29 ms | +| Median after | ~0.1487 ms | +| **前/后比率** | **~49×** | + +## 与 lint 的对应 + +`zperf-lint` `zp-unbuffered-io` / `unbuffered-io`。`rules.zig` 中的默认严重性为 **关闭**(CLI 不会报告此规则);有关保留/升级的详细信息,请参见 [evidence-policy](../../docs/zh-CN/evidence-policy.md)。证据存在并且在方向上 **一致**。 + +## 结论 + +同意 ~49.0× 墙;与指南第19页(~50×)相符。 + +## 限制 + +一次性写入 / readVec-writeVec / 被 lint 跳过的测试。 diff --git a/packages/zperf/evidence/zh-CN/zp-usize-index-field.md b/packages/zperf/evidence/zh-CN/zp-usize-index-field.md new file mode 100644 index 0000000..43ceb99 --- /dev/null +++ b/packages/zperf/evidence/zh-CN/zp-usize-index-field.md @@ -0,0 +1,17 @@ +# zp-usize-index-field — 待定措施 + +## 状态 + +**待处理措施** — 尚无 `evidence/zp-usize-index-field.jsonl`。 + +## 原因 + +`usize` 索引字段如果可以更窄,就是一种 **布局密度/ABI** 问题。本次提交没有承诺任何实际带宽的利用。 + +## 为什么没有 JSONL + +没有交通测量的尺寸/密度不是速度门。 + +## 解阻条件 + +结构数组遍历,其中缩小索引字段会改变行密度,足以移动中位时钟;然后是 JSONL。在此之前,该规则保持 **关闭**。 diff --git a/packages/zperf/zh-CN/CONTRIBUTING.md b/packages/zperf/zh-CN/CONTRIBUTING.md new file mode 100644 index 0000000..81bacf9 --- /dev/null +++ b/packages/zperf/zh-CN/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# 参与贡献 + +本 package 是 [XPerf](../../../zh-CN/README.md) monorepo 的一部分。 + +请遵循根目录 [CONTRIBUTING.md](../../../zh-CN/CONTRIBUTING.md) 与 [docs/playbook.md](../../../docs/zh-CN/playbook.md)。 +Zig 检查:`cd zperf-lint && zig build && zig build test`;从仓库根目录用 `bash scripts/build-guides.sh zperf` 重新生成指南(见 [README.md](README.md))。 diff --git a/packages/zperf/zh-CN/README.md b/packages/zperf/zh-CN/README.md new file mode 100644 index 0000000..07a57cb --- /dev/null +++ b/packages/zperf/zh-CN/README.md @@ -0,0 +1,76 @@ +# zperf + +属于 [XPerf](../../../zh-CN/README.md)。共享文档:[docs/README.md](../../../docs/zh-CN/README.md)。 + +Zig 性能指南、可运行示例,以及 `zperf-lint` 静态检查器。 + +**corpus-ready: 是**。G3/r4 triage 下调了 OK-idiom 大量命中的规则;默认映射保持九条开启:zp-page-allocator-small、zp-broad-runtime-safety、zp-static-string-map、zp-queue-ordered-remove、zp-shift-on-read、zp-invariant-float-div、zp-arena-reset-free-all、zp-create-destroy-hot 与 zp-fixed-size-alloc-hot。见 [docs/corpus.md](../docs/zh-CN/corpus.md)。 + +许可:[MIT License](../../../LICENSE)。 + +## 目录 + +- [英文性能指南](../docs/zh-CN/zig-performance-guide.md) — 由 `docs/guide-src/` 生成。 +- [社区工具对比](../docs/zh-CN/community-tooling.md) — 相关 Zig linter:范围与限制。 +- [`zperf-lint` 文档](../zperf-lint/README.md) — 构建、用法、规则、夹具。 +- [语料固定目标与 G3 结果](../docs/zh-CN/corpus.md)、[证据策略](../docs/zh-CN/evidence-policy.md)、[规则 ID](../docs/zh-CN/rule-ids.md)、[证据笔记](../evidence/zh-CN/README.md)。 + +## 关注点 + +循环内分配、arena 与定长模式、packed 布局、运行时安全检查、热路径 I/O,以及诚实的测量说明。没有本地基准或引用的主张保持暂定。 + +## 运行 lint + +示例与 `zperf-lint` 面向 Zig 0.17.0。`zperf-lint` 使用 Zig 标准 AST,无第三方运行时依赖。 + +Zig 包根是 **`zperf-lint/`**(`build.zig` 与 `build.zig.zon` 在那里),不是 `packages/zperf/` 伞目录。 + +```sh +cd zperf-lint +zig build -Doptimize=fast +zig build test +zig-out/bin/zperf-lint ../examples +``` + +发现是无类型信息的基于语法的启发式。把它们当评审提示,不是编译器诊断。每规则限制与抑制见工具文档。 + +从 `packages/zperf/` 与检查器一起跑社区 linter: + +```sh +zig run scripts/repo.zig -- lint-all +``` + +`lint-all` 默认看 `third-party/bin`。用 `TOOLS` 覆盖;用 `ZPERF` 覆盖检查器二进制。 + +## 示例与指南生成 + +示例在 `examples/`: + +```sh +cd examples && zig build test +``` + +指南由 `docs/guide-src/` 章节分片生成。编辑分片后,从仓库根重新生成: + +```sh +bash scripts/build-guides.sh zperf +``` + +这会写出 `docs/zig-performance-guide.md`。当指南与分片不一致时,`bash scripts/build-guides.sh --check zperf` 失败。 + +## 布局 + +```text +ci/ 指向根 `.github/workflows/ci.yml` 的指针(无嵌套 Actions) +docs/ 指南、语料固定目标、证据策略,以及 guide-src 章节 +evidence/ 墙钟门禁与每规则笔记(*.md + *.jsonl) +examples/ 指南引用的可运行 Zig 示例 +scripts/ repo.zig(lint-all)与 regen-evidence.sh +zperf-lint/ 检查器包、测试与夹具 +``` + +`zperf-lint/` 与 `examples/` 是分开的 Zig 构建根;各自在其目录下构建。 + +## 参与贡献 + +见 [CONTRIBUTING.md](CONTRIBUTING.md)。 diff --git a/scripts/build-guides.sh b/scripts/build-guides.sh index 8d50d48..377acb7 100755 --- a/scripts/build-guides.sh +++ b/scripts/build-guides.sh @@ -3,7 +3,9 @@ # bash scripts/build-guides.sh [kit...] write the guides (default: all kits) # bash scripts/build-guides.sh --check [kit...] exit 1 if a committed guide differs # Layouts: docs/guide-src/*.md -> docs/*-performance-guide.md -# docs/guide-src//*.md -> docs//performance-guide.md +# docs/guide-src//*.md -> docs//.md +# For locale pairing, zh-CN output reuses the English *-performance-guide.md basename +# when present; otherwise performance-guide.md (jvmperf-style en/zh-CN layout). set -euo pipefail export LC_ALL=C ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -16,6 +18,7 @@ build() { # shard_dir out local dir="$1" out="$2" local shards=("$dir"/*.md) [[ -e "${shards[0]}" ]] || return 0 + mkdir -p "$(dirname "$out")" if [[ "$MODE" == check ]]; then if ! cat "${shards[@]}" | cmp -s - "$out"; then echo "build-guides: ${out#"$ROOT"/} differs from ${dir#"$ROOT"/}/*.md" >&2 @@ -26,6 +29,16 @@ build() { # shard_dir out fi } +lang_out() { # docs_dir lang -> stdout path + local d="$1" lang="$2" + local eng=("$d"/*-performance-guide.md) + if [[ "$lang" == zh-CN && -e "${eng[0]}" ]]; then + echo "$d/$lang/$(basename "${eng[0]}")" + else + echo "$d/$lang/performance-guide.md" + fi +} + for kit in "$ROOT"/packages/*/; do name="$(basename "$kit")" if [[ ${#KITS[@]} -gt 0 ]] && [[ " ${KITS[*]} " != *" $name "* ]]; then continue; fi @@ -34,7 +47,8 @@ for kit in "$ROOT"/packages/*/; do outs=("$d"/*-performance-guide.md) [[ -e "${outs[0]}" ]] && build "$d/guide-src" "${outs[0]}" for lang in en zh-CN; do - [[ -d "$d/guide-src/$lang" ]] && build "$d/guide-src/$lang" "$d/$lang/performance-guide.md" + [[ -d "$d/guide-src/$lang" ]] || continue + build "$d/guide-src/$lang" "$(lang_out "$d" "$lang")" done done diff --git a/scripts/build-pages.sh b/scripts/build-pages.sh index 37372a9..5011720 100755 --- a/scripts/build-pages.sh +++ b/scripts/build-pages.sh @@ -16,3 +16,19 @@ if [[ "${1:-build}" == "serve" ]]; then fi export PAGES_STATIC="${PAGES_STATIC:-1}" npm run build + +# Static export keeps root . Crawlers do not run the client +# HtmlLang effect, so rewrite zh HTML to zh-CN for SEO/GEO. +python3 - <<"PY" +from pathlib import Path +root = Path("out") / "zh" +if root.is_dir(): + n = 0 + for html in root.rglob("*.html"): + text = html.read_text(encoding="utf-8") + new = text.replace(" - - {children} - + {children} ); } diff --git a/site-app/app/robots.ts b/site-app/app/robots.ts new file mode 100644 index 0000000..4d15320 --- /dev/null +++ b/site-app/app/robots.ts @@ -0,0 +1,35 @@ +import type { MetadataRoute } from 'next'; +import { SITE_URL } from '@/lib/site-meta'; + +export const dynamic = 'force-static'; + +/** Allow general crawlers and common generative-engine / AI bots (GEO). */ +const AI_BOTS = [ + 'GPTBot', + 'ChatGPT-User', + 'Google-Extended', + 'anthropic-ai', + 'ClaudeBot', + 'Claude-Web', + 'PerplexityBot', + 'Bytespider', + 'CCBot', + 'meta-externalagent', +] as const; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: '/', + }, + ...AI_BOTS.map((userAgent) => ({ + userAgent, + allow: '/', + })), + ], + sitemap: `${SITE_URL}/sitemap.xml`, + host: SITE_URL, + }; +} diff --git a/site-app/app/sitemap.ts b/site-app/app/sitemap.ts new file mode 100644 index 0000000..27eeaf9 --- /dev/null +++ b/site-app/app/sitemap.ts @@ -0,0 +1,70 @@ +import type { MetadataRoute } from 'next'; +import { SITE_URL, absoluteUrl, pagePath } from '@/lib/site-meta'; +import { source } from '@/lib/source'; + +export const dynamic = 'force-static'; + +function withTrailingSlash(path: string): string { + if (path === '/') return '/'; + return path.endsWith('/') ? path : `${path}/`; +} + +export default function sitemap(): MetadataRoute.Sitemap { + const byKey = new Map(); + + for (const locale of ['en', 'zh'] as const) { + for (const page of source.getPages(locale)) { + const path = withTrailingSlash(page.url || '/'); + const url = absoluteUrl(path === '/' ? '/' : path); + const key = (page.slugs ?? []).join('/') || ''; + const row = byKey.get(key) ?? { depth: page.slugs?.length ?? 0, firstSlug: page.slugs?.[0] }; + if (locale === 'en') row.en = url; + else row.zh = url; + row.depth = page.slugs?.length ?? 0; + row.firstSlug = page.slugs?.[0]; + byKey.set(key, row); + } + } + + // Ensure home keys exist even if a locale omits the root somehow. + if (!byKey.has('')) { + byKey.set('', { + en: absoluteUrl(pagePath(undefined, 'en')), + zh: absoluteUrl(pagePath(undefined, 'zh')), + depth: 0, + }); + } + + const entries: MetadataRoute.Sitemap = []; + for (const row of byKey.values()) { + const url = row.en ?? row.zh; + if (!url) continue; + let priority = 0.6; + if (row.depth === 0) priority = 1; + else if (row.firstSlug === 'docs') priority = 0.8; + else if (row.firstSlug === 'packages') priority = 0.7; + + const languages: Record = {}; + if (row.en) languages.en = row.en; + if (row.zh) languages['zh-CN'] = row.zh; + if (row.en) languages['x-default'] = row.en; + + entries.push({ + url, + changeFrequency: 'weekly', + priority, + alternates: Object.keys(languages).length ? { languages } : undefined, + }); + // Also list the zh URL as its own entry when both exist (crawlers that ignore xhtml links). + if (row.en && row.zh && row.zh !== row.en) { + entries.push({ + url: row.zh, + changeFrequency: 'weekly', + priority, + alternates: { languages }, + }); + } + } + + return entries; +} diff --git a/site-app/assemble.js b/site-app/assemble.js index a2ffba9..5f9d9db 100644 --- a/site-app/assemble.js +++ b/site-app/assemble.js @@ -6,6 +6,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { bundledLanguages } from 'shiki'; +import { slug as githubSlug } from 'github-slugger'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const CONTENT = path.join(ROOT, 'site-app', 'content', 'docs'); @@ -80,15 +81,54 @@ function shikiLangs() { const KNOWN_LANGS = shikiLangs(); +// Match Fumadocs heading ids (remark-heading → github-slugger). +// Keep Unicode letters so zh-CN TOC fragments like #0-范围与方法 survive assembly. function siteAnchor(frag) { const body = frag.startsWith('#') ? frag.slice(1) : frag; - const slug = body - .toLowerCase() - .replace(/[^a-z0-9-]+/g, '') - .replace(/-{2,}/g, '-') - .replace(/^-+|-+$/g, ''); - if (!slug) return frag.startsWith('#') ? frag : `#${frag}`; - return `#${slug}`; + let decoded = body; + try { + decoded = decodeURIComponent(body); + } catch { + // keep the raw fragment when it is not percent-encoded + } + const next = githubSlug(decoded); + if (!next) return frag.startsWith('#') ? frag : `#${frag}`; + return `#${next}`; +} + +function assertSiteAnchors() { + const cases = [ + ['#0-范围与方法', '#0-范围与方法'], + ['#2-热路径-io', '#2-热路径-io'], + ['#0-scope-and-method', '#0-scope-and-method'], + ['#2-Hot-path-I/O', '#2-hot-path-io'], + ['#lint对照表', '#lint对照表'], + ]; + for (const [input, want] of cases) { + const got = siteAnchor(input); + if (got !== want) { + throw new Error(`siteAnchor(${JSON.stringify(input)}) => ${JSON.stringify(got)}, want ${JSON.stringify(want)}`); + } + } +} + +function assertPageHrefs() { + const cases = [ + ['docs/zh-CN/index.md', 'docs/zh-CN/quickstart.md', 'quickstart/'], + ['docs/zh-CN/index.md', 'packages/jsperf/docs/zh-CN/js-performance-guide.md', '../packages/jsperf/docs/js-performance-guide/'], + ['packages/jsperf/docs/zh-CN/js-performance-guide.md', 'packages/jsperf/evidence/zh-CN/jp-alloc-hot.md', '../../evidence/jp-alloc-hot/'], + ['docs/index.md', 'docs/quickstart.md', 'quickstart/'], + ['packages/jsperf/docs/js-performance-guide.md', 'packages/jsperf/evidence/jp-alloc-hot.md', '../../evidence/jp-alloc-hot/'], + ['docs/zh-CN/quickstart.md', 'docs/zh-CN/index.md', '../'], + ]; + for (const [from, dest, want] of cases) { + const got = pageHref(from, dest); + if (got !== want) { + throw new Error( + `pageHref(${JSON.stringify(from)}, ${JSON.stringify(dest)}) => ${JSON.stringify(got)}, want ${JSON.stringify(want)}`, + ); + } + } } function normRel(base, urlPath) { @@ -117,11 +157,17 @@ function relLink(fromLogical, toPath) { return path.posix.relative(origin, toPath); } +// Published files may be `guide.zh.md` (fumadocs dot-locale). Public URL +// slugs drop the locale marker; the `/zh/` prefix carries the language. +function publicLogical(published) { + return published.replace(/\.zh\.md$/i, '.md'); +} + // Fumadocs serves pages as directories (trailingSlash), not as `.md` files. -// Map a content logical path to that URL path (always with a trailing slash, +// Map a published content path to that URL path (always with a trailing slash, // except the site root which is `./` from nested pages and `` from root). function sitePath(destLogical) { - let dest = destLogical.replaceAll('\\', '/'); + let dest = publicLogical(destLogical.replaceAll('\\', '/')); if (dest === 'index.md') return ''; if (dest.endsWith('/index.md')) dest = dest.slice(0, -'index.md'.length); else if (dest.endsWith('.md')) dest = `${dest.slice(0, -3)}/`; @@ -588,6 +634,8 @@ function skip(filePath) { } function assemble() { + assertSiteAnchors(); + assertPageHrefs(); fs.rmSync(CONTENT, { recursive: true, force: true }); fs.rmSync(PUBLIC, { recursive: true, force: true }); fs.mkdirSync(CONTENT, { recursive: true }); @@ -640,6 +688,15 @@ function assemble() { } writeSidebar(); checkNav(copied); + const seoStatic = path.join(ROOT, 'site-app', 'seo-static'); + if (fs.existsSync(seoStatic)) { + for (const name of fs.readdirSync(seoStatic)) { + if (name.startsWith('.')) continue; + const src = path.join(seoStatic, name); + if (!fs.statSync(src).isFile()) continue; + fs.copyFileSync(src, path.join(PUBLIC, name)); + } + } console.log( `build-pages: assembled ${mdFiles.length} pages, ${files.length} files, rewrote ${rewritten} links`, ); @@ -648,6 +705,7 @@ function assemble() { process.exit(1); } const mdHrefs = []; + const localeHrefs = []; for (const [, , published] of mdFiles) { const body = fs.readFileSync(path.join(CONTENT, published), 'utf8'); let fence = false; @@ -665,6 +723,10 @@ function assemble() { if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) continue; const pathOnly = raw.split('#', 1)[0].split('?', 1)[0]; if (pathOnly.endsWith('.md')) mdHrefs.push(`${published}: ${raw}`); + // Locale markers belong in content filenames, not public URL slugs. + if (/(?:^|\/)[^/]*\.zh(?:\/|$)/.test(pathOnly)) { + localeHrefs.push(`${published}: ${raw}`); + } } } } @@ -674,6 +736,12 @@ function assemble() { ); process.exit(1); } + if (localeHrefs.length) { + console.error( + `build-pages: ${localeHrefs.length} page links still carry a .zh locale slug\n${localeHrefs.slice(0, 40).join('\n')}`, + ); + process.exit(1); + } } function checkNav(copied) { @@ -691,8 +759,18 @@ function checkNav(copied) { } const cmd = process.argv[2] ?? 'build'; +if (cmd === 'check-anchors') { + assertSiteAnchors(); + console.log('assemble: siteAnchor checks OK'); + process.exit(0); +} +if (cmd === 'check-hrefs') { + assertPageHrefs(); + console.log('assemble: pageHref checks OK'); + process.exit(0); +} if (cmd !== 'build' && cmd !== 'serve') { - console.error('usage: node assemble.js [build|serve]'); + console.error('usage: node assemble.js [build|serve|check-anchors|check-hrefs]'); process.exit(1); } assemble(); diff --git a/site-app/lib/docs-page.tsx b/site-app/lib/docs-page.tsx index 453678f..9e6e01b 100644 --- a/site-app/lib/docs-page.tsx +++ b/site-app/lib/docs-page.tsx @@ -4,6 +4,15 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { getMDXComponents } from '@/components/mdx'; import type { Locale } from '@/lib/i18n'; +import { + OG_IMAGE_ALT, + OG_IMAGE_ALT_ZH, + OG_IMAGE_PATH, + SITE_NAME, + descriptionFor, + ogLocale, + pagePath, +} from '@/lib/site-meta'; import { source } from '@/lib/source'; function load(slugs: string[] | undefined, locale: Locale) { @@ -38,9 +47,50 @@ export function renderDocsPage(slugs: string[] | undefined, locale: Locale) { export function docsMetadata(slugs: string[] | undefined, locale: Locale): Metadata { const page = load(slugs, locale); const title = page.data.title === 'XPerf' ? { absolute: 'XPerf' } : page.data.title; + // Always set a string. Returning `description: undefined` from generateMetadata + // clears the root layout description in the static HTML export. + const description = page.data.description?.trim() || descriptionFor(locale); + const canonical = pagePath(slugs, locale); + const enPath = pagePath(slugs, 'en'); + const zhPath = pagePath(slugs, 'zh'); + const ogTitle = + typeof title === 'string' ? title : title.absolute === 'XPerf' ? SITE_NAME : String(page.data.title); + const imageAlt = locale === 'zh' ? OG_IMAGE_ALT_ZH : OG_IMAGE_ALT; + return { title, - description: page.data.description, + description, + alternates: { + canonical, + languages: { + en: enPath, + 'zh-CN': zhPath, + 'x-default': enPath, + }, + }, + openGraph: { + type: 'article', + locale: ogLocale(locale), + alternateLocale: locale === 'zh' ? ['en_US'] : ['zh_CN'], + url: canonical, + siteName: SITE_NAME, + title: ogTitle, + description, + images: [ + { + url: OG_IMAGE_PATH, + width: 1200, + height: 630, + alt: imageAlt, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: ogTitle, + description, + images: [OG_IMAGE_PATH], + }, }; } diff --git a/site-app/lib/docs-shell.tsx b/site-app/lib/docs-shell.tsx index a471579..0a3d00e 100644 --- a/site-app/lib/docs-shell.tsx +++ b/site-app/lib/docs-shell.tsx @@ -5,15 +5,20 @@ import SearchDialog from '@/components/search'; import { HtmlLang } from '@/components/html-lang'; import { i18n, type Locale } from '@/lib/i18n'; import { baseOptions } from '@/lib/layout.shared'; +import { htmlLang, siteJsonLd } from '@/lib/site-meta'; import { source } from '@/lib/source'; -const htmlLang: Record = { en: 'en', zh: 'zh-CN' }; - export function DocsShell({ locale, children }: { locale: Locale; children: ReactNode }) { + const lang = htmlLang(locale); + const jsonLd = siteJsonLd(locale); return ( - -
+ +