必须用llvm-exegesis实测带依赖链的端到端延迟,再按硬件资源、操作数宽度和寄存器类在*Schedule.td中精确建模ProcResource与WriteLatency,否则调度模型会误导llvm-mca和代码生成。

直接在 Cpu0Schedule.td 或对应目标的 *Schedule.td 里写 TableGen 调度模型,不是填数字那么简单——你得先测出真实延迟,再反推资源约束,否则模型越“精确”越误导 llvm-mca 和最终生成代码。
为什么不能照搬手册写 SchedModel
手册上写的 ADDrr 延迟是 1 cycle,但实测在你的 CPU 上可能是 2 cycle(因为微码更新后端口重映射了),或者在特定操作数组合下变成 3 cycle(比如源/目标寄存器别名触发 micro-op fusion 拆分)。llvm-exegesis 能暴露这些细节,而 TableGen 里硬编码的 WriteLatency = 1 会让整个调度链路误判关键路径。
- 手册数据滞后:AVX-512 指令在不同 stepping 的 Intel CPU 上端口分配可能完全不同,
llvm-exegesis -mode=latency才能拿到当前芯片的真实值 - 依赖关系影响延迟:同一个
MOV64rr,前一条指令写RAX、本条读RAX,和前后完全无关,测出来延迟差一倍以上 - 寄存器重命名绕过伪依赖:
XOR32rr %reg, %reg后接ADD32rr %reg, %other,手册标延迟 1,实测为 0(重命名直接消除了依赖)
怎么用 llvm-exegesis 测出可信的延迟值
别用默认参数跑,它默认只测“无依赖”场景,而调度模型真正要建模的是有依赖链的端到端延迟。关键命令是:
llvm-exegesis -mode=latency -opcode-name=ADD32rr -uops=2 -iterations=100000
其中 -uops=2 强制构造一条两指令依赖链(比如 MOV32rr %rax, %rbx; ADD32rr %rax, %rcx),这样测出来的才是调度器真正需要的 WriteLatency。
- 对每条关键指令(ALU、load、store、branch、vector)都跑一遍带依赖链的测试
- 注意区分
latency(端到端延迟)和throughput(吞吐量),TableGen 中WriteRes描述后者,WriteLatency描述前者 - 测完用
llvm-exegesis -mode=report生成 CSV,挑出 median 值,别用平均值(受噪声干扰大)
*Schedule.td 里怎么写才不翻车
TableGen 不是配置文件,它是编译期生成 C++ 调度逻辑的 DSL。一个常见错误是把所有指令都塞进同一个 SchedRW,结果 llvm-mca 报 Resource pressure too high 却找不到源头。
- 每个功能单元(如 ALU0、ALU1、AGU)必须定义独立的
ProcResource,不能共用 ID -
SchedWriteRes必须严格匹配硬件资源:比如你的 CPU 有 2 个整数 ALU,就得写两个ProcResource并在SchedWriteRes中显式指定使用哪个 -
WriteLatency要按操作数宽度和寄存器类拆分:例如ADD32rr和ADD64rr延迟不同,就得定义两个独立的WriteRes条目 - 别漏掉
ReadAdvance:load 指令如果支持地址生成提前完成(如 LEA-like behavior),得在这里声明,否则调度器会多等 cycles
验证模型是否生效的最快方式
改完 *Schedule.td 后,不要急着跑完整编译,先用 llvm-mca 快速验证:
llc -march=yourtarget -mcpu=yourcpu -o - input.ll | llvm-mca -mcpu=yourcpu -timeline -iterations=10
重点看输出里的 Timeline view 是否出现预期的指令并行(比如两条独立 ADD 真的被调度到同一 cycle),以及 Resource pressure 数值是否与你定义的 ProcResource 容量一致。
最容易被忽略的是:TableGen 编译后生成的 C++ 代码不会自动 reload,每次改完 *Schedule.td 都必须重新 build libLLVMYourTargetCodeGen.a,否则 llc 用的还是旧模型——这个步骤没有报错提示,静默失效。

















