课题14:常量加载合并优化
难度:低 | 类型:项目实战 | 源文件:
scratchv/backend/const_merge.py| 行数:~300 状态:✅ 已完成
概述
优化RISC-V加载大常量的指令序列,将lui + addi对合并为单条li伪指令,并消除冗余lui。
理解背景
是什么?
常量加载合并(const_merge.py)在汇编层面优化 RISC-V 加载 32 位常量的指令序列。RISC-V 要加载一个 32 位常数需要两条指令(lui + addi),这个优化器做了两件事:
- lui+addi 合并:把相邻的
lui rd, hi+addi rd, rd, lo合并为一条li rd, full_value - 冗余 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)
最终值 = (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(第一次已加载)
详细任务
- 理解
lui(加载高20位)和addi(加低12位,注意符号扩展)构成32位常量的机制。
- 识别连续两条指令:
lui rd, imm_hi后跟addi rd, rd, imm_lo,计算最终常量值。
- 替换为一条
li rd, final_value(如果后端支持li),否则保留但减少一条指令。
- 检测冗余
lui:同一个rd的lui在之前出现过且中间未修改,则删除后面的lui,调整addi的源寄存器。
- 实现迭代扫描,统计节省的指令数。
- 集成到后端,添加
--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: 观察合并效果
lui(加载高20位)和addi(加低12位,注意符号扩展)构成32位常量的机制。lui rd, imm_hi后跟addi rd, rd, imm_lo,计算最终常量值。li rd, final_value(如果后端支持li),否则保留但减少一条指令。lui:同一个rd的lui在之前出现过且中间未修改,则删除后面的lui,调整addi的源寄存器。--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: 观察合并效果
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: 观察合并效果
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: 观察合并效果
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, M → li x, N+M)。
常见坑
| 坑 | 说明 |
|---|---|
| 符号扩展陷阱 | addi 的 12 位立即数会被符号扩展,0x800 = -2048,不是 +2048 |
| 寄存器被修改 | 两次 lui 之间如果目标寄存器被其他指令修改了,就不能消除 |
| li 是伪指令 | li rd, imm 是汇编器伪指令,实际可能被展开为 lui+addi 或单条 addi |
进阶阅读
- RISC-V 指令集手册:第 2.4 节(Immediate Encoding)
- 相关 topic: 课题13 — 窥孔优化器 | 课题12 — 指令计数统计器
12周每周目标
- W1:学习RISC-V加载大常量的机制,手动拆解一个32位常量(如
0x12345678)。
- W2:编写汇编解析函数,识别
lui和addi指令,提取目标寄存器和立即数。
- 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:撰写文档,包含常量拆分与合并的数学原理。
- W1:学习RISC-V加载大常量的机制,手动拆解一个32位常量(如
0x12345678)。 - W2:编写汇编解析函数,识别
lui和addi指令,提取目标寄存器和立即数。 - 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:撰写文档,包含常量拆分与合并的数学原理。