课题14:常量加载合并优化

难度:低 | 类型:项目实战 | 源文件scratchv/backend/const_merge.py | 行数:~300 状态:✅ 已完成


概述

优化RISC-V加载大常量的指令序列,将lui + addi对合并为单条li伪指令,并消除冗余lui


理解背景

是什么?

常量加载合并(const_merge.py)在汇编层面优化 RISC-V 加载 32 位常量的指令序列。RISC-V 要加载一个 32 位常数需要两条指令(lui + addi),这个优化器做了两件事:

  1. lui+addi 合并:把相邻的 lui rd, hi + addi rd, rd, lo 合并为一条 li rd, full_value
  2. 冗余 lui 消除:删除连续两次加载相同高位立即数的 lui

为什么?

RISC-V 指令都是 32 位定长,无法在一条指令里编码 32 位立即数。所以加载常数需要:

lui t0, 0x12345     # t0 = 0x12345 << 12 = 0x12345000
addi t0, t0, 0x678  # t0 = t0 + 0x678 = 0x12345678

编译器简单翻译时可能产生不必要的冗余。这个优化器在汇编生成后做"post-pass"清理。

核心概念

RISC-V 32 位常数加载
最终值 = (imm_hi << 12) + sign_extend_12(imm_lo)

关键细节:addi 的 12 位立即数会被符号扩展0x800-2048(不是 +2048)。

两遍优化

Pass 1: lui+addi 合并

lui t0, 0x12345     →    li t0, 0x12345678
addi t0, t0, 0x678

条件:两条指令相邻,目标寄存器相同,addi 源寄存器与 lui 目标相同。

Pass 2: 冗余 lui 消除

lui t0, 0x12345     →    (删除)
... (t0 未修改)
lui t0, 0x12345          lui t0, 0x12345(第一次已加载)

详细任务
  1. 理解lui(加载高20位)和addi(加低12位,注意符号扩展)构成32位常量的机制。
  2. 识别连续两条指令:lui rd, imm_hi后跟addi rd, rd, imm_lo,计算最终常量值。
  3. 替换为一条li rd, final_value(如果后端支持li),否则保留但减少一条指令。
  4. 检测冗余lui:同一个rdlui在之前出现过且中间未修改,则删除后面的lui,调整addi的源寄存器。
  5. 实现迭代扫描,统计节省的指令数。
  6. 集成到后端,添加--merge-constants开关。

交付产物
  • 优化脚本或模块
  • 测试汇编文件(包含各种常量值)
  • 文档:算法原理、使用示例

代码走读

使用
from scratchv.backend.const_merge import merge_constants

asm = """
    lui t0, 0x12345
    addi t0, t0, 0x678
    lui t0, 0x12345
    addi t0, t0, 256
"""

optimized, changes = merge_constants(asm)
print(optimized)
print(f"Changes: {changes}")

命令行
python -m scratchv.backend.const_merge input.s -o output.s -v

合并逻辑
def _merge_lui_addi(self, lines):
    """找到相邻的 lui+addi 对并合并"""
    for i in range(len(lines) - 1):
        if lines[i].opcode == "lui" and lines[i+1].opcode == "addi":
            if lines[i].operands[0] == lines[i+1].operands[0]:  # 同目标
                if lines[i+1].operands[1] == lines[i].operands[0]:  # addi 读同一寄存器
                    hi = int(lines[i].operands[1], 16) << 12
                    lo = self._sign_extend_12(int(lines[i+1].operands[2]))
                    full = (hi + lo) & 0xFFFFFFFF
                    lines[i] = AsmLine(opcode="li", operands=[
                        lines[i].operands[0], str(full)
                    ])
                    lines[i+1] = None  # 标记删除

符号扩展处理
def _sign_extend_12(self, imm):
    if imm & 0x800:   # 第11位是1 → 负数
        return imm - 0x1000
    return imm

动手练习

练习 1: 观察合并效果

编译 CNN 模型,对输出汇编运行 const_merge,统计合并了多少对 lui+addi。

练习 2: 手动计算

手工计算 lui t0, 0x12345 + addi t0, t0, 0x800 的最终值(注意符号扩展)。

练习 3: 扩展优化

添加对 li 后紧跟 addi 的合并(li x, N; addi x, x, Mli x, N+M)。


常见坑
说明
符号扩展陷阱 addi 的 12 位立即数会被符号扩展,0x800 = -2048,不是 +2048
寄存器被修改 两次 lui 之间如果目标寄存器被其他指令修改了,就不能消除
li 是伪指令 li rd, imm 是汇编器伪指令,实际可能被展开为 lui+addi 或单条 addi

进阶阅读

12周每周目标
  • W1:学习RISC-V加载大常量的机制,手动拆解一个32位常量(如0x12345678)。
  • W2:编写汇编解析函数,识别luiaddi指令,提取目标寄存器和立即数。
  • W3:实现合并检测:判断连续两条指令是否构成lui+addi对,计算最终常数值(处理符号扩展)。
  • W4:实现替换:删除原两条,插入li rd, final_value(若后端支持),否则保留原指令但添加注释。
  • W5:处理冗余lui:扫描中记录每个寄存器的最后一次lui值,若重复则删除后面lui
  • W6:实现合并优化函数,扫描整个汇编文件,迭代应用直到没有变化。
  • W7:测试各种常量值(正数、负数、边界0x80000000),用模拟器验证结果相同。
  • W8:增加优化报告:显示合并的对数、删除的冗余lui数、节省指令数。
  • W9:集成到编译器后端(在代码生成后执行),添加--merge-constants开关。
  • W10:处理特殊情况:addi使用的寄存器不是lui的目标(如lui x1; addi x2, x1),谨慎合并。
  • W11:扩展支持跨基本块复用(简单版本)。
  • W12:撰写文档,包含常量拆分与合并的数学原理。