Skip to content

课题4 W5:DCE use-def 回溯 - #51

Open
DzSexton wants to merge 6 commits into
ScratchV-Compiler:mainfrom
DzSexton:feat/topic-04-w5-dce-use-def
Open

课题4 W5:DCE use-def 回溯#51
DzSexton wants to merge 6 commits into
ScratchV-Compiler:mainfrom
DzSexton:feat/topic-04-w5-dce-use-def

Conversation

@DzSexton

Copy link
Copy Markdown

内容

  • DeadCodeEliminator 改为单基本块 use-def mark-and-sweep:建立 dest.name → Instruction 定义索引,从可观察根递归回溯生产者,再稳定顺序 sweep。
  • 保留 STORERETURNBRBR_IFFORENDFORALLOCA 及所有 dest-less 指令,并完整保留其 operand 依赖链。
  • 使用 instruction identity 作为递归访问保护,支持外部/隐式输入叶子和畸形环终止,不修改 IR 数据模型。
  • 多基本块函数、重复 SSA 定义和非空 phi_nodes 在 W5 保守 no-op,避免提前进行不安全的跨块删除。
  • 删除计数覆盖整条死依赖链且只统计本次调用;同一实例或同一 PassManager 重复运行不会累计历史值。
  • 新增独立 DCE 行为矩阵、PassManager 集成测试,并同步课题文档与优化指南。

范围

本 PR 基于并依赖 #48(W2 统一 Pass 接口与 PassManager)。本分支直接从 W2 基线创建,不依赖 #50 的 W3 ConstantFolder 内容。

本 PR 仅实现课题 4 的 W5「DCE use-def 回溯」。没有实现 W6 的跨基本块分析、CFG 数据流或不动点迭代,也没有修改 Value/Instruction、IRBuilder、前端、后端、PassManager 配置或其他优化算法。

验证

  • W5 DCE 行为矩阵:22 passed
  • DCE、optimizer 与 PassManager 专项:72 passed
  • 可运行完整回归:435 passed, 4 skipped, 3 deselected
  • 未筛选完整测试仅保留 3 个既有 Windows 临时文件句柄/TinyFive 环境失败
  • Python 语法编译与 git diff --check 通过
  • 本地环境未安装 Black、isort、Ruff、mypy,未将这些检查写为已通过
  • 已对照 W5 设计自审,确认未提前实现 W6

草稿 PR,等待维护者先确认 #48 的 W2 接口,再审查 W5 的单块安全边界、effect roots 与删除计数语义。

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 AI Code Review

共审查 10 个变更文件
⚠️ 另有 13 个文件超过上限(最多 10 个)未审查

📁 CONTRIBUTING.md

🔴 API 一致性待确认 — 步骤 2/3 将方法从 run 改为 optimize,并引用 OptimizationPass;步骤 4 注册位置改为 create_optimization_pass_manager()scratchv/compiler.py)。若这些符号在代码库不存在或签名不同,会误导贡献者。请先核实再合并。

🟡 缺少 name 唯一性约束 — 既然 manager 用该标识符做报告/诊断,建议明确加上 “must be unique across all passes”,避免贡献者重名导致冲突。

🟡 测试未覆盖返回计数 — 新契约强调 optimize 返回非负计数,但步骤 5 只覆盖了正/负变换场景。建议补充:测试应断言返回值等于实际变换次数。

💭 未指明基类位置 — 建议补一句 OptimizationPass 的定义位置(例如 scratchv/optimizer/base.py),方便新贡献者查 API。

💭 注册方式太笼统create_optimization_pass_manager() 中是手动实例化还是通过注册器装饰器?如果已有固定模式,给一行伪代码示例会减少猜测。


📁 benchmarks/run_benchmark.py

🔴 Potential Wrong Timing for "none"_optimize now calls manager.run(program).elapsed_seconds even for the empty pipeline. The docstring claims the value is exactly 0.0, but if run() measures wall time around an empty pass list, it will be a small non-zero number that breaks baseline comparisons.
Suggestion: keep an early if level == "none": return 0.0, or make sure create_optimization_pass_manager("none").run() itself hardcodes 0.0.

🟡 API contract for unknown levels — The old code accepted any non-"none" level by running constant folding + DCE, and "all" added extra passes. If create_optimization_pass_manager rejects previously accepted values (e.g. "base" or any typo), this is a behavior break.
Suggestion: confirm every level supported by the CLI is handled by the new manager, and ideally validate the level at argument parsing rather than deep inside _optimize.

🟡 Redundant IR count for "none" — Removing the if optimize_level != "none" guard means _count_ir now runs twice for the no-op case. It is not timed, so it only adds benchmark-loop overhead, but keeping an early return for "none" would preserve the previous, leaner path.

💭 Verify time sourceelapsed_seconds should match the previous time.perf_counter() semantics (wall time). If the pass manager measures with time.process_time or includes pipeline construction time, results won’t be comparable to historical runs. Worth a quick check in create_optimization_pass_manager.


📁 benchmarks/test_benchmark.py

🟡 断言 inst_after <= inst_before 依赖 pass 的“不膨胀”语义test_optimize 现在假设 "all" 中的所有 pass 都不会增加指令数。若 "all" 未来加入 loop unrolling、inlining、lowering 等可能扩大 IR 的 pass,这个断言会误报。建议确认 "all" 只包含“简化型”优化;否则只保留 inst_after > 0,或改成对 inst_after 上限的显式说明。

🟡 局部 import 会在每个参数化用例中重复执行from scratchv.compiler import create_optimization_pass_manager 放在函数内可以避免循环导入,但在 pytest.mark.parametrize 下会每个模型执行一次 import。若该模块导入成本较高,建议移到模块顶部;如果确实是为避免循环导入,加一行注释说明原因。

💭 测试只检查指令数,没检查语义保持inst_after > 0 不能防止优化器删掉必要指令。如果项目里有 interpreter 或 golden output,建议在 smoke test 中加一个轻量等价性校验,避免优化器“变成空壳”。

💭 "basic" 的 pass 集合应保持可预期test_codegen_riscv 从显式的 ConstantFolder + DCE 换成 "basic" 后,测试行为与 pass manager 的命名耦合。建议在 create_optimization_pass_manager 的 docstring 中明确 "basic" 包含哪些 pass,或在测试里断言 "basic" 的行为不会改变。

👍 改进:原来 inst_after >= 0 是恒真断言,改成 0 < inst_after <= inst_before 后至少能捕捉到“优化后 IR 为空”或“指令数异常增长”的情况,方向是对的。


📁 docs/optimization_guide.md

🔴 文档与示例不一致 — 新增段声称所有 pass 都定义 name 且实现 OptimizationPass.optimize(...),但后面的 PeepholeOptimizer/LICM 示例既没有 name 属性,也没有继承或注册到 OptimizationPass。建议在示例中补上 name = "peephole" 等常量,或说明基类仅作接口约定、实际类可以独立存在。

🟡 --optimize 描述容易误解 — “remains an alias for the option name” 中的 “the option name” 指代不清,建议直接写 “alias for --opt-level”。同时可以补充一句:--opt-level 是带值选项,所以裸写 --optimize 才会无效;否则读者可能误以为 --optimize 是布尔开关被移除了。

🟡 W5 跳过逻辑需要明确报告行为 — “Multi-block functions, duplicate SSA names, and non-empty phi_nodes remain unchanged” 容易让读者误以为 pass 会报错或整个 pipeline 失败。建议说明:这类函数会被安全跳过,optimize() 返回 0,且 OptimizationReport.executions 中仍会记录该 pass 已运行(如果实际如此)。

💭 “non-negative number” 可改为 “non-negative integer” — 因为签名明确返回 int,用 number 不够精确。

💭 两个工具共用 --opt-level 但取值不同 — 主 CLI 是 none|basic|all,LLVM 工具是 0|1|2|3。虽然文档写了 “separate”,但同一选项名容易让用户混淆。建议在文档中强调工具名,或考虑给 LLVM 工具单独命名(如 --llvm-opt-level)。


📁 docs/topics/04-IR优化器框架.md

🔴 文档矛盾:ALLOCA 列入“没有目标值的指令” — 活性根列表将 ALLOCASTORE/RETURN 并列,随后又称“所有没有目标值的指令”。若 ALLOCA 在 IR 中产生结果(通常分配给一个 SSA 名),它就不是无目标指令,表述自相矛盾。请明确 ALLOCA 在该 IR 中是否具有目标值,或调整分类说明。

🟡 _build_definitions 的重复检测未说明 — 代码中 definitions is None 会触发跳过,说明该方法在遇到重复 SSA 名称时返回 None,但文档没有解释它是如何识别“重复”的(按名称?按目标?)。建议补充两句:重复定义会保留整个函数不变,以免读者误解为可自动选最后定义。

🟡 基类 property 与子类类属性OptimizationPass.name 声明为 property,但示例用 name = "constant-folding" 直接覆盖。技术上可行,但对不熟悉 Python 描述符的读者会困惑。建议改为在基类中把 name 设为普通类属性(如 name = ""),再在子类中覆盖,或明确注释这是有意的覆盖。

💭 id(instr) 的边界条件可交代一句 — 使用 id() 可以规避指令类未实现 __hash__/__eq__ 的重载问题,但依赖于指令对象在遍历期间不被 GC。既然文档是设计说明,值得加一句“所有指令在 pass 执行期间存活,故 id 唯一性成立”,否则后续修改可能意外破坏这个假设。

💭 命令行示例的别名参数缺少等号可能误解scratchv model.onnx --optimize basic--opt-level basic 都可行,但若实际解析器要求 --optimize=basic 形式,示例会产生误导。建议确认并统一示例风格。


📁 docs/topics/archive/optimizer_framework.md

🟡 归档文档中新增了“当前实现”细节 — 归档说明已声明本文是历史摘要、以新文档和代码为准;但紧接着又加入工厂函数、管线映射和 CLI 用法等当前实现细节,容易形成多源信息。建议将这些内容移到 docs/topics/04-IR优化器框架.md,归档文档仅保留指针;若必须保留,请先把归档说明调整为“以下为归档时点的快照”。

🟡 代码示例和 CLI 参数未关联实际源码create_optimization_pass_manager--opt-level/--optimize 等均未给出对应源码路径或测试依据。建议补充指向 scratchv/compiler.py、CLI 模块的链接,或注明“示例仅为概念,请以实际代码为准”。

🟡 新链接目标存在性需确认 — 链接文本写作 docs/topics/04-IR优化器框架.md,但实际相对路径从 archive/ 解析。请确认目标文件已存在且路径正确,否则归档文档会保留一个断链。

💭 “三级管线”措辞有歧义 — 后续列出的实际是三个优化级别(none/basic/all)的映射,并非“三个阶段的管线”。建议改为“三档优化管线”或“三个优化级别”。

💭 “当前公共接口”应标注时间或版本 — 归档文档中的“当前”在读者眼中可能是最新状态,建议写明归档日期或对应 commit,例如“截至 2025-xx-xx”。

💭 OptimizationReport.executions 缺少字段名 — 描述只写了记录“pass 名称、变更数和耗时”,未给出条目字段名(如 namechangeselapsed)。补全后读者可直接获取报告数据,无需查看代码。


📁 docs/verification.md

🔴 语义变更风险:--opt-level all ≠ 原来的 --optimize
原命令 --optimize 看起来是布尔开关(开启优化),改成 --opt-level all 意味着启用“所有优化级别”。如果 all 包含激进/实验性 pass,验证场景可能偏离真实行为,且可能更慢。请确认 all 的定义;若是“全部优化”,建议改为 --opt-level basic 或省略(使用默认级别)。

🟡 Note 表述有歧义 — “--optimize 只是 --opt-level 的参数名兼容别名” 容易让读者以为 --optimize 可以裸用。建议明确:--optimize--opt-level 的兼容拼写,但两者都必须携带级别值(如 --optimize all),裸 --optimize 不合法。

🟡 缺少 none/basic/all 的定义 — 文档只列出三个级别,但没有说明它们对输出 IR/汇编的影响。建议在 Note 中链接到 CLI 参考,或各加一句简介,否则用户无法判断 all 是否符合预期。

💭 检查全文档其他裸 --optimize — 本次 diff 只改了示例,后面 “LLVM IR Verification” 等章节可能还有遗留的裸 --optimize,需要一并扫描替换,避免文档前后不一致。


📁 examples/end_to_end_pipeline.py

整体改动方向正确:用 pass manager 统一管道比手工拼接 optimizer 更清晰,也顺带修掉了旧代码里可能在未折叠 IR 上继续跑 DCE 的隐患。

🟡 建议

  1. 硬编码 pass 名取报告demo_matmul 里用 changes_by_name["constant-folding"] / ["dead-code-elim"] 直接访问。若 pass 改名、未注册或执行顺序变化,会直接 KeyError
    建议:若 report 提供按名称查询的 API(如 report.get_changes("constant-folding")),优先使用;否则至少用常量/枚举管理 pass 名。

  2. “all” 的具体含义不够透明 — 注释只写了 “five canonical passes in registration order”,但旧的 demo 是 fold + dce + peephole。改成 "all" 后输出可能包含额外优化,效果更好但对比对象变了。
    建议:在注释里列出这 5 个 pass 的名称,让读者清楚 After full optimization 到底跑了什么。

  3. 两个 demo 的 report 处理逻辑重复demo_matmuldemo_optimized_pipeline 都在构造 changes_by_name / 打印统计。示例代码可以接受,但抽一个小 helper(如 print_optimizer_report(report))能减少未来漂移。

💭 Note

  • run(program) 原地修改 program 并返回 immutable report,这是合理设计,但示例中容易让读者误以为 program 没变。建议在注释里明确:“后续 LLVMCodegen(program) 使用的是优化后的 IR”。
  • 若某个 pass 的 changesNone 而不是 intf"{folded_changes} constant fold(s)" 会打出 None constant fold(s)。建议统一让 changes 始终为 int,或直接用 report.total_changes 做汇总打印。

📁 examples/llvm_optimization_pipeline.py

🟡 硬编码 pass 名称有 KeyError 风险changes_by_name["constant-folding"] / ["dead-code-elim"] 直接依赖 pass manager 内部命名。若 "basic" pipeline 稍后调整 pass 名称或顺序,这里会直接 KeyError,而且错误信息不直观。
建议:优先使用 report 自带的按名称查询接口(如 report.changes_for("constant-folding"));若必须用 dict,改用 changes_by_name.get(...) 并给出明确的失败提示。

🟡 report.executions 的结构假设没有验证 — 这里假设 executions 是 list,且每个 execution 都有 .name.changes。如果 report 是“immutable report”,建议确认它是不是返回了受保护视图,避免调用方误以为可以修改。

💭 “in place” 行为值得在注释中保留到函数签名/文档里run(program2) 既修改 program2 又返回 report,这种“副作用 + 返回值”的 API 很容易被误用。如果 create_optimization_pass_manager 不是本项目独有 API,建议在 report 文档中显式标注。

💭 changes_by_name 这个中间 dict 只用于打印两个值,稍显多余 — 如果 report 提供了 changes_for(name),直接调用即可;否则 report.total_changes"all" 已经够用,"basic" 也可以考虑只打印 report.total_changes,减少对 pass 具体名称的依赖。


📁 examples/onnx_llvm_verification.py

  • 🟡 Potential KeyError on pass-name lookup — Lines ~65-68: changes_by_name["constant-folding"] and changes_by_name["dead-code-elim"] will crash if the "basic" pipeline changes execution names (e.g., "constant_folding" or a renamed pass). Consider using changes_by_name.get("constant-folding", 0) or, better, add a lookup method to report if one doesn’t already exist.

  • 🟡 Verify the in-place/return contract — The comment says run() optimizes program in place and returns a report, but a pass manager commonly returns a new module. If the actual API does not mutate program, the later LLVM generation step will operate on the unoptimized program. Double-check the API and, if safe, assign the optimized program back or use the report’s program reference.

  • 💭 Stringly-typed pass names — The pass names in the dict lookup are hardcoded magic strings. Defining constants (e.g., PASS_CONSTANT_FOLDING) or using an API-provided enum would make typos compile-time detectable and keep the example aligned with the library’s public surface.



⚠️ 未审查的文件

  • examples/verify_with_tinyfive.py
  • scratchv/compiler.py
  • scratchv/main.py
  • scratchv/optimizer/constant_folding.py
  • scratchv/optimizer/dead_code.py
  • scratchv/optimizer/licm.py
  • scratchv/optimizer/muladd_fusion.py
  • scratchv/optimizer/peephole.py
  • scratchv/pass_interface.py
  • tests/test_dead_code.py
  • tests/test_optimizer.py
  • tests/test_optimizer_advanced.py
  • tests/test_pass_manager.py

@DzSexton
DzSexton marked this pull request as ready for review August 20, 2026 13:46
@FeelTheBeats

Copy link
Copy Markdown
Contributor

need to update branch to fix ci.yaml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants