Skip to content

feat: complete safe RV32 constant-load merge optimization - #35

Open
yuki-328 wants to merge 12 commits into
ScratchV-Compiler:mainfrom
yuki-328:feature/const_merge
Open

feat: complete safe RV32 constant-load merge optimization#35
yuki-328 wants to merge 12 commits into
ScratchV-Compiler:mainfrom
yuki-328:feature/const_merge

Conversation

@yuki-328

@yuki-328 yuki-328 commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • reuse the shared assembly parser and canonicalize RV32 integer-register aliases
  • safely merge numeric lui + addi pairs and remove redundant lui only within basic blocks
  • reject relocations, out-of-range immediates, invalid registers, unknown-opcode state, and unsafe control-flow cases
  • preserve whitespace/comments and guarantee textual fixed-point idempotence
  • expose categorized statistics and integrate the pass with the compiler CLI
  • add real same-assembly A/B reporting plus clearly separated synthetic microbenchmarks
  • repair the TinyFive test stub so the complete regression suite passes
  • synchronize the development and technical-design documents with implemented behavior

Statistics semantics

  • candidate_pairs: structural same-block lui/addi candidates before safety checks
  • merged_pairs: candidates that pass register and immediate validation and are transformed
  • redundant_lui_removed: safely removed duplicate high-immediate loads

Real benchmark zero-hit cases are retained. li is treated as a pseudo-instruction, so source assembly reductions are not presented as machine-code or runtime wins. Machine instruction count, .text size, and execution equality remain N/A when a RISC-V toolchain is unavailable.

Validation

  • pytest -q: 430 passed, 4 skipped, 0 failed
  • 4 skips require optional onnxruntime
  • 10,000 randomized RV32 programs passed semantic-equivalence, textual-idempotence, and change-count invariants
  • compileall: passed
  • git diff --check: passed

Known environment limitation

The current environment does not provide GNU RISC-V binutils or Spike, so the documented three-sample external toolchain execution check is still pending and is not represented as completed.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🤖 AI Code Review

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

📁 .github/workflows/ci.yml

🔴 权限缺失导致部署失败 — 全局 permissions 移除了 pages: writeid-token: write,但 deploy job 的 actions/deploy-pages 需要这两个权限。deploy job 会失败,且无降级路径。

🟡 硬编码 PR #35 测试步骤 — 新增 Run PR #35 constant-merge regressions 每次 CI 都会执行,包括非 PR 事件。PR 编号和测试用例不应长期保留在稳定工作流中,建议移除或改为条件触发(如 if: github.ref == 'refs/heads/feature/xxx')。

🟡 缺少 actions: write 权限 — 当前全局权限仅 contents: read,但 upload-artifact 步骤需要 actions: write(即使原始代码可能同样缺失,但应修复该遗留问题)。

💭 pytest 版本约束建议 — 在 CI 中硬编码 pip install "pytest>=7,<10" 应改为在 pyproject.tomlrequirements-dev.txt 中声明,避免与环境不一致。


📁 benchmarks/bench_const_merge.py

🔴 Bug: 参数验证逻辑不完整 — 当 pair_density + redundant_lui_density 恰好等于 1.0 时,choice < redundant_lui_densitychoice < redundant_lui_density + pair_density 能覆盖所有情况,但验证条件 > 1.0 正确。然而,if not 0.0 <= pair_density <= 1.0 会让类似 pair_density=0.0, redundant_lui_density=1.0 通过,但 choice < 1.0 时永远走不到 else 分支(因为 redundant_lui_density=1.0 时第一个条件恒成立),导致永远不会生成普通指令,与预期行为不符。
Suggestion: 明确约束 redundant_lui_density 必须小于 1.0,或调整逻辑,确保三种情况都有合理的概率范围。

🟡 代码健壮性:重复解析开销bench_merge 中调用了两次 parse_asm(一次输入、一次输出),且每次解析都会重新遍历整个汇编文本。如果 repeats 较大(如默认 50),解析开销会累积,但计时器已排除解析时间(仅统计 merge_constants_detailed),所以不影响基准测量精确度,但增加不必要的内存和 CPU 消耗。
Suggestion: 只解析一次输出结果(如 parse_asm(results[0][0])),并复用解析后的指令计数逻辑。

🟡 可维护性:硬编码 benchmark_type — 打印 "benchmark_type=synthetic" 硬编码在 main 中,未来若扩展为真实文件基准测试,需修改代码。
Suggestion: 通过命令行参数 --type 或函数参数获取,或从 _gen_synthetic_asm 的调用上下文推断。

🟡 可移植性:sys.path 插入sys.path.insert(0, PROJ_DIR) 会污染全局路径,可能影响其他模块导入(如 parse_asm 的依赖)。虽然 benchmark 文件常这样做,但更推荐使用 PYTHONPATH 环境变量或相对导入(如 from ..backend._asm_parser import parse_asm)。
Suggestion: 考虑使用相对导入或 pip install -e . 安装模块。

💭 类型提示遗漏 — 移除了 Optional 导入,但新增函数(如 _gen_synthetic_asm 的参数 num_instructions 等)未添加类型注解,降低了可读性。
Suggestion: 为公共函数添加完整类型提示。

💭 变量命名泛化changes_list 虽能理解,但实际存储的是 total_changes 数值,建议改为 num_changes_listchange_counts,更清晰。

💭 干扰指令硬编码寄存器 — 生成冗余 LUI 时使用了 add a4, a5, a6,这些寄存器未在 regs 列表中定义,虽不影响正确性,但若未来扩展生成逻辑(如使用随机寄存器),可能引入不一致。
Suggestion: 考虑使用 rng.choice 从更大的寄存器池中选取。


📁 benchmarks/run_benchmark.py

🔴 Missing import: OptionalBenchResult uses Optional[int] and Optional[bool] on lines 61–67 but Optional is not imported. Add from typing import Optional at the top.

🔴 asm_line_count now counts optimized asm — Line 199 overwrites asm_str with the merged output, then result.asm_line_count = len(asm_str.splitlines()) on line 246 records the optimized line count. This breaks the metric’s original meaning (codegen output size) and makes RISC-V results incomparable with other backends. Keep the original line count or add a separate field.

🟡 Inconsistent import placementmerge_constants_detailed is imported inside run_benchmark (line 202). Move to the top of the file unless there’s a circular‑import reason. It’s a minor style issue but harms readability.

🟡 Unused fields in BenchResultmachine_instructions_before/after, code_size_before/after, and output_equal are never assigned (always None). Remove them or add a comment explaining they are reserved for future toolchain integration.

💭 Column header RedLUI is ambiguous — The table prints RedLUI for redundant_lui_removed. Consider Redund.LUI or RedLUI seems fine, but be consistent with the field name for clarity.

💭 print_summary hardcodes “N/A without toolchain” — The message at line 310 is fine, but consider printing the actual values if they ever become non‑None (e.g., if r.machine_instructions_before is not None: ...).


📁 benchmarks/test_benchmark.py

🔴 测试可能失败: test_json_keeps_zero_and_na_fields 假设未设置字段有默认值0_zero_hit_result() 只设置了部分字段,而断言 candidate_pairs==0 等。若 BenchResult 未给这些字段提供默认值0,可能导致 AttributeError 或断言失败(如为 None)。请显式设置这些字段或确认类定义。

🟡 直接测试私有函数_count_asm_instructions_gen_synthetic_asm 是以下划线开头的私有函数。测试私有实现细节会让重构难度增加,建议改为测试公开接口的行为。

🟡 密度验证参数化可能遗漏总和检查test_synthetic_density_validation(0.6, 0.5) 总和为1.1,但若 _gen_synthetic_asm 只检查单个密度范围 [0,1] 而不检查总和,该参数不会触发 ValueError,测试将失败。请确保验证逻辑与测试用例一致。

💭 test_synthetic_benchmark_covers_both_rules 断言等价于实现 — 断言 instruction_reduction == merged_pairs + redundant_lui_removed 只是复制了 bench_merge 内部计算,未独立验证实际指令减少行为。考虑添加更独立的检查(如对比输入输出指令数)。

💭 test_json_keeps_zero_and_na_fields 中多余 capsys 调用capsys.readouterr() 读取后未使用其输出,可以移除以避免混淆。

💭 test_perf_pipeline 新增断言假设所有减少来自合并 — 断言 reduction == tracked_changes 假设没有其他优化导致指令减少。若管道包含其他优化步骤,该断言可能失败。建议只在明确的 const-merge 测试中保留,或使用 >= 放宽条件。


📁 docs/课题14-常量加载合并优化-开发文档初稿.md

🔴 文档目的不明确 — 标题为“开发文档初稿”,但内容大量描述“已完成实现”,前后矛盾。读者无法判断这是设计文档、开发计划还是实现总结。建议统一为“开发总结”或“实现报告”,并在开头说明文档定位。

🔴 验收标准与进度状态不一致 — 第8节验收标准中多处未勾选(如“至少3个样例通过工具链等价验证”),但第10节进度显示“工具链等价验证”为⏸ N/A,且多处声称“已完成”。应明确标注哪些是未完成项,并给出原因或计划,否则会误导后续维护者。

🟡 CLI参数命名未决 — 第2.1节提到归档课题用--merge-constants,当前实现用--const-merge,并说“是否增加旧名称别名由评审决定”。建议在文档中明确决定,或至少给出推荐处理方式,避免接口不兼容。

🟡 状态清空条件表述不清晰 — 第2.2节说“对已跟踪寄存器产生定义的普通指令使该寄存器状态失效;这不妨碍规则A对相邻lui+addi进行整体匹配”。规则A是直接匹配相邻指令,不依赖跟踪状态,但原文可能让读者混淆。建议分两句说明:规则A依赖指令顺序,不依赖跟踪状态;规则B依赖跟踪状态,普通指令会导致状态失效。

🟡 已实现步骤与计划步骤混杂 — 第5节分步实现计划中,大部分步骤标记为“已完成”,但依然保留详细的任务描述。建议将已完成步骤简化为“✅ 已完成”并引用代码位置,未完成步骤保留详细描述,避免冗余。

🟡 缺少工具链验证结果 — 第9节风险评估将“错误理解li的性能收益”列为高风险,缓解措施是“用objdump展开验证”,但文档中未提供任何objdump输出或对比结果。建议在文档中补充至少一个样例的before/after反汇编对比,或明确说明原因(如工具链不可用并标记为N/A)。

💭 第11节“第一天实际工作清单”过长 — 保留开发过程记录是好的,但包含大量具体命令和阅读笔记,占文档篇幅约1/4。建议精简为关键步骤,或移至附录。

💭 第2.1节接口描述可更精确 — 说“保持merge_constants(asm_text)”但新增merge_constants_detailed,建议用表格列出新旧接口名称、参数、返回值,避免混淆。

💭 第3.1节环境初始化包含pip install -e . — 若项目已安装,此步骤可能多余。建议注明“如尚未安装”或直接指向项目README。

💭 第7.4节Benchmark验证矩阵表述优秀 — 明确区分真实case、synthetic、工具链验证,并强调零命中必须保留,这是很好的实践,建议保持。

💭 第13节参考资料链接 — 确认链接均有效,特别是https://scratchv-compiler.github.io/ScratchV/docs/...,若为内部页面则标注“内部链接”。


📁 docs/课题14-常量加载合并优化-技术设计文档初稿.md

🔴 设计文档与实现状态混淆 — 文档多处使用“已实现结论”(0.1节)、“现已整改”(3.3节)等描述已完成状态,但标题为“技术设计文档初稿”。设计文档应当聚焦于“计划如何实现”,而非“已做了什么”。建议统一为设计阶段,将实现结论移至附录或单独的状态跟踪文档。

🔴 待评审问题缺乏作者立场 — 12节列出10个问题,但未给出任何作者倾向或建议。设计文档应体现设计决策,评审者才能针对决策提出反馈。例如“第一阶段目标是RV32还是同时支持RV64?”应明确给出建议(如RV32)并说明理由,否则评审变为无焦点讨论。

🟡 规则A匹配条件中“第二条指令不能带可作为跳转目标的标签” — 该条件正确但未定义“可作为跳转目标的标签”的判定方法。文档仅提及“数字标签、包含$的标签及无法分类的非空汇编行均作为状态边界”,但未明确如何检测标签与指令是否同行。建议补充:例如,若addi所在行解析后label字段非空,则视为带标签,不合并。

🟡 固定点迭代顺序先B后A的合理性依赖实现细节 — 6.5节描述“先应用规则B,再应用规则A”,并给出示例。但若规则B删除冗余lui后,可能使前一条lui与后续addi相邻,但规则B在扫描时会清除lui_state,导致后续规则A扫描时可能无法正确匹配?实际上,规则B删除指令后,下一次迭代(规则A)会重新扫描,所以没问题。但需明确:每次迭代内,规则B和规则A各自独立扫描整段文本,而不是在删除后立即继续同一次扫描。建议在算法描述中细化流程。

🟡 Pass顺序中const merge在scheduler之前 — 7.4节建议“调度器可能打乱相邻关系,应在常量合并之后运行”。但若调度器在之前运行,可能会破坏本来相邻的lui+addi导致合并机会减少。实际上,更常见的做法是const merge在调度之前,因为调度器可以重新排列指令,但合并不依赖调度。建议明确理由:常量合并减少指令数,调度器再优化指令顺序,这样更高效。或补充权衡:若调度器后运行,可先合并再调度,但调度器可能引入新的相邻lui+addi?不现实。当前顺序合理,但建议补充一句说明为什么不是相反顺序。

💭 文档中日期为未来时间(2026-07-28) — 可能是占位符,建议改为实际编写日期或使用“YYYY-MM-DD”格式表明是模板。若未实际编写,应标注“草案”。

💭 3.3节“历史基线问题”过于详细 — 列出7个问题并声称“现已整改”,但设计文档应关注当前设计如何规避这些问题,而非详细罗列旧代码的不足。可精简为2-3句背景,或移至附录。

💭 4.2节计算final_s32的代码 — 使用final_u32 if final_u32 < 0x80000000 else final_u32 - 0x100000000,但Python中0x80000000作为整数是正数,条件< 0x80000000会排除0x80000000本身,导致0x80000000被错误映射为-2147483648。正确应为<=& 0x7FFFFFFF。建议修复:final_s32 = final_u32 if final_u32 < 0x80000000 else final_u32 - 0x100000000 应改为 final_s32 = final_u32 - 0x100000000 if final_u32 >= 0x80000000 else final_u32

💭 9.1节A.汇编解析缺少对伪指令的处理 — 例如linopmv等伪指令可能在输入中出现,但解析器需要能正确识别并跳过(不优化)。文档仅提到“未知操作码保守处理”,但伪指令常见,建议明确定义对于伪指令的处理方式:保留原样,不尝试合并或消除。

💭 10.2节“禁止修改真实case以人工插入lui+addi” — 正确,但可在benchmark设计部分补充:若真实case零命中,应提供人工构造的集成测试(如9.3节用例)来验证优化器功能,而非依赖真实case。


📁 scratchv/backend/_asm_parser.py

🟡 to_asm() 空行行为改变 — 原来返回 "" 现在返回 self.raw。如果 self.raw 是空白字符(如 \n),输出将保留空行,而之前会跳过。请确认下游是否需要保留空行,或者是否应保持原行为。

🟡 canonical_regx05 等带前导零的写法不识别 — 正则 x([0-9]|[12][0-9]|3[01]) 不匹配 x05(两个数字)。虽然 RISC-V 规范通常不接受前导零,但有些汇编器允许。如果此类输入可能出现,建议扩展正则(如 x0?[0-9] 等)或明确拒绝。

💭 _looks_like_reg 中重复 strip/lower — is_integer_reg(s.strip().lower()) 内部已经做 strip 和 lower,可简化成 is_integer_reg(s),但无功能影响。


📁 scratchv/backend/const_merge.py

🔴 Bug: _parse_imm 改变了 010 等数字的语义
int(text, 0) 会将前导零的字符串解释为八进制(如 "010" → 8),而原代码将其视为十进制(10)。RISC-V 汇编中常见前导零的十进制数,此变更会导致立即数解析错误,破坏兼容性。建议恢复原来的显式进制判断,或明确只支持 0x 前缀,拒绝其他非十进制前缀。

🔴 Bug: 冗余 LUI 删除中,立即数解析失败的 LUI 未清除寄存器状态
_parse_lui_imm(inst.operands[1]) 返回 None 时,代码仅 result.append(inst)continue,未更新 lui_state。这导致该 LUI 写入的寄存器仍保留之前的已知值,后续相同寄存器的 LUI 可能被错误删除。例如:

lui x5, 0x100000   # 无效立即数,但实际写入 x5
lui x5, 0x12345    # 由于 state 仍为旧值,此条可能被误删

解决:至少应 lui_state.pop(rd, None) 清除该寄存器状态。

🟡 统计信息 candidate_pairs 可能显著高估
_count_merge_candidates 只检查 opcode 和操作数数量,不验证寄存器相等性或立即数有效性。这会导致报告的数字远高于实际可合并的 merged_pairs,可能误导用户。建议根据实际合并条件统计,或明确说明这是“结构候选”。

💭 合成的 li 指令丢失原始行号上下文
合并后的 ParsedAsmLine 使用了 lui.lineno,但后续的中间注释行(insts[i+1:j])保留了它们自己的行号。如果调试或反汇编依赖行号映射,可能产生偏差。不过对于纯优化场景影响较小。


📁 scratchv/compiler.py

🟡 Potential AttributeError if merge_constants_detailed returns None — Line 454: stats = merge_constants_detailed(asm_text) then stats.total_changes. If the function can return None (e.g., on error), accessing .total_changes will crash. Consider adding a guard or ensuring the function always returns a valid stats object.

🟡 Missing type hint for stats — The returned object's shape is implicit. Adding a typed NamedTuple or dataclass for stats would improve clarity and catch attribute errors early.

💭 F-string cross‑line formatting — The warning message spans multiple lines inside parentheses. Consider using implicit string concatenation (e.g., f"Const merge: {stats.total_changes} changes " f"({stats.merged_pairs} pairs, " f"{stats.redundant_lui_removed} redundant lui)") to avoid the extra indentation and make it easier to read.


📁 scratchv/simulator/tinyfive.py

🟡 建议:父类初始化可能遗漏StubProfiledMachine.__init__ 完全跳过了 super().__init__()。如果父类未来添加了其他状态(如 self._cache),Stub 会丢失它。建议显式调用 super().__init__(),再覆盖 _available_m 等属性,或确保父类 __init__ 没有副作用。

🟡 建议:run 中冗余的 getattrwords = getattr(self, '_code_words', []) 多余,因为 __init__ 已定义该属性,直接使用 self._code_words 即可。

🟡 建议:_code_words 命名易误导load_asm 只追加 0 用于计数,并未存储实际指令字。建议重命名为 _instruction_count_code_word_count,或存储真实指令以便后续扩展。

💭 小意见:read_mem_i32 未检查对齐 — 假设地址是 4 字节对齐,但未验证。非对齐读取可能返回错误数据。可考虑添加断言或文档说明。

💭 小意见:load_asm 中的 import 位置from scratchv.backend._asm_parser import parse_line 放在方法内部,建议移至文件顶部,避免重复导入开销。

💭 小意见:set_reg 对非法索引静默忽略 — 当 idx 为负数或大于等于 32 时,函数默默返回,不给出任何提示。可考虑抛出 IndexError 以帮助调试。



⚠️ 未审查的文件

  • tests/test_backend.py
  • tests/test_const_merge.py
  • tests/test_simulator.py

@yuki-328
yuki-328 force-pushed the feature/const_merge branch from a04a5a8 to 179f885 Compare August 15, 2026 09:32
@yuki-328 yuki-328 changed the title topic14-docs feat: complete safe RV32 constant-load merge optimization Aug 15, 2026
def get_reg(self, idx: int) -> int:
"""Read signed 32-bit register value."""
if not self._available:
if not self._available or not 0 <= idx < 32 or idx == 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认这里的改动是什么需求还是bug引起的

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