类型合法化处理数据“长什么样”,即确保sdnode操作数类型被目标平台原生支持;操作合法化处理“想干什么但硬件不认”,即在类型合法前提下,将不支持的操作码转换为等效指令序列。

类型合法化处理的是数据“长什么样”
类型合法化(Type Legalization)关注的是 SelectionDAG 中每个 SDNode 的操作数类型是否被目标平台原生支持。比如 RISC-V 32 位后端不支持 i1(1 位整数)或 v4i1(4 元素布尔向量),也不支持 f16(半精度浮点)——这些类型在硬件上没有对应寄存器或 ALU 支持,就必须改。
常见转换方式包括:
- 提升(Promotion):把
i1→i8或i32,再用掩码/截断模拟布尔语义 - 拆分(Splitting):把
i128拆成两个i64节点,配合进位链处理 - 标量化解(Scalarization):把
v2i64拆成两个独立的i64节点(当目标无向量 ALU 时) - 向量化(Widening):把
v8i8扩展为v2i32,再用 shuffle + 整数运算模拟(较少见,多用于定制后端)
它发生在操作合法化之前,因为只有类型“站得住脚”,后续才好判断“这个操作能不能做”。TargetLowering::getTypeAction() 和 getSetCCResultType() 这类函数就是干这事的。
操作合法化处理的是“想干什么但硬件不认”
操作合法化(Operation Legalization)解决的是:类型已经合法了,但某个 SDNode 的操作码(如 ISD::CTPOP、ISD::BSWAP、ISD::SINT_TO_FP)在目标平台上没有直接对应指令。这时不能报错,得“绕着走”。
典型应对策略有三种:
-
扩展(Expansion):用多个基础指令拼出原语义,例如把
ISD::CTPOP展开为一系列AND、SHL、ADD(常用于无 popcnt 硬件的 RISC-V 或 MSP430) -
提升(Promotion):把
ISD::SINT_TO_FP(i16→f32)先转成i32,再调用f32版本 intrinsic -
定制(Custom):注册
TargetLowering::LowerOperation()钩子,在 C++ 层手动构造 DAG 子图,比如把ISD::ATOMIC_LOAD映射为带lr.w/sc.w循环的 RISC-V 序列
注意:操作合法化不改变类型本身,只替换或重写 SDNode。如果某操作既非法类型又非法操作(比如 v4i1 上做 CTPOP),类型合法化会先把它变成 v4i8,之后操作合法化再决定怎么算 popcount。
RV64LegalI32 编译选项实际影响哪一层
RV64LegalI32 是一个 RISC-V 后端特有的开关,它控制的是类型合法化策略,不是操作合法化。
它的作用是:在 RV64(64 位地址/寄存器宽度)目标上,**强制将所有 i32 类型视为“非法”并提升为 i64**。这会导致:
- 所有
i32load/store 变成i64指令 + 隐式截断(可能引入额外ADDI或SLLI/SRLI) -
ISD::ADD、ISD::MUL等操作全走 64 位路径,哪怕原始 IR 是int - ABI 参数传递仍按 LP64 规则(
i32仍用 32 位寄存器传),但内部计算全程升格 —— 这正是它容易引发性能退化或行为差异的原因
它不干预 ISD::SDIV 是否合法,也不决定 ISD::CTLZ 是用硬件指令还是展开;那些由操作合法化策略或 TableGen 中 Pat 模式匹配控制。
调试时最该盯住的两个地方
当你遇到生成代码异常、指令未命中或性能突降,优先检查:
- 用
llc -debug-only=legalizer看类型/操作合法化日志,确认i1、v2f16等是否被意外提升或拆分 - 用
llc -view-dag-combine1-dags或-view-isel-dags查看合法化前后的 SelectionDAG 图,对比ISD::节点是否被替换成ISD::BITCAST、ISD::TRUNCATE或自定义SDNode
最容易被忽略的是:合法化不是一次性动作,而是在 DAG 合并(DAGCombine)、指令选择(Instruction Selection)之间反复迭代的。一次 LowerOperation 插入的新节点,可能立刻触发新一轮类型检查 —— 所以钩子里加的节点必须自身类型合法,否则会陷入死循环或断言失败。











