LLVM RISC-V 后端:从 IR 到 MIR 再到汇编

“The Analytical Engine weaves algebraical patterns,

just as the Jacquard loom weaves flowers and leaves.”

“分析机编织代数的花样,正如雅卡尔织机编织花与叶。”

(Ada Lovelace《分析机概要》注 A,1843)

“9 月 11 日”

三点四十分。

水塔的影子刚好盖住半张长椅。

蝉还在叫,只是叫得比七月敷衍。

我偶尔会思考这种事情。

比如,一张已经印好的座位表,和一张还没印的座位表,究竟哪一张比较自由。

这个问题我想了三年,还是没有想出答案。

贩卖机的罐装咖啡是一百三十円。

旭座上个月关了门,退票窗口的玻璃上贴着一张纸,说座位表可以自取。

我就取了一张。

对号入座,全场一百四十四个格子,七排十二号被人用铅笔圈过。

不是我的字。

水科:「你在看什么?」

我:「座位表……」

水科:「那间电影院已经没有了……」

我:「嗯!」

水科:「没有了的电影院,座位表还有什么用?」

我:「垫罐子……」

我把那张纸折成四折,压在罐子底下。

纸角被水珠洇软了一小块。

カン、

罐子磕在长椅的铁扶手上,声音很干。

水科把笔记型电脑摊在膝盖上,屏幕转过来朝着我。

上面只有一行字。

llc -mtriple=riscv64

水科:「上一篇你听懂了多少?」

我:「……一半……」

水科:「哪一半?」

我:「改写合不合法的那一半……」

水科:「那叫精化……」

我:「对,就是那个……」

水科:「这一篇的判据不止一条……」

我:「还有哪一条?」

水科:「机器认不认?」

我:「机器不认,会怎么样呢?」

水科:「不会怎么样。编不出来而已」

影子。

水塔的影子又往东挪了两公分,罐子底下那张纸露出来一个角。

我:「就像我这张座位表……」

我:「票卖出去之前,七排十二号可以是任何人的」

我:「卖出去之后呢?」

水科:「之后就不重开了……」

我:「不重开?」

水科:「后面的人只能在不动它的前提下,安排自己那一排」

我:「……」

我:「水科,你今天讲话很像编译器……」

水科:「编译器比我诚实。它至少会说 Cannot select……」

第一章 Not Explained Here 「不解释的那些名词」

“9 月 11 日”

三点五十分。

水塔的影子刚好盖住整张长椅。

蝉还在叫,只是叫得比七月敷衍。

贩卖机的冰咖啡卖完了,只剩热的。

九月喝热的罐装咖啡,是一种很小的自我惩罚。

我:「刚才那个,能再说一遍吗?」

水科:「哪一个?」

我:「机器认不认,那一句……」

水科:「上一篇文章谈中端,谈的是一次改写合不合法……」

水科:「给定 IR 和它的语义,某个 pass 能不能把它改成另一种写法」

水科:「上篇管这条判据叫精化……」

我:「啊、」

我:「嗯……」

水科:「到了后端,判据不止一条……」

我:「还有哪一条?」

水科:「改写还必须真的能在目标机器上执行」

我:「机器有它的脾气呢?」

水科:「机器没有脾气。机器只有它有的东西……」

我:「那些故意不定的东西,具体是哪些?」

水科:「五件……」

水科:「整数宽度比任何一台机器都多,寄存器无限个,名字随便取……」

水科:「地址是一个可以随意运算的整数,跳转范围不受限……」

我:「那、那个」

我:「嗯、」

水科:「常量想多大就多大」

我:「听起来很富裕呢?」

水科:「富裕……」

水科:「这些不定,正是中端能优化的原因……」

水科:「也正是后端必须解决的问题……」

不定,是一种富裕。

那么定下来,是贫穷吗。

或者说,只有定下来的那种富裕,才真的能被执行。

……啊哈哈,我在想什么。罐子已经空了。

我:「后端的任务,一句话是什么?」

水科:「给 IR 里每一处目标无关的选择」

水科:「补上目标相关的具体选择,直到机器指令能被编码为止」

我:「那还是两句话吧?」

水科:「一句……」

水科:「补选择的过程分四步,每一步落定一类可能性……」

水科:「某一步定下的选择,后面不再重开」

我:「啊、」

我:「一点都不重开?」

水科:「只允许在不推翻它的前提下做局部改写……」

我:「哪些改写算不推翻?」

水科:「第二章那张表,逐行写了……」

为什么定下来的选择不能重开。

因为重开就要把前面所有步再算一遍。

那么再算一遍,算出来的一定是同一个结果吗。

如果不是同一个,第一次定下的那个,又算是什么。

……

我:「这一篇的例子都跑在哪台机器上?」

水科:「主线是 llc -mtriple=riscv64」

水科:「RV32 的差异单独标出」

我:「代码是哪一版?」

水科:「llvm-project 的 026e3f3c……」

水科:「跟上篇同一个 commit……」

水科:「文中每一处 file.h:line 都指向它」

我:「前端语义呢?」

水科:「不涉及」

我:「中端优化呢?」

水科:「也不涉及……」

我:「那你打算从哪里讲起?」

水科:「从代码」

水科:「所以不按先讲后端框架、再讲各阶段算法的顺序写」

水科:「先把后端要做的四类选择一次讲完,后面每章展开一类……」

我:「等一下……」

水科:「什么?」

我:「有些名词你已经用过了……」

水科:「上篇定义过的,本文不再解释……」

我:「哪些?」

水科:「pass,还有 New Pass Manager……」

水科:「本文写作 NewPM」

我:「用在哪?」

水科:「第二章,legacy 与 NewPM 两条流水线的对比……」

我:「SSA、φ 节点、CFG 呢?」

水科:「上篇 T2,用在机器级 SSA」

水科:「和最后一章的 MachineVerifier……」

我:「格、不动点、worklist、有限高度格」

水科:「上篇 T1……」

水科:「两处停机论证」

水科:「类型合法化一处,vsetvli 插入一处……」

我:「精化和 nsw?」

水科:「上篇 T0,引理 0.6」

水科:「引子的判据对比,还有寄存器分配之前那些 MIR 标记……」

我:「GEP 呢?」

水科:「上篇引子的通用术语表,正式使用处在 T5」

水科:「sum 那个例子里那条 getelementptr……」

我:「上篇剩下的部分呢?」

水科:「T3 的遍历序与自然循环……」

水科:「T4 的 SCEV 与退出次数」

水科:「T5 的 MemorySSA 与别名分析……」

水科:「T6 的频度与代价,T7 的规范化,本文用不到,不必先读……」

我:「图论那边呢?」

水科:「本文只用到有向图的前驱和后继这一层最基础的说法……」

水科:「不依赖具体词条」

我:「上篇用了四条吧?」

水科:「DFS、支配树、强连通分量、拓扑排序……」

水科:「那四条留在上篇,本文一次都不用……」

我:「那这一篇要从零讲清的有多少?」

水科:「很长」

我:「多长?」

水科:「后端流水线的阶段划分」

水科:「legacy pass manager 与 NewPM 两条路……」

水科:「TargetMachine 与 Subtarget……」

水科:「DataLayout……」

水科:「ABI 与调用约定,寄存器类,SelectionDAG……」

我:「不是……」

我:「那个……」

水科:「指令选择,机器指令与 MIR……」

水科:「虚拟寄存器与物理寄存器,寄存器分配与溢出……」

水科:「伪指令」

我:「那个……」

水科:「AsmPrinter 与 MC 层,汇编器……」

水科:「重定位,链接期松弛」

我:「……」

我:「这些名词第一次出现在哪里,就在哪里定义?」

水科:「就地定义……」

水科:「后文再用到的时候不再展开……」

我:「一个都不重讲?」

水科:「有五个例外……」

水科:「PIC、GOT、代码模型、链接期松弛、Zmmul」

水科:「跨了好几节,第二次用到的时候把简写重述一遍……」

我:「PIC 是什么的简写?」

水科:「现在不告诉你」

我:「为什么?」

水科:「因为第四章才用得到……」

我:「RVV 为什么单独一章?」

水科:「因为它引入了前面几章都没有的东西」

我:「什么东西?」

水科:「长度在编译期未知的寄存器组……」

我:「……」

我:「就像印座位表的时候,还不知道剧场有多大……」

水科:「差不多」

我:「差不多?」

水科:「剧场的大小,写的是一个范围……」

罐子空了之后,重量变得很轻。

我把那张座位表从罐子底下抽出来。

纸角上的水渍干了,留下一圈淡淡的印子。

七排十二号那个铅笔圈,还在。

第二章 Two Pipelines 「四条不能重发的票」

“9 月 11 日”

四点二十分。

水塔的影子刚好盖住整张长椅,还多出来一截。

蝉还在叫,只是叫得比七月敷衍。

我:「热的也买了……」

水科:「九月喝热的」

我:「贩卖机只剩这个了……」

水科:「那叫没得选……」

我:「没得选也算选?」

水科:「算第 1 类。整个函数一次定死……」

我:「那张表,现在可以看了吗?」

水科:「哪张?」

我:「你说逐行写了的那张……」

水科:「四行,第一行,要选的是指令集子集和 ABI……」

水科:「选完的结果叫 Subtarget」

我:「谁来选?」

水科:「机器配置。-march、-mabi、-mcpu」

我:「选完还能不能改?」

水科:「不能。整个函数一次定死……」

我:「一次定死……」

水科:「第二行,用哪条机器指令,数据放哪类寄存器……」

水科:「结果是机器指令序列,MIR,里面是虚拟寄存器」

我:「谁来选?」

水科:「指令选择。输入是一个基本块的 IR……」

我:「这一行能改吗?」

水科:「原则上不再重选」

水科:「只允许保持语义的局部改写,压缩、配对拆分……」

我:「第三行呢?」

水科:「每个虚拟值放哪个物理寄存器……」

水科:「结果是物理寄存器,或者栈槽」

我:「谁来选?」

水科:「寄存器分配。输入是 MIR 与活跃区间……」

我:「能改吗?」

水科:「不能。此后一切按物理寄存器写……」

我:「第四行……」

水科:「每条指令编成哪些字节,跨模块引用留什么记号……」

水科:「结果是汇编,或者目标文件」

我:「谁来选?」

水科:「MC 层。AsmPrinter 降出来的 MCInst……」

我:「这一行总能改了吧?」

水科:「只有汇编器与链接器能改」

我:「……」

我:「四类选择对应四章?」

水科:「机器配置、指令选择、寄存器分配、汇编与目标文件……」

我:「ABI 是什么的简写?」

水科:「application binary interface」

我:「具体管什么?」

水科:「函数之间怎么传参,寄存器的用途怎么分,数据怎么对齐……」

水科:「具体选哪一套,由 -mabi 决定……」

我:「调用约定为什么夹在中间?」

水科:「它在指令选择与寄存器分配之间,它决定寄存器文件里的哪些成员能作什么用……」

水科:「同时约束选哪条指令,和哪个虚拟值放哪个物理寄存器」

我:「四步之间是什么关系?」

水科:「单向」

水科:「第一步定下的 Subtarget,决定第二步能选哪些指令……」

水科:「第二步定的寄存器类,决定第三步能分到哪些物理寄存器」

水科:「第三步分完寄存器之后」

我:「够了」

我:「罐子空了……」

水科:「再做指令级优化就必须照顾已经定下的寄存器边界……」

我:「每一步都是拿前一步的输出当输入?」

水科:「把不确定性继续落定……」

我:「落定……」

我:「就像发票……」

我:「第一张发出去,第二张才能印……」

水科:「印错了也不重印」

我:「不重印?」

水科:「只在不动第一张的前提下,改第二张的边……」

长椅的另一头,有人坐下来了。

我不知道她是什么时候坐下来的。

三日月:「票已经发出去了……」

我:「诶,你听到了?」

三日月:「发出去的东西,只是还没有被收回……」

她把手里的纸对折了一次,没有打开。

水科:「流水线有两条路」

我:「两条?」

水科:「默认那条是 legacy pass manager……」

水科:「New Pass Manager 之前那套 pass 调度框架」

水科:「llc 至今默认走它,下文简称 legacy 路径……」

我:「……」

我:「代码在哪?」

水科:「RISCVPassConfig」

水科:「RISCVTargetMachine.cpp 的 385 行……」

我:「另一条呢?」

水科:「-enable-new-pm=1」

水科:「走 RISCVCodeGenPassBuilder.cpp 的 37 行」

我:「阶段划分一样吗?」

水科:「相同……」

水科:「但那条路上只移植了一部分……」

我:「没移植的怎么办?」

水科:「逐条写在同一个文件的 TODO 里……」

水科:「按段分……」

我:「pre-RA 那一段呢?」

水科:「从 128 行起到 138 行……」

水科:「RISCVMergeBaseOffset……」

水科:「RISCVZilsdOptimizer 的预分配半……」

水科:「RISCVInsertReadWriteCSR」

三日月:「呵呵……」

水科:「RISCVInsertWriteVXRM……」

我:「……」

水科:「RISCVVMV0Elimination,都在这段……」

我:「post-RA 呢?」

水科:「143 行的 RISCVRedundantCopyElimination」

我:「还有吗?」

水科:「RISCVLoadStoreOpt 在 152 行」

水科:「sched2 之前……」

水科:「发射前几批从 164 行起」

水科:「含 171 行的 RISCVMakeCompressible……」

我:「全都老实记在 TODO 里?」

水科:「还有两件连 TODO 都没有」

我:「哪两件?」

水科:「向量值的分配,和 RISCVInsertVSETVLI……」

水科:「只挂在 legacy 的两个分配钩子里」

我:「哪两个?」

水科:「addRegAssignAndRewriteOptimized」

水科:「的 458 和 460 行……」

水科:「addRegAssignAndRewriteFast 的 449 和 450 行」

水科:「NewPM 路径上没有等价物……」

我:「那么用 -enable-new-pm=1 编向量代码,会怎么样?」

水科:「不会经过 vsetvli 插入」

我:「……」

我:「不报错?」

水科:「不报错」

我:「这不是很容易踩到吗?」

水科:「很容易,构造函数里记了一处更小的……」

我:「多小?」

水科:「-riscv-enable-sink-fold 传不进 NewPM,恒为默认值」

水科:「49 行……」

我:「那整条流水线,从头到尾长什么样?」

水科:「从 IR 开始……」

水科:「先是 IR 级收尾」

水科:「AtomicExpand、GatherScatterLowering 那些」

水科:「加上 IR 层的 CodeGenPrepare……」

水科:「467 行和 511 行……」

我:「嗯」

我:「然后?」

水科:「指令选择。SelectionDAG 变成 MachineInstr」

水科:「MIR,虚拟寄存器,517 行」

我:「再然后呢?」

水科:「机器 SSA pass……」

水科:「MachineLICM、CSE、Sinking」

水科:「RISCVOptWInstrs,向量 peephole」

我:「是、」

水科:「612 行,然后是伪指令展开的 pre-RA 那一批」

水科:「和寄存器分配,greedy……」

我:「诶?」

水科:「RVV 另有单独分配器……」

水科:「636 行和 433 行……」

我:「分配之后呢?」

水科:「调度前一批。post-RA 伪指令展开、访存配对」

水科:「在 sched2 之前,556 行……」

水科:「发射前一批。复制传播、分支松弛、压缩指令……」

我:「嗯、」

水科:「565 行……」

水科:「发射前二批。move merge、push/pop、atomics 伪指令展开」

水科:「584 行,最后是 AsmPrinter」

水科:「降成 MCInst,出汇编或者目标文件……」

我:「啊、」

我:「那些行号都在同一个文件里?」

水科:「都在 RISCVTargetMachine.cpp 里……」

水科:「指向 legacy 路径的各个 addXXX 钩子」

我:「这张图写的是每个 pass 的完整清单吗?」

水科:「只写阶段……」

水科:「也没有逐个标注某个 pass 是 legacy 独有还是两条路都有」

水科:「这部分差异,直接看」

水科:「RISCVCodeGenPassBuilder.cpp 里的 TODO……」

我:「然后呢?」

水科:「然后按顺序展开……」

三日月已经不在长椅的另一头了。

那张被她对折过一次的纸留在扶手上。

不是座位表。

是一张空的、连格子都还没有印的纸。

第三章 Subtarget 「这台机器叫什么名字」

“9 月 12 日”

三点四十分。

水塔的影子刚好盖住半条坡道。

蝉还在叫,只是叫得比七月敷衍。

我:「那么第一类选择,从哪里开始?」

水科:「从一台机器的名字开始」

我:「机器有名字?」

水科:「三元组……」

水科:「riscv64-unknown-linux-gnu……」

我:「四段?」

水科:「riscv64 是架构,后面三段是厂商、操作系统、环境」

我:「这四段决定什么?」

水科:「位宽与目标文件格式……」

水科:「还用来校验 -mabi」

我:「校验不通过会怎么样?」

水科:「32 位 ABI 配 64 位三元组,会被报一声警告,然后忽略,退回默认值……」

水科:「RISCVBaseInfo.cpp 的 54 行……」

水科:「computeTargetABI……」

我:「那要是没给 -mabi 呢?」

水科:「默认 ABI 由 -march 里的扩展算出来」

水科:「有 D 得 lp64d,有 F 得 lp64f」

水科:「两个都没有得 lp64……」

水科:「RV32 同理」

水科:「RISCVISAInfo.cpp 的 1084 行」

我:「蝉还在叫……」

我:「……」

水科:「computeDefaultABI……」

我:「名字定完了,然后呢?」

水科:「LLVM 内部,一台具体的目标机器由一个 TargetMachine 对象表示」

水科:「它按函数给出 Subtarget……」

我:「Subtarget 是什么?」

水科:「当前函数的机器配置……」

我:「那 TargetLowering 呢?」

我:「还有 TargetInstrInfo、TargetRegisterInfo」

我:「TargetFrameLowering?」

水科:「都由 Subtarget 自己持有并构造」

水科:「TargetMachine 只在最外层创建 AsmPrinter……」

我:「AsmPrinter 是做什么的?」

水科:「汇编打印器。流水线最后一站」

水科:「把机器指令降成 MCInst 交给 MC 层……」

水科:「第八章会讲到它」

水科:「RISC-V 的这一层在 RISCVTargetMachine,170 行……」

我:「嗯、」

我:「具体有哪些指令、哪些寄存器,是谁决定的?」

水科:「另外三个输入……」

水科:「-march,ISA 字符串」

水科:「instruction set architecture」

三日月:「也许……」

水科:「例如 rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_v1p0……」

我:「这一长串怎么读?」

水科:「开头是基础整数指令集,rv32i 或者 rv64i……」

水科:「其后每个下划线分隔的片段是一个扩展……」

水科:「例子里都是单字母扩展」

水科:「zmmul、zba 这类多字母扩展写在同样的位置……」

我:「2p1 是什么?」

水科:「该扩展的版本是 2.1」

我:「那 Zmmul 是 M 的父集吧?」

水科:「反了……」

我:「诶!」

水科:「只含乘法的子集。M 蕴含它……」

我:「……啊哈哈,我把方向记倒了……」

水科:「解析器是 RISCVISAInfo,解析结果是一组 feature bit」

我:「feature bit?」

水科:「每个扩展对应一个开关位……」

我:「-mcpu 呢?」

水科:「处理器型号,它给出一组默认的扩展和流水线信息……」

我:「流水线信息是什么?」

水科:「调度模型,每条指令的延迟与资源占用……」

水科:「还有指令融合」

我:「融合就是把两条指令合并成一条?」

水科:「不是……」

水科:「调度上把相邻两条指令当作一个宏操作来算冒险,并不是真把它们合并成一条指令……」

我:「-mabi 呢?」

水科:「ABI 名称,例如 lp64d」

水科:「它决定浮点参数走整数寄存器还是浮点寄存器……」

我:「那个第五章再讲?」

水科:「第五章」

我:「rv32i 和 rv64i 的差别在哪……」

水科:「通用寄存器宽度……」

水科:「rv32i 是 32 位,rv64i 是 64 位……」

水科:「这个宽度在后端里有一个统一的名字,XLen」

水科:「对应的机器值类型叫 XLenVT……」

我:「RV32 上是什么?」

水科:「i32……」

我:「RV64 呢?」

水科:「i64。RISCVSubtarget.h 的 231 行」

水科:「后文说 XLen 宽度指的就是这个宽度……」

电线杆上贴着一张褪了色的电影海报。

旭座的。

海报下角有人用签字笔写了一行小字,被雨打糊了。

我:「常用的扩展有哪几组?」

水科:「M 是乘除法」

水科:「A 是原子,其他线程看不到操作的中间状态……」

水科:「F 和 D 是单精度、双精度浮点,C 是 16 位压缩指令……」

水科:「V 是向量,还有大量 Z 开头的小扩展……」

我:「蝉还在叫……」

我:「行号也记得?」

水科:「RISCVFeatures.td 里」

水科:「FeatureStdExtM 在 223 行」

水科:「A 在 250 行……」

水科:「F 在 307 行,D 在 315 行」

水科:「C 在 417 行,V 在 698 行……」

我:「慢一点……」

三日月:「呵呵……」

我:「……你到底背了多少?」

水科:「不够多」

我:「那没有 M 会怎么样?」

水科:「没有 Zmmul 时变量乘法落库函数……」

水科:「没有 M 时除法也落」

水科:「RV64 是 __muldi3 和 __divdi3」

水科:「RV32 是 __mulsi3 和 __divsi3……」

我:「那、那个」

我:「啊、」

我:「一点都不会用硬件算?」

水科:「只有乘数是常数时才会折成移位加」

我:「没有 F 和 D 呢?」

水科:「浮点类型本身不合法……」

水科:「运算被软化」

我:「软化?」

水科:「soften。改写成整数上的位运算加库函数调用……」

水科:「两处的机制都在指令选择那一章……」

我:「那么这些 feature bit?」

我:「加上 CPU 和 ABI,合起来叫什么?」

水科:「Subtarget……」

水科:「它是后面所有阶段查询这台机器能不能做某件事的唯一入口……」

我:「唯一?」

水科:「唯一……」

我:「为什么要按函数缓存?」

水科:「同一个模块里不同函数可以带不同的 target-features 属性」

水科:「getSubtargetImpl……」

水科:「RISCVTargetMachine.cpp 的 197 行……」

我:「不是……」

水科:「从函数属性里读 target-cpu、tune-cpu、target-features」

水科:「再看有没有 vscale_range 属性……」

我:「vscale 是什么?」

水科:「向量长度的运行期缩放因子……」

水科:「正式定义在 RVV 那一章」

水科:「那个属性的单位是 vscale,不是 bit……」

我:「Subtarget 里面有什么?」

水科:「主要成员在 RISCVSubtarget.h 的 102 行起……」

水科:「一批 feature bit 开关由 107 行的宏批量生成」

水科:「FrameLowering、InstrInfo、TLInfo」

水科:「三个成员在 120 到 122 行……」

我:「够了」

我:「Subtarget 定的是指令,那数据呢?」

水科:「数据由 DataLayout 定……」

水科:「它由三元组和 ABI 名算出,是一个字符串」

水科:「RV64 加 LP64D 得到一个字符串……」

水科:「e-m:e-p:64:64-i64:64-i128:128-n32:64-S128」

水科:「TargetDataLayout.cpp 的 280 行……」

我:「诶、」

水科:「computeRISCVDataLayout……」

我:「那个……」

我:「这一串每一项都在回答什么?」

水科:「一个后端必须知道的问题」

水科:「p:64:64,指针 64 位、对齐 8 字节……」

水科:「i64:64,64 位整数对齐 8 字节」

三日月:「都不是……」

我:「RV32 上呢?」

水科:「也是 i64:64」

水科:「所以 long long 在 RV32 上按 8 字节对齐……」

我:「i128 呢?」

水科:「128 位整数对齐 16 字节……」

我:「n32:64 是什么?」

水科:「32 位和 64 位是原生宽度,RV32 的字符串是 n32……」

我:「最后那个 S128 呢?」

水科:「栈对齐 16 字节」

水科:「ILP32E 和 LP64E 两个嵌入式 ABI 是 4 和 8 字节,其余都是 16」

水科:「RISCVFrameLowering.cpp 的 38 行……」

我:「这句 16 字节后面还会用到吗?」

水科:「调用约定那一章的栈帧部分,还会用到一次……」

我:「那这个字符串什么时候定下来?」

水科:「必须在任何 pass 之前」

水科:「结构体大小、GEP 的字节偏移、各处的对齐要求都按它算……」

水科:「IR 里的 align 标注够不够,栈对象怎么摆,都问它……」

坡道走到一半,自动贩卖机的灯还亮着。

白天亮着的贩卖机灯,看起来比夜里更徒劳。

我:「寄存器呢,RISC-V 有多少个?」

水科:「寄存器文件有三个,32 个通用寄存器,x0 到 x31……」

水科:「汇编里也写 ABI 名,zero、ra、sp 那些……」

水科:「32 个浮点寄存器,f0 到 f31,F 或 D 扩展才有」

我:「嗯」

水科:「32 个向量寄存器,v0 到 v31,V 扩展才有……」

我:「然后?」

我:「三十二!」

水科:「三十二……」

我:「都一样大吗?」

水科:「不全是一样大」

水科:「f 寄存器按 F 或 D 扩展可以是 32 位或 64 位……」

水科:「v 寄存器按 RVV 是可变长的寄存器组」

我:「指令选择和寄存器分配,直接面对 32 个寄存器这个全集吗?」

水科:「不!」

水科:「它们面对寄存器类……」

我:「register class?」

水科:「一个类是一组用途、宽度都相容的寄存器,指令的操作数声明自己属于哪个类……」

我:「RISC-V 的类定义在哪?」

水科:「RISCVRegisterInfo.td……」

水科:「GPR 在 320 行」

水科:「FPR32 在 562 行,VR 在 900 行」

我:「.td 是什么?」

水科:「TableGen 的输入文件」

水科:「TableGen 是 LLVM 的目标描述语言」

水科:「构建时把 .td 里的声明生成成 C++ 表……」

水科:「后文遇到的寄存器、指令、调度模型都写在 .td 里……」

我:「鞋带松了……」

水科:「GPR 是通用寄存器,FPR 是浮点寄存器,VR 是向量寄存器……」

我:「同一批寄存器会派生出别的类吗?」

水科:「好几个……」

水科:「GPRNoX0 去掉恒为零的 x0」

水科:「GPRC 只留能用 2 字节压缩格式编码的那 8 个……」

水科:「GPRJALR 去掉不能用 jalr 的 x1 与 x5」

我:「为什么这两个不能用?」

水科:「有的微架构把它们当作弹出返回地址栈的提示」

水科:「再去掉保留寄存器 x0、x2 到 x4……」

水科:「RISCVRegisterInfo.td 的 377 行」

水科:「FPR32C 和 FPR64C 同理……」

我:「类是随便切的吗?」

水科:「不是……」

水科:「一个类是某条指令的某个操作数可以换成哪些寄存器的集合」

水科:「换进去的每一个,都必须让这条指令仍然编得出、语义不变……」

我:「要求极大吗?」

水科:「既不要求极大,也不要求互斥……」

水科:「上面那批派生类就是层层包含的」

水科:「类越小,分配器的可选范围越小,但指令能选得越省空间……」

我:「该用哪个类,谁决定?」

水科:「后面每一层都要问这个问题……」

我:「有永远不参与分配的吗?」

水科:「有几个?」

水科:「getReservedRegs」

水科:「RISCVRegisterInfo.cpp 的 172 行……」

水科:「x0 恒零,x2 是栈指针 sp」

水科:「x3 是全局指针 gp,x4 是线程指针 tp……」

我:「是、」

我:「……」

水科:「x8 是帧指针 fp,只在需要时保留」

水科:「还有 vl、vtype、vxrm、frm 这些 CSR……」

水科:「control and status register」

水科:「控制和状态寄存器,RVV 那一章会用,还有 SSP,影子栈指针」

我:「啊、」

水科:「在 getReservedRegs 里无条件保留……」

我:「诶、」

我:「指令本身呢,它们长什么样是谁决定的?」

水科:「编码形式」

水科:「RISC-V 基础指令集的每条指令都是 32 位……」

水科:「按立即数在编码里的摆法分成几种形式」

我:「啊、」

水科:「TableGen 里用 InstFormat 枚举……」

我:「嗯、」

水科:「RISCVInstrFormats.td 的 30 行……」

我:「哪几种?」

水科:「R,两个源寄存器加一个目标,I,一个源寄存器加 12 位立即数……」

水科:「S,存储,12 位立即数,B,条件跳转,13 位立即数、最低位恒 0」

水科:「也就是正负 4 KiB……」

我:「……」

水科:「U,20 位上立即数,配 lui 和 auipc」

水科:「J,无条件跳转,21 位立即数、最低位恒 0……」

水科:「也就是正负 1 MiB……」

水科:「还有四个寄存器操作数的 R4,浮点融合乘加,三源一目标……」

我:「压缩指令呢?」

水科:「另有十四种 16 位形式」

水科:「InstFormatCR 到 InstFormatCSH」

水科:「其中基础 C 扩展只占九种……」

水科:「CR、CI、CSS、CIW、CL、CS、CA、CB、CJ」

我:「剩下五种呢?」

水科:「全部来自 Zcb……」

水科:「基本压缩指令集的补充,几条 8 位和 16 位访存」

水科:「加一族单寄存器运算……」

水科:「CLB 是 c.lbu,CLH 同时装 c.lhu 与 c.lh」

我:「嗯、」

水科:「CSB 是 c.sb,CSH 是 c.sh……」

我:「我不懂……」

水科:「RISCVInstrInfoZc.td 的 188 行起」

水科:「CU 是 c.not、c.sext.b、c.sext.h」

水科:「c.zext.b、c.zext.h、c.zext.w 这一族……」

水科:「139 行的 RVZcArith_r,谓词都含 HasStdExtZcb」

三日月:「注释而已……」

我:「哪个?」

水科:「c.zext.w 另需 Zba,也就是地址生成扩展,且只在 RV64……」

水科:「c.zext.h 另需 Zbb,基本位操作扩展……」

我:「啊、」

我:「形式只是编码细节吧?」

水科:「它会一路反向影响后端各阶段的决定」

我:「怎么影响?」

水科:「12 位立即数的取值范围是负 2048 到 2047……」

水科:「任何超出这个范围的加法、访存偏移、地址计算」

水科:「都不能用一条 addi、ld、sd 解决,得先算出一个中间值……」

我:「那、那个」

三日月:「也许……」

我:「U 型呢?」

水科:「U 型指令只有 20 位……」

水科:「一个 64 位常量至少要 lui 加若干条 addi、slli、add 拼出来」

水科:「或者放进常量池,用 lui 加 ld 取……」

我:「常量池是什么?」

水科:「编译器在函数附近开的一段只读数据区,专放装不进立即数的常量与地址……」

我:「几套拼法之间挑哪一套,谁挑?」

水科:「generateInstSeq」

水科:「RISCVMatInt.cpp 的 294 行」

水科:「判据是条数……」

水科:「TmpSeq.size() + 1 < Res.size() 才换……」

水科:「外加一条换成压缩对更友好的启发……」

我:「不是……」

我:「那 getInstSeqCost 呢,名字很像?」

水科:「另一回事,它给一套现成的拼法打加权分,非压缩记 100,可压缩记 70……」

我:「70 是什么意思?」

水科:「两条压缩指令略贵于一条非压缩指令」

水科:「16 行。全仓只被 getIntMatCost 调用……」

水科:「在 577 行……」

我:「够了」

水科:「供上层估这个常量值多少钱……」

我:「上层是哪些地方?」

水科:「TTI 的常量代价」

水科:「RISCVTargetTransformInfo.cpp 的 144 行……」

水科:「select 两条路谁便宜」

水科:「RISCVISelLowering.cpp 的 10704 行」

我:「蝉还在叫……」

水科:「一串常量能不能合成一次存储,24819 行……」

水科:「换一个等价立即数是否更省」

我:「那个……」

水科:「selectImm64IfCheaper」

水科:「RISCVISelDAGToDAG.cpp 的 4332 行……」

我:「B 型呢?」

水科:「B 型跳转的范围是正负 4 KiB……」

水科:「函数内的条件跳转一旦超范围」

水科:「就由后端的 BranchRelaxation 在发射前改写」

水科:「第八章讲……」

我:「全局地址呢,能一条指令直接取吗?」

水科:「不能」

水科:「PC 相对寻址要 auipc 加 addi……」

水科:「%pcrel_hi 和 %pcrel_lo 之类两三条指令的组合」

水科:「位置无关代码取外部可见符号还要多经过一层 GOT……」

我:「PIC 是……」

水科:「position-independent code。加载地址不固定的代码」

水科:「GOT 是 global offset table」

水科:「存放符号实际地址的表……」

水科:「用哪一套组合、能不能省掉其中一条,由代码模型与是否 PIC 共同决定」

我:「那个……」

水科:「代码模型是对符号地址能落在多远的假设……」

水科:「这两项和 -march、-mabi 一样,属于第 1 类选择就定下来的输入……」

我:「指令本身写在哪里?」

水科:「RISCVInstrInfo.td」

水科:「ADD 复用 ALU_rr 模板,761 行」

水科:「目标与两个源操作数都在 GPR,是模板在 763 行声明的……」

我:「慢一点……」

水科:「def ADD 本身在 881 行,ADDI 复用 ALU_ri,749 行」

水科:「第二个操作数是 12 位有符号立即数……」

我:「操作数类型只是给汇编器打印用的吧?」

水科:「不只是……」

水科:「它同时是后面两章所有能不能这么选的判据」

我:「GPR 和 simm12 这种?」

水科:「这种……」

蝉忽然停了一拍。

只有一拍。

然后又接上去,像什么都没发生过。

我:「那么第一类选择,到这里就讲完了?」

水科:「讲完了,后面所有阶段都在这个范围内工作……」

水科:「Subtarget 说有哪些指令……」

水科:「DataLayout 说数据多大,寄存器类说操作数能放哪……」

我:「……」

水科:「编码形式说立即数能多大……」

我:「全都是别人先定好的」

水科:「全都是」

我把座位表从口袋里掏出来,又折了一次。

四折变成八折,格子小了一半。

七排十二号还在,只是那个铅笔圈被折痕正好穿过。

第四章 SelectionDAG 「先合法,再挑一条」

“9 月 12 日”

五点十分。

水塔的影子刚好盖住整张长椅,多出来的那一截搭在水泥地上。

蝉还在叫,只是叫得比七月敷衍。

我:「指令选择要回答什么问题?」

水科:「一个基本块里的 IR 操作序列,对应目标机器上的哪几条机器指令……」

水科:「主要实现是 SelectionDAG」

水科:「把每个基本块变成一个 DAG,有向无环图……」

水科:「节点是操作,边是数据依赖与顺序依赖……」

水科:「chain 把访存与其他有副作用的节点串成一条序……」

三日月:「那一格是空的……」

我:「诶、」

水科:「在 DAG 上做类型与操作的合法化、模式化简」

水科:「再把节点一条条选成机器指令,按依赖关系排成序列……」

水科:「整个过程一次只处理一个基本块,这是它和 IR 优化最大的区别……」

我:「还有?」

水科:「也是 RISC-V 后端有那么多」

水科:「RISCVCodeGenPrepare 一类 IR 层补丁的原因」

水科:「跨基本块的事,DAG 表示不了……」

我:「有例子吗?」

水科:「RVV 的 llvm.vp.merge 合并 i1 掩码……」

水科:「保持 i1 宽度,DAG 只能用一长串掩码指令拼」

水科:「RISCVCodeGenPrepare 在 IR 层把合并的宽度扩到 i8……」

水科:「后端就能用一条 vmerge.vim 完成」

我:「嗯」

水科:「RISCVCodeGenPrepare.cpp 的 118 行附近」

我:「……」

水科:「注释给了这个例子……」

水科:「这个改写要看整条 def-use 链和循环结构,DAG 做不了」

我:「流程是什么?」

水科:「CodeGenAndEmitDAG」

水科:「SelectionDAGISel.cpp 的 947 行」

水科:「顺序是固定的……」

我:「是、」

水科:「建图,SelectionDAGBuilder」

水科:「IR 指令与基本块参数变成 DAG 节点……」

水科:「combine 1,988 行,还没做类型合法化时的化简」

我:「便当凉了……」

水科:「常量折叠、冗余节点、目标无关的代数化简……」

水科:「类型合法化,1010 行的 LegalizeTypes」

水科:「把目标不支持的机器类型替换成支持的组合」

水科:「combine LT,1034 行,这一步有改动时才跑……」

水科:「向量合法化,1051 行的 LegalizeVectors」

我:「啊、」

我:「嗯、」

水科:「把向量操作拆成目标支持的形式,类型合法化 2,1068 行」

水科:「向量合法化引入新类型,再补一轮……」

水科:「仅在向量合法化有改动时跑」

水科:「combine LV,1088 行,紧随上一步……」

我:「那个」

我:「啊、」

水科:「操作合法化,1108 行的 CurDAG->Legalize()……」

水科:「把目标不支持的操作替换成支持的操作序列或库函数调用……」

水科:「combine 2,1128 行」

我:「嗯?」

水科:「选择,1152 行和 1168 行的 DAG 调度器」

水科:「DAG 节点变成机器指令」

水科:「装进 MachineBasicBlock……」

三日月:「都不是……」

我:「化简为什么反复出现?」

水科:「每个阶段都会产生新的可折叠结构」

水科:「类型合法化插入截断与扩展,向量合法化拆开向量操作……」

水科:「操作合法化把一个操作拆成几条基本运算」

水科:「前一次化简的结论,后一次未必还成立,所以只能各跑一遍……」

三日月:「还没被填进去……」

水科:「四个 combine 调用点」

我:「那个……」

水科:「BeforeLegalizeTypes、AfterLegalizeTypes」

水科:「AfterLegalizeVectorOps、AfterLegalizeDAG……」

水科:「中间两个只在相应合法化确实改动了 DAG 时才跑」

水科:「前后两次无条件跑」

我:「真的?」

水科:「把 DAG 变成机器指令的只有选择与随后的调度……」

水科:「建图是造 DAG,中间的合法化与化简都只是在改写 DAG……」

我:「类型合法化,是说 IR 里的类型合法不合法?」

水科:「不是。名字容易误解……」

水科:「它说的是这个类型能不能直接装进某个寄存器类」

水科:「合法集合由 Subtarget 构造时的一组 addRegisterClass 定义……」

水科:「RISCVISelLowering.cpp 的 172 行」

水科:「XLenVT 对 GPR……」

我:「啊、」

我:「……」

水科:「RV32 是 i32,RV64 是 i64……」

水科:「只有这个宽度合法,其余宽度靠合法化补,RV32 上 i64 不合法」

水科:「一次 64 位运算或访存拆成两条 32 位操作……」

我:「然后?」

水科:「RV64 上 i32 也不合法,但这些算术被标成 Custom,395 行」

我:「诶、」

水科:「改走 ReplaceNodeResults 里 16595」

水科:「行的 customLegalizeToWOpWithSExt……」

水科:「用 addw 这类 32 位指令直接算」

水科:「而不是整体提升到 i64,f16 对 FPR16」

我:「不是……」

我:「嗯、」

水科:「只有 Zfh 或 Zfhmin 在时才合法……」

水科:「否则 f16 被提升成 f32 算完再截回」

水科:「f32 对 FPR32 要 F,f64 对 FPR64 要 D」

水科:「没有对应扩展时浮点类型全部不合法……」

水科:「浮点操作退化成整数上的位运算加库函数调用」

我:「那个……」

我:「……」

水科:「例外是 Zfinx、Zdinx 把浮点值放进 GPR……」

水科:「Zhinxmin 把 f16 放进 GPRF16……」

水科:「那是通用寄存器的低 16 位子寄存器组成的类」

水科:「RISCVRegisterInfo.td 的 595 行……」

我:「诶、」

水科:「RVV 把一组 <vscale x N x」

水科:「T> 注册成 VR、VRM2、VRM4、VRM8」

我:「水科,你喘口气……」

水科:「226 行的 addRegClassForRVV……」

水科:「没有 V 时 IR 里的向量全部被拆成标量」

水科:「P 扩展把 v2i32 这类小整数向量放进 GPR,是例外……」

三日月:「谁知道呢……」

水科:「P 是 RISC-V 的 packed-SIMD、DSP 扩展……」

我:「动作有几种?」

水科:「五类。提升、拆分、标量化、放宽、软化……」

水科:「提升是小的换成大的」

水科:「i8 算术在 RV64 上用 i64 算完再截断」

水科:「标量整数的提升一定一步落到合法类型……」

水科:「TargetLoweringBase.cpp 的 1453 行起」

我:「嗯」

我:「嗯、」

水科:「给每个非法整数类型直接填 TransformToType[IntReg] = LegalIntReg」

水科:「1462 行」

水科:「LegalIntReg 是比它宽的、最近的那个合法整数类型……」

水科:「1455 行起的循环自宽向窄扫,扫到合法者就更新一次」

我:「是、」

我:「为什么?」

水科:「RISC-V 上合法整数类型只有 XLenVT 一个」

水科:「不留中间台阶……」

水科:「1110 行那条 Promote may not」

我:「啊、」

水科:「follow Expand or Promote 只是配套约束」

水科:「它禁止的是提升落到的类型自己还要再被提升……」

水科:「该断言对向量目标不施约束」

我:「那个……」

水科:「拆分是大的拆成小的,RV32 的 i64 拆成两个 i32」

水科:「向量按元素数折半……」

水科:「标量化是只有一个元素的向量换成它的元素类型,放宽是把元素个数补到 2 的幂」

我:「等一下……」

水科:「<3 x i32> 变成 <4 x i32>……」

三日月:「注释而已……」

水科:「只在两种情形放宽」

水科:「要么元素个数不是 2 的幂、补一次到下一个 2 的幂……」

水科:「要么沿更宽的向量一直找到合法类型为止,找到就停」

水科:「软化是浮点类型换成同宽的整数类型……」

我:「硬币只剩三枚……」

水科:「运算改成整数上的位操作加库函数调用……」

水科:「f128 到 i128 在 1489 行,f64 到 i64 在 1507 行」

水科:「f32 到 i32 在 1516 行……」

水科:「f16 与 bf16 不走软化」

我:「嗯、」

我:「……」

水科:「另有 TypeSoftPromoteHalf,目标是 f32」

水科:「1534 行和 1544 行……」

我:「它会停吗?」

水科:「拆分后的每一半都要重新走一遍合法化……」

水科:「所以它是一个不动点过程,迭代到没有非法类型为止……」

水科:「形式上与上篇 T1 同型……」

三日月:「只是名字不同……」

水科:「停机按一个字典序测度归纳……」

水科:「测度是是不是整数、是不是标量、元素个数是不是 2 的幂、元素个数、标量位宽」

水科:「整数小于浮点,标量小于向量,是 2 的幂小于不是……」

我:「诶、」

水科:「拆分、标量化、软化这三类都让测度严格下降,提升与放宽乍看会上升……」

水科:「但只要它们是沿更宽的类型往上扫扫出来的,就一步落到合法类型」

水科:「链条当场结束……」

我:「啊、」

我:「嗯、」

我:「为什么能保证一步落到?」

水科:「合法化查的不是现算的搜索」

水科:「getTypeConversion 在 1101 行」

水科:「读 computeRegisterProperties 预先建好的表」

水科:「1427 行……」

水科:「建表时那两个搜索都把 isTypeLegal 写进了命中条件」

我:「那、那个」

我:「再说一遍?」

水科:「元素提升沿更宽的元素类型往上扫」

水科:「扫到第一个合法的就填表并停,1572 行……」

水科:「放宽同理,命中条件里带 isTypeLegal(SVT),1595 行」

水科:「补出来的 2 的幂类型本身合法才在放宽那一支落定」

我:「不是……」

水科:「getPow2VectorType,1608 行……」

水科:「仍不合法就落到拆分或标量化那一支,1619 到 1650 行」

我:「我记不住这么多……」

水科:「那一支开头还要再看一次是不是 2 的幂……」

水科:「不是就仍记成放宽,1645 到 1647 行,第三位变小」

水科:「是才真的拆分或标量化,1631 到 1644 行」

我:「够了」

水科:「第四位或第二位变小……」

我:「有一支不落在合法类型上吧?」

水科:「半精度。f16 的表项填 f32,位宽变大又不合法」

水科:「但 f32 自己的表项只会是合法或软化,不会再指回半精度……」

水科:「于是这一支要么一步到合法,要么再经软化落到整数……」

水科:「落到整数之后就归入前面那三组,不会再回头,于是不存在无限上升的链……」

我:「诶、」

我:「嗯、」

水科:「可能的类型只有有限多个,每个类型在有限步内变合法」

水科:「DAG 里的节点有限,每个节点只在类型被替换时重新处理……」

水科:「所以 worklist 一定会空……」

我:「第二层呢?」

水科:「操作合法化。类型都合法了,但目标可能没有对应的操作」

水科:「LegalizeDAG.cpp 的 982 行」

水科:「LegalizeOp 查 getOperationAction……」

水科:「四个返回值里 Legal 表示不用动」

三日月:「假设还有一个……」

我:「啊、」

水科:「第四条不是独立的返回值,而是 Expand 拆到底最常见的结果……」

水科:「RISC-V 把两组操作设成 Expand,条件不同……」

我:「嗯」

水科:「没有 Zmmul 时 MUL、MULHS、MULHU 在 XLen 上是 Expand」

水科:「404 行……」

水科:「Zmmul 是只含乘法的子集,M 蕴含它……」

我:「那个……」

水科:「没有 M 时 SDIV、UDIV、SREM、UREM 在 XLen 上是 Expand」

水科:「413 行……」

水科:「SDIVREM 和 UDIVREM 无条件是 Expand,421 行」

水科:「上面三条设的是操作动作,XLen 本身又是合法类型……」

我:「再说一遍?」

水科:「所以触发的是操作合法化」

我:「那个……」

水科:「LegalizeOp 的 1381 行是 case Expand」

水科:「先 if (ExpandNode(Node)) return」

水科:「紧跟一个 fallthrough 落到 case LibCall」

水科:「ConvertNodeToLibcall(Node),4746 行……」

三日月:「还没被填进去……」

水科:「ExpandNode 拆不出来就发库调用」

水科:「SDIV 那一支在 4098 行,只有两条路……」

水科:「SDIVREM 是 legal 或 Custom 时改用它」

水科:「否则什么也不产出,RISC-V 属于后者」

水科:「它的 SDIVREM 是 Expand……」

我:「啊、」

我:「嗯?」

水科:「MUL 那一支在 4155 行」

水科:「先看 SMUL_LOHI、UMUL_LOHI」

水科:「RISC-V 上它们同样是 Expand……」

我:「那个」

水科:「再试 expandMUL 带 OnlyLegalOrCustom」

水科:「因为 MUL 本身不是 legal 或 custom 而失败」

水科:「类型非法时,例如 RV32 的 i64」

我:「诶、」

水科:「走 LegalizeIntegerTypes 里形状相同的另一套……」

水科:「ExpandIntRes_MUL 在 4501 行」

水科:「ExpandIntRes_SDIV 在 4877 行」

水科:「同样先试展开、失败就 makeLibCall……」

我:「嗯、」

我:「嗯、」

水科:「这一版两套都已经没有用高位乘法做 non-restoring 除法那处通用展开了」

我:「实测过吗?」

水科:「用本版 llc……」

水科:「rv64i 下 mul i64 变成 tail __muldi3」

水科:「sdiv i64 变成 tail __divdi3」

水科:「rv64 加 -mattr=+zmmul,-m」

水科:「下乘法直接用硬件 mul a0, a0, a1」

三日月:「呵呵……」

我:「……」

水科:「因为 404 行的条件是 !hasStdExtZmmul()……」

水科:「只有除法仍落 __divdi3。两者都不是移位加序列」

我:「Promote 和 Custom 呢?」

水科:「Promote 是提升到更宽的类型做……」

水科:「Custom 是目标自己写 lowering」

水科:「入口 RISCVTargetLowering::LowerOperation」

水科:「8268 行……」

我:「啊、」

水科:「返回值类型需要改变时走 ReplaceNodeResults」

水科:「16351 行……」

我:「等一下……」

水科:「库函数调用记成一次对固定符号的调用,等后面按调用约定落成普通调用……」

水科:「RV32 的 64 位除法、没有 F 时的浮点运算都走这条」

我:「选择的入口在哪?」

水科:「Custom lowering 集中在几处……」

水科:「取全局、常量池、跳转表的地址」

水科:「lowerGlobalAddress,10158 行」

我:「那、那个」

水科:「select,lowerSELECT,10498 行……」

水科:「brcond,lowerBRCOND,10876 行」

我:「嗯、」

水科:「可变参数起始,lowerVASTART,10899 行,以及原子的读写改……」

水科:「select 有 Zicond 时用 czero.eqz 和 czero.nez……」

水科:「Zicond 提供条件不成立就把目标写零的两条指令……」

我:「啊、」

水科:「否则落成 RISCVISD::SELECT_CC」

水科:「分支型 select」

水科:「在 MIR 里是条件分支加汇合块上的复制……」

水科:「原子那里分三个决定」

水科:「shouldExpandAtomicRMWInIR」

我:「不是……」

水科:「28427 行……」

我:「诶?」

水科:「只管要不要在 IR 层就换成 cmpxchg 或掩码原子 intrinsic」

水科:「浮点原子、nand 一类硬件没有的操作走这条……」

水科:「留下不展开的,由模式直接选成原子内存操作指令,amo 那批」

水科:「atomicrmw add i32 选成 amoadd.w」

我:「够了」

水科:「RISCVInstrInfoA.td 的 232 行」

我:「那个……」

水科:「硬件没有对应 amo 的先落成 PseudoAtomicLoad,380 行……」

水科:「等最后一批伪指令展开时才变成 lr 和 sc 循环」

水科:「lr 是 load-reserved」

我:「诶、」

水科:「sc 是 store-conditional……」

我:「罐子空了……」

水科:「Zacas 提供的 amocas 走 cmpxchg 那条路」

水科:「Zacas 是原子比较交换扩展……」

水科:「RISCVDAGToDAGISel::Select」

水科:「1099 行……」

我:「嗯」

水科:「分工写在函数开头那句注释里,1107 行……」

三日月:「那一格是空的……」

水科:「自动生成的 TableGen 选择没处理掉的,都在这里处理」

水科:「所以顺序是先手写、再交给生成的匹配器……」

水科:「手写那一半 switch 掉一批 TableGen 表达不了的形状」

水科:「常量物化,1117 行起……」

水科:「里面还含高位没人读就把立即数改写成能用压缩指令编码的形式」

我:「是、」

水科:「(shl (and X, C2), C) 一类位运算形状」

我:「诶、」

水科:「1298 行……」

水科:「有符号位域的抽取与插入,1425 行,访存的非临时提示,3312 行……」

水科:「都没接住,才在最后一行落到 SelectCode(Node)」

我:「啊、」

我:「嗯、」

水科:「3320 行……」

水科:「生成那一半跑的是 TableGen 按 .td 里的模式生成的选择表」

水科:「模式的形式是 DAG 的一小棵树对一条指令……」

水科:「PatGprGpr<add, ADD>」

我:「那个」

水科:「RISCVInstrInfo.td 的 1500 行」

我:「……」

水科:「说两个 GPR 的 add 可以选成 ADD」

水科:「立即数形式靠操作数类型把范围卡在匹配时……」

水科:「PatGprSimm12 展开成 PatGprImm 带 simm12_lo,1424 行」

水科:「用的不是 TableGen 的 Predicate……」

水科:「SelectCode 进的是 SelectCodeCommon,3357 行」

我:「嗯、」

水科:「先用一个通用 switch 就地处理掉一批与目标无关的节点」

我:「然后?」

水科:「CopyFromReg、CopyToReg」

水科:「TokenFactor、TargetConstant 等……」

水科:「再跑目标的匹配表,接不住时当场报错,CannotYetSelect」

水科:「4623 行的 report_fatal_error」

我:「啊、」

我:「嗯、」

水科:「调用点 4545 行……」

水科:「没有退回去再拆一层这一步,拆分是合法化阶段的事……」

水科:「走到选择这一步,节点的类型与操作都必须已经合法……」

我:「那、那个」

我:「Cannot select 一般是什么原因?」

水科:「两种。目标没给这个已经合法的节点写模式,或者合法化漏了它」

我:「地址形式是第三件事吗?」

水科:「容易被误当成第三件事,其实它也在生成这一侧……」

水科:「是被 TableGen 回调的,不是绕开它的手写代码」

水科:「def AddrRegImm 是 ComplexPattern」

水科:「iPTR,2,SelectAddrRegImm……」

我:「啊、」

水科:「RISCVInstrInfo.td 的 590 行」

水科:「把它登记成一个复合模式……」

水科:「生成的匹配器在匹配访存模式时回调它,3503 行……」

水科:「它把任意的地址表达式拆成一个基址寄存器加一个 12 位以内的偏移」

水科:「拆不出来就当场造一条额外的地址计算指令……」

我:「不是……」

水科:「3574 行的 ADDI、3592 行的 ADD」

我:「那个……」

水科:「同族另外三个变体各服务一种非基础的偏移宽度……」

水科:「都不是给压缩指令或 Zilsd 用的,SelectAddrRegImm26 给」

水科:「Qualcomm Xqcilo 的 26 位偏移访存……」

我:「够了」

水科:「比 12 位更大,3607 行的注释写明」

我:「那个……」

水科:「SelectAddrRegImm9 给 MIPS 厂商扩展的 prefetch」

水科:「uimm9……」

水科:「RISCVInstrInfoXMips.td 的 24 行」

我:「诶、」

水科:「SelectAddrRegImmLsb00000 给 Zicbo 的缓存块操作」

我:「鞋带松了……」

我:「压缩访存是在这一步选出来的吗?」

水科:「不是。Zilsd 也不是,两件事都发生在更晚……」

水科:「Zilsd 把两条相邻 32 位访存合成一条 64 位 ld 或 sd」

水科:「只在 RV32 上存在」

水科:「压缩由发射期的 RISCVRVC::compress 决定」

我:「嗯」

水科:「RISCVAsmPrinter.cpp 的 276 行……」

我:「……」

水科:「判据是一张 CompressPat 表」

水科:「LW 的 rd 在 GPRC、rs1 在 BasePtrC、偏移是 uimm7_lsb00」

水科:「才压成 c.lw……」

水科:「RISCVInstrInfoC.td 的 700 行」

水科:「c.sw 在 712 行」

我:「是、」

水科:「选择阶段先按 12 位偏移选出普通 lw……」

三日月:「谁知道呢……」

水科:「寄存器类与更窄的偏移类型是在压缩时再检查一遍」

水科:「BasePtrC 是 GPRC 用作访存基址时的包装」

水科:「RISCVInstrInfo.td 的 242 行……」

我:「啊、」

水科:「Zilsd 那两条真指令 LD_RV32 和 SD_RV32 在 .td 里没有选择模式」

我:「嗯、」

水科:「RISCVInstrInfoZilsd.td 的 36 行只声明指令与汇编形式……」

水科:「配对是 MIR 层的 pass 造出来的」

水科:「分配前先拼成两个独立寄存器加 12 位偏移的 PseudoLD_RV32_OPT……」

水科:「RISCVZilsdOptimizer.cpp 的 353 行」

我:「那个」

水科:「分配后再检查是否凑成了对」

我:「……」

水科:「凑成才换成 GPRPairRV32 的真指令……」

水科:「RISCVLoadStoreOptimizer.cpp 的 916 行」

水科:「GPRPairRV32 在 RISCVInstrInfoZa.td 的 35 行……」

水科:「约束到寄存器类 GPRPair……」

水科:「RISCVRegisterInfo.td 的 458 行……」

我:「嗯、」

我:「地址拆不出来时多出来的那条指令呢?」

水科:「后面 RISCVFoldMemOffset、RISCVMergeBaseOffset」

水科:「会尝试再折回去,折叠的等式很直接……」

水科:「中间指令是 addi rd, rs, C,访存是 ld rt, off(rd)」

我:「啊、」

水科:「折叠后变成 ld rt, C+off(rs)……」

我:「我不懂……」

水科:「两边的有效地址都是 rs + C + off……」

水科:「访问的是同一个位置,成立需要两个条件……」

水科:「新偏移仍能装进 12 位有符号字段,否则编码不下」

我:「那、那个」

水科:「且 rd 除了作这个地址没有别的用途……」

我:「嗯、」

水科:「两条都满足时指令少一条,省下的寄存器也还回寄存器文件」

我:「产物是什么?」

水科:「机器指令 MachineInstr,操作数是虚拟寄存器……」

水科:「一个可以无限编号的临时寄存器,还没绑定到物理寄存器」

水科:「此时指令可能仍是伪指令,pseudo instruction……」

我:「啊、」

水科:「机器指令框架里的占位,名字以 Pseudo 开头」

水科:「表示这条操作要做」

水科:「但用哪几条真实指令、需要什么寄存器,等后面再定……」

我:「有哪几类?」

水科:「主要是三类,后面还会看到条件运算 PseudoCC 和原子 PseudoAtomic」

水科:「地址类,PseudoLLA、PseudoLGA」

水科:「PseudoLA_TLS_IE、PseudoLA_TLS_GD……」

水科:「RISCVInstrInfo.td 的 1965 行起」

水科:「la 助记符对应的 PseudoLA 只由汇编器接受」

我:「不是……」

水科:「指令选择在 medlow 下直接产出 lui」

我:「那个……」

水科:「和 addi 的 HI、ADD_LO 组合……」

水科:「medlow 是小代码模型,假设符号地址装得进 32 位有符号数」

水科:「要等代码模型、重定位类型确定后才能展开……」

水科:「重定位是链接期才填上的地址记号,第八章讲……」

我:「罐子空了……」

我:「还有?」

水科:「跳转类,PseudoBR 在 1817 行」

水科:「PseudoCALL 在 1860 行,PseudoTAIL 在 1918 行」

水科:「PseudoBR 发射时按 TableGen 写好的规则变成 jal x0……」

我:「诶、」

我:「慢一点……」

水科:「PseudoCALL 和 PseudoTAIL 在」

水科:「ELF 上由 MC 层一律展开成 auipc 加 jalr」

水科:「能不能再压成一条 jal,要到链接期松弛才知道」

水科:「MachO 上才直接发 jal……」

我:「嗯」

水科:「向量配置类,PseudoVSETVLI」

我:「……」

水科:「RISCVInstrInfoVPseudos.td 的 6090 行」

水科:「伪指令是选择已经做完、编码还没做的中间态……」

水科:「存在的原因和第 4 类选择还没做有关……」

水科:「不把地址写法、跳转范围、向量参数这些后面才能确定的信息定死……」

水科:「前面就能少做无用功……」

我:「是、」

我:「有实际的例子吗?」

水科:「全局变量 @x,i64,初值 5。函数 h 把它读出来返回」

水科:「triple 是 riscv64-unknown-linux-gnu」

水科:「load 的 align 是 8……」

我:「啊、」

水科:「不启用 PIC 时 @x 的地址是绝对值,选成 lui 加 ld」

水科:「启用 PIC 时外部可见的符号要经过 GOT 取地址……」

我:「诶、」

水科:「选成 auipc 加 ld 再从表项里读一次」

水科:「汇编里带上重定位记号」

1
2
3
4
5
6
7
# 非 PIC                                   # PIC
h: h:
lui a0, %hi(x) .Lpcrel_hi0:
ld a0, %lo(x)(a0) auipc a0, %got_pcrel_hi(x)
ret ld a0, %pcrel_lo(.Lpcrel_hi0)(a0)
ld a0, 0(a0)
ret

我:「左边三条,右边五条?」

水科:「取地址那一段两边都是两条指令……」

水科:「PIC 多了一次从 GOT 表项读值的 ld」

水科:「总条数比左边多一条……」

水科:「右边 %pcrel_lo 引用的标签必须与上面那条 auipc 配对……」

我:「那个」

三日月:「只是名字不同……」

水科:「这个配对关系是后面汇编器要检查的内容」

我:「链接期松弛会缩短它们吗?」

水科:「因链接器而异。以 lld 的 relaxHi20Lo12 为例,992 行……」

水科:「它对绝对的 medlow 序列 lui 加 ld 做两件事」

水科:「符号的绝对地址落在 12 位可编码范围内时,lui 整条删掉……」

水科:「访存改成以 x0 为基址的 ld……」

我:「嗯、」

我:「……」

水科:「地址落在 gp 正负 2 KiB 内时同理,只是基址换成 gp」

水科:「这就是第五章寄存器表里 gp 那一行的兑现处……」

水科:「GOT 间接那一类它不做,relax 在 1035 行」

我:「诶?」

水科:「分支里只有 HI20、LO12、CALL、CALL_PLT……」

水科:「TPREL 那一族、ALIGN 与 TLSDESC」

我:「便当凉了……」

水科:「没有 GOT_HI20」

水科:「松弛都要编译器先用 +relax 放出 R_RISCV_RELAX 标记」

水科:「relaxable 在 735 行,查的就是紧邻的下一条重定位是不是它……」

我:「那、那个」

水科:「clang 默认开 -mrelax」

我:「真的?」

水科:「RISCV.cpp 的 142 行那个 hasFlag 默认是 true」

水科:「llc 则要显式 -mattr=+relax……」

蝉声在这里断了一下。

不是停,是换了个调子。

我:「这一章定下的是什么?」

水科:「用哪条指令,和数据放哪类寄存器……」

我:「还能重选吗?」

水科:「原则上不。只允许保持语义的局部改写」

座位表在口袋里被压出了一道新的折痕。

我把它摊开,七排十二号那个铅笔圈的中间,正好落在折痕上。

圈还在,号码看不见了。

第五章 Calling Convention 「开场前就分掉的一半」

“9 月 13 日”

十点十五分。

水塔的影子刚好盖住旭座门口那三级台阶。

蝉还在叫,只是叫得比七月敷衍。

卷帘门拉下来了,上面还留着去年那场特别放映的海报。

我把座位表贴在卷帘门上比对了一下。

一百四十四个格子,一个都不少。

水科:「指令选完之后进入寄存器分配……」

水科:「但在此之前,有一批寄存器已经被 ABI 预定了用途」

我:「预定?」

水科:「调用约定就是这份预定表……」

我:「它在 IR 里能看到吗?」

水科:「只留下符号级的痕迹……」

水科:「call 自带 calling convention」

水科:「聚合体参数带 byval 或 sret……」

水科:「哪个值放哪个物理寄存器,不在 IR 层?」

我:「啊、」

我:「那它影响后端的哪些地方?」

水科:「每一个阶段,指令选择要为参数准备正确的寄存器类……」

水科:「寄存器分配要把参数寄存器当作活的,把调用者保存的寄存器当作被破坏的……」

水科:「函数进出还要保存和恢复被调用者保存的寄存器……」

我:「这张表是谁定的?」

水科:「RISC-V 的 psABI」

水科:「processor-specific ABI」

水科:「体系结构相关的 ABI 约定……」

水科:「它把每个寄存器都定死了用途……」

水科:「RISCVRegisterInfo.td 的 105 行起」

我:「不是……」

水科:「let RegAltNameIndices 块」

我:「那个……」

水科:「后端只认这张表」

我:「调用者保存是什么意思?」

水科:「调用别的函数之后值可能被改……」

水科:「调用方要用,就得自己先存」

我:「被调用者保存呢?」

水科:「被调用的函数如果要改它,必须先存好、返回前恢复……」

我:「那从头念一遍吧?」

水科:「三十二个」

我:「三十二个……」

水科:「zero 是 x0,恒为零,写它等于丢弃……」

水科:「ra 是 x1,返回地址,psABI 记作调用者保存……」

水科:「本函数一发起调用就会冲掉 ra,要用就得自己在序言里存好」

水科:「叶子函数从不写它,于是也不用存……」

我:「嗯、」

我:「那个……」

水科:「LLVM 把 X1 列进 CalleeSavedRegs」

水科:「被调用者保存寄存器表……」

水科:「注意别跟前文的 CSR 撞名,那个是控制状态寄存器……」

我:「诶、」

水科:「再由 determineCalleeSaves 按需保存……」

我:「贩卖机又吞钱了……」

我:「sp 呢?」

水科:「x2,栈指针」

我:「gp」

水科:「x3,全局指针,用于相对 gp 的地址」

水科:「省掉取高 20 位的那条 lui 或 auipc……」

水科:「由链接器松弛兑现,第四章末尾那一段」

我:「tp……」

水科:「x4,线程指针……」

水科:「TLS,thread-local storage」

水科:「每个线程自己一份的数据的基址……」

我:「临时寄存器呢?」

水科:「t0 到 t2 是 x5 到 x7,调用者保存……」

水科:「s0 也就是 fp,是 x8,被调用者保存,作帧指针时用 fp 名……」

水科:「s1 是 x9,被调用者保存」

我:「那个……」

水科:「a0 到 a7 是 x10 到 x17,整数参数与返回值……」

我:「……」

水科:「s2 到 s11 是 x18 到 x27,被调用者保存……」

水科:「t3 到 t6 是 x28 到 x31,临时寄存器,调用者保存……」

我:「浮点那边呢?」

水科:「fa0 到 fa7 是 f10 到 f17」

水科:「浮点参数与返回值,F 或 D 扩展……」

水科:「fs0 到 fs11 是 f8、f9 和 f18 到 f27」

水科:「浮点被调用者保存」

水科:「ft0 到 ft11 是 f0 到 f7 和 f28 到 f31」

我:「是、」

水科:「浮点临时寄存器……」

我:「诶、」

我:「向量呢?」

水科:「v0 是掩码寄存器,带掩码的向量指令从它读掩码……」

水科:「此时它不能同时当这条指令的目标」

水科:「v8 到 v23 是向量参数与返回值,调用者保存……」

我:「啊、」

我:「为什么?」

水科:「v1 到 v7、v24 到 v31 是向量被调用者保存」

水科:「RISCVCallingConv.td 的 29 行,CSR_V……」

我:「被调用者保存的那些,汇总在哪里?」

水科:「TableGen 里几个 CalleeSavedRegs 列表……」

水科:「RISCVCallingConv.td 的 18 行」

水科:「CSR_ILP32_LP64,加上 F 和 D 版本,和 V 版本……」

我:「那个」

水科:「寄存器分配之后,函数会按实际用到的情况决定保存哪些」

三日月:「假设还有一个……」

水科:「没用到就不保存……」

水科:「这是栈帧那一段要讲的事……」

我从口袋里摸出铅笔,在座位表上画了一道斜线。

从一排一号到四排十八号,整整一半。

我:「这一半是已经被预定掉的?」

水科:「开场之前就分掉了」

我:「那我们只能坐另一半……」

水科:「你们只能抢剩下的……」

我:「……」

我:「参数怎么分?」

水科:「整数参数依次用 a0 到 a7,浮点参数用 fa0 到 fa7」

水科:「两者是两条独立的通道……」

水科:「返回值分别用 a0、a1 和 fa0、fa1」

我:「麻烦的是别的类型吧?」

水科:「RISC-V 后端的文件头把规则写得很直接……」

水科:「RISCVCallingConv.cpp 的 58 行」

我:「第一条?」

水科:「单个标量从不拆开传给 IR 层,拆分由后端处理……」

我:「第二条呢?」

水科:「结构体如果小于等于 2 乘 XLen,可以拆成两个寄存器传……」

水科:「两个整数、两个浮点、或一整数一浮点,具体取决于字段类型和 ABI……」

水科:「ABI 里浮点参数是否走 f 寄存器由 -mabi 决定」

水科:「lp64 全走整数」

我:「水科,你喘口气……」

水科:「lp64f 和 lp64d 允许 F 和 D……」

我:「大于 2 乘 XLen 的呢?」

水科:「不拆,用指针传」

水科:「IR 里给参数打上 byval,被调用者按值拿到一份拷贝……」

水科:「返回值大于 2 乘 XLen 时,改为由调用者提供缓冲区……」

水科:「把指向它的 sret 指针当第一个隐式参数传……」

我:「可变参数呢?」

水科:「按整数 ABI 传,fa0 到 fa7 不使用」

水科:「这条不在上面那串文件头规则里,是实现写死的……」

水科:「RISCVCallingConv.cpp 的 463 行……」

水科:「AllowFPRForF64 = !ArgFlags.isVarArg()……」

我:「RV32 上传 f64 呢?」

水科:「如果软浮点 ABI 或浮点寄存器已耗尽」

水科:「拆成两个 a 寄存器,或一半在寄存器、一半在栈上……」

水科:「CC_RISCVAssign2XLen,308 行就是这段逻辑……」

我:「为什么最多只占两个寄存器?」

水科:「聚合体最多占两个寄存器是 psABI 直接定的」

水科:「不超过 2 乘 XLen 的拆成两个寄存器传」

水科:「超过就整块按内存传……」

水科:「参数寄存器只有八个,还可能先被软浮点或别的参数占满」

水科:「而一个聚合体最多只要两个……」

我:「嗯、」

我:「嗯、」

水科:「所以部分在寄存器、部分在栈上,只会出现在第二个寄存器不够用这一个边界上」

水科:「调用方与被调用方只需要处理这一种分裂……」

我:「可变参数为什么必须走整数通道?」

水科:「因为被调用者在编译时只看到那三个点,不知道每个实参的类型……」

水科:「只有所有可变实参用同一种通道传,va_arg 才能往下扫」

水科:「va_arg 是按顺序取下一个可变参数的宏……」

水科:「所以浮点实参在可变部分也放进 a 寄存器或栈……」

我:「啊、」

我:「啊、」

水科:「这也是 fa0 到 fa7 不参与可变参数的原因……」

我:「这些规则的实现是谁?」

水科:「CC_RISCV_Impl,401 行,它按顺序尝试 fa、a、栈」

水科:「并处理参数一半在寄存器、一半在栈上的边界情况……」

我:「分配结果落到哪里?」

水科:「三个地方」

水科:「LowerFormalArguments,26996 行……」

水科:「把进来的物理寄存器 liveins 写进 DAG」

我:「那、那个」

水科:「让函数体里的参数值可用,栈上的参数变成当前函数的栈帧对象……」

水科:「LowerCall,27301 行」

我:「那个……」

水科:「把实参从虚拟寄存器或栈槽搬进 a、fa、栈上传参区」

水科:「再生成调用指令……」

水科:「LowerReturn,27822 行」

水科:「把返回值搬进 a0、a1、fa0、fa1……」

我:「liveins 是什么?」

水科:「MIR 里的一个属性列表,写在基本块头上」

水科:「表示进入这个块时哪些物理寄存器已经有值……」

水科:「函数入口块的 liveins 就是 ABI 规定的参数寄存器集合」

我:「看个例子吧?」

水科:「一个函数叫 sum……」

水科:「把 p 指向的 n 个 i64 相加,入口块直接跳到循环块……」

水科:「循环里两个 phi,一个是 i,一个是 s」

水科:「一条 getelementptr inbounds i64」

我:「那个……」

水科:「一条 load i64,align 8……」

水科:「一条 add nsw,一条 add nuw nsw……」

水科:「一条 icmp ult,然后 br,退出块把 s 返回……」

我:「再说一遍?」

我:「它在 -stop-after=finalize-isel 时长什么样?」

水科:「只截入口块,bb.0.entry:……」

水科:「successors: %bb.1(0x80000000)」

水科:「liveins: x10,x10, x11……」

水科:「%6:gpr = COPY x11……」x11……」
我:「……」

水科:「%5:gpr = COPY x10」x10」
我:「那两行注释呢?」

水科:「# %n 和 # %p 是文章加的,llc 并不打印 IR 名字……」

我:「x10 和 x11 是……」

水科:「就是 a0 和 a1,COPY 是机器级复制指令……」

水科:「把物理寄存器里的入参值搬到虚拟寄存器,后面的函数体只跟虚拟寄存器打交道……」

我:「返回的时候呢?」

水科:「反向来一次……」

水科:「x10=COPYx10 = COPY %3……」
水科:「然后 PseudoRET implicit x10……」x10……」
我:「栈呢,函数要用到的栈空间什么时候定?」

水科:「指令选择后逐渐确定,最后由帧布局定下来」

水科:「帧相关的规则集中在 RISCVFrameLowering……」

我:「栈往哪边长?」

水科:「向下」

水科:「最终大小按 ABI 栈对齐向上取整,通常是 16 字节」

水科:「determineFrameLayout,533 行……」

我:「栈对象的地址是怎么算的?」

水科:「每个栈对象有个抽象的编号,FrameIndex」

水科:「具体地址在 eliminateFrameIndex 里变成基址寄存器加偏移……」

水科:「基址是 sp,需要帧指针时是 x8」

水科:「getFrameIndexReference」

水科:「1620 行……」

我:「不是……」

我:「什么时候需要帧指针?」

水科:「栈大小在编译期不确定,比如 alloca……」

水科:「栈对齐超过 ABI 要求」

水科:「或者函数里用了 __builtin_frame_address……」

我:「进出函数的时候呢?」

水科:「进入时按需调整 sp、保存用到的被调用者保存寄存器」

水科:「determineCalleeSaves」

水科:「1829 行决定保哪些,离开时反向恢复……」

水科:「emitPrologue 在 987 行」

我:「够了」

水科:「emitEpilogue 在 1301 行……」

我:「诶、」

我:「ra 是每层都要存的吗?」

水科:「不是……」

水科:「ra 只在函数里真的发生了调用时才需要保存」

水科:「不是 ABI 强制每层都存,机制是,调用指令会隐式写 ra……」

我:「诶、」

我:「嗯、」

水科:「确定保存集合时,凡是函数里被写到过的被调用者保存寄存器都要存」

水科:「没有调用的函数里 ra 从不被写,于是就不进集合……」

我:「有例外吗?」

水科:「用了帧指针的函数是例外,此时 ra 与 x8 无条件保存……」

水科:「determineCalleeSaves 里那段 if (hasFP(MF))」

我:「栈很大的时候呢?」

水科:「栈偏移超过 12 位立即数范围时……」

水科:「sp 的调整要拆成多条 addi……」

水科:「帧指针访问也要先算地址……」

我:「RV32 和 RV64 在这一章的差别是什么?」

水科:「体现在数字上,x 寄存器宽度、指针宽度、栈对齐……」

水科:「E ABI 是 4 或 8」

水科:「以及 RV32 上 64 位值的读写拆成两条」

我:「向量寄存器的栈布局也是这一套吗?」

水科:「单独有一套对齐规则……」

水科:「assignRVVStackObjectOffsets,1943 行」

水科:「RVV 的寄存器长度在编译期只是个范围……」

水科:「所以栈上向量区不能按固定字节数摆」

我:「嗯」

我:「那按什么摆?」

水科:「每个对象按 8 字节和对象自身对齐里较大的那个对齐……」

水科:「不足 8 字节的分数类型补到 8 字节,整段最小 16 字节……」

水科:「最后按最小 vscale 为单位补一次对齐」

水科:「使所有偏移都是 vscale 的倍数」

我:「……」

我:「最小 vscale 怎么算?」

水科:「代码里是 getRealMinVLen() 除以 RVVBitsPerBlock……」

水科:「RVVBitsPerBlock 是 64,所以是最小 VLEN 除以 64……」

水科:「VLEN 是一条向量寄存器的位宽,第七章展开」

我:「序言里这段记成什么?」

水科:「块数乘 vlenb……」

水科:「RVVStackSize 除以 8,乘运行期读出的 vlenb」

水科:「vlenb 是 RVV 的 CSR,值等于 VLEN 除以 8」

水科:「也就是一条向量寄存器的字节数……」

三日月:「呵呵……」

水科:「数值上等价于拿实际 vscale 乘一遍……」

我:「所以这一章定下的是什么?」

水科:「哪些成员能作什么用……」

我:「还能改吗?」

水科:「这张表不是后端定的……」

水科:「后端只认它」

我:「……」

户口这种东西,我从来没觉得它像一张纸。

它更像一行早就写在别人本子上的字。

你去查的时候,它已经在那里了。

我:「水科,那我们的座位呢?」

水科:「我们是剩下的那一半……」

我:「剩下的那一半,也要抢?」

水科:「抢不到就 spill」

我:「spill 是什么?」

水科:「第六章……」

卷帘门上的海报被风掀起一个角。

我把座位表从门上取下来,重新折成四折。

斜线那道铅笔印,正好压在半张纸的对角。

第六章 Interference 「三十二根与一个栈槽」

“9 月 13 日”

八点四十分。

水塔的影子已经盖不住任何东西。

蝉还在叫,只是叫得比七月敷衍。

夜里的屋顶比白天窄,因为能看见的部分只剩灯照到的那一块。

水科:「指令选择的输出是机器指令序列」

水科:「它有一套自己的表示,叫 Machine IR……」

水科:「MIR」

我:「里面有什么?」

水科:「四件」

水科:「MachineFunction,一个函数的机器表示……」

水科:「持有栈帧信息、函数属性、基本块列表,MachineBasicBlock,基本块」

水科:「边上带概率,块头可带 liveins……」

我:「嗯、」

我:「嗯、」

水科:「MachineInstr,一条机器指令」

水科:「操作数是寄存器、立即数、栈帧编号、基本块、符号……」

水科:「还有虚拟寄存器……」

我:「啊、」

我:「虚拟寄存器怎么写?」

水科:「用 %0、%1 这样的编号加寄存器类表示……」

水科:「例如 %5:gpr……」

水科:「寄存器分配前,机器指令里的操作数几乎全是虚拟寄存器……」

我:「MIR 能看吗?」

水科:「有文本序列化格式,MIRLangRef.md」

水科:「llc -stop-after= 可以把任意阶段的状态打印出来……」

水科:「-run-pass= 可以只跑一个 pass」

水科:「它只吃 .mir 输入」

我:「那个」

我:「嗯?」

水科:「先把 -stop-after 的输出存成 .mir,再喂回去……」

水科:「这是后端调试最主要的手段……」

我:「sum 那个例子,循环块长什么样?」

水科:「同样是 -stop-after=finalize-isel 的输出」

水科:「只截循环块,所以 PHI 还在……」

水科:「再往后到分配之前,PHI 就没了,bb.1.loop:……」

水科:「successors: %bb.2(0x04000000),」

水科:「%bb.1(0x7c000000)」

我:「嗯、」

我:「那个……」

水科:「%1:gpr = PHI %5, %bb.0, %4, %bb.1……」

水科:「%2:gpr = PHI %7, %bb.0, %3, %bb.1」

水科:「%12:gpr = LD %1, 0」

水科:「后面跟着 load (s64) from %ir.lsr.iv……」

我:「啊、」

水科:「%3:gpr = nsw ADD %2, killed %12」

我:「那个……」

水科:「%4:gpr = ADDI %1, 8……」

水科:「BNE %4, %0, %bb.1,PseudoBR %bb.2……」

我:「机器层也有 PHI?」

水科:「机器层在寄存器分配之前保持 SSA 形式……」

水科:「%1:gpr,每个虚拟寄存器只被赋值一次」

我:「那上篇那些 SSA 结论还能用吗?」

水科:「同样成立……」

水科:「每条 use 直接指向 def」

水科:「删指令只要看 use 计数是否为零,且指令本身无副作用」

我:「那、那个」

水科:「机器级 CSE、LICM、DCE 于是能沿用中端那套基于单一定义的判据……」

我:「水塔的影子又长了……」

我:「还有一层作用吧?」

水科:「单一定义意味着一个值只对应一个活跃范围……」

水科:「live range,它可能由若干条区间组成……」

水科:「定义在入口、在几段不相交的区域里被使用」

水科:「但不会再出现同一个值被重新定义那种多个活跃范围共享同一虚拟寄存器的情况……」

我:「……」

水科:「每个虚拟寄存器因此只需要记一组区间」

水科:「两个值是否互相干涉,退化成两组区间里有没有重叠的一对……」

水科:「这就是活跃区间分析能工作的前提……」

我:「干涉是什么意思?」

水科:「interfere。两个值的活跃区间有重叠,因而不能分到同一个物理寄存器……」

我:「区间是谁算的?」

水科:「LiveIntervals,算出每个虚拟寄存器的区间……」

水科:「一个 LiveRange 就是一串按程序点排好序的 Segment」

水科:「每段记着起点、终点与产生它的那个定义……」

水科:「两个 LiveRange 重不重叠由 LiveInterval.h」

水科:「的 458 行 overlaps 逐段比较回答……」

我:「不是……」

我:「诶、」

我:「那么在 SSA 还成立的时候,机器级会跑什么?」

水科:「一批与中端同名的优化……」

水科:「通用部分在 addMachineSSAOptimization 里……」

水科:「legacy 路径在 TargetPassConfig.cpp 的 1337 行……」

水科:「NewPM 路径在 CodeGenPassBuilder.cpp 的 731 行」

我:「我不懂……」

我:「嗯、」

水科:「两份清单一致」

我:「顺序呢?」

水科:「提前尾复制,EarlyTailDuplicate……」

水科:「φ 化简,OptimizePHIs,把互相复制的 φ 合并」

水科:「按 lifetime 标记合并互不相交的 alloca 栈对象」

我:「诶、」

水科:「StackColoring……」

水科:「局部栈槽分配,LocalStackSlotAllocation……」

我:「诶?」

水科:「死指令消除,DeadMachineInstructionElim」

水科:「机器组合,MachineCombiner,经 addILPOpts 钩子插入」

水科:「RISC-V 在 legacy 路径上覆写了这个钩子、默认开启……」

我:「嗯」

水科:「NewPM 路径没有覆写,所以只有 legacy 下会跑」

水科:「然后是机器 LICM、机器 CSE、机器 sinking、peephole……」

我:「等一下……」

水科:「最后再消一次死指令……」

我:「算法跟中端一样吗?」

水科:「相同,区别只在于作用对象是机器指令……」

水科:「可以用机器层的额外信息,寄存器类、寻址方式、指令延迟」

我:「RISC-V 自己插了几个?」

水科:「几个……」

水科:「RISCVOptWInstrs,RV64 专有」

水科:「RV64 的 32 位运算有两种写法」

水科:「add 是 64 位,addw 是 32 位……」

水科:「设两个操作数分别为 a 和 b」

我:「慢一点……」

三日月:「呵呵……」

水科:「addw 的结果是 (a+b) 模 2 的 32 次方」

水科:「再做 32 位符号扩展……」

水科:「add 的结果是 (a+b) 模 2 的 64 次方」

我:「那、那个」

水科:「两者模 2 的 32 次方相等……」

我:「啊、」

水科:「差别只在第 31 位以上的位与符号扩展……」

我:「这个 pass 做什么?」

水科:「两件事,删掉多余的 sext.w……」

水科:「addw 的结果本来就是符号扩展过的,再扩一次不变」

水科:「以及在带 w 与不带 w 的版本之间挑一个……」

水科:「方向由目标决定」

我:「那个」

我:「默认是哪个方向?」

水科:「默认,或目标偏好不带 w 的指令时」

水科:「在所有使用者都只看低 32 位的前提下去掉 w 后缀……」

水科:「文件头逐个列了 addw、addiw、mulw、slliw 四种」

水科:「理由各不相同……」

水科:「addw 换成 add,换来的是更大的可压缩寄存器范围……」

我:「嗯、」

水科:「c.add 是 CR 形式」

我:「那个……」

水科:「可编 GPRNoX0 的 31 个……」

水科:「RISCVInstrInfoC.td 的 468 行」

水科:「c.addw 是 CA 形式,只有 GPRC 的 8 个,405 行……」

我:「……」

水科:「mulw 和 slliw 是因为 c.mulw、c.slliw 根本不存在」

我:「那个……」

水科:「而 c.mul 要 Zcb,c.slli 存在……」

水科:「addiw 只为缩小 RV32 与 RV64 的测试差异……」

我:「反过来呢?」

水科:「显式开启、或目标偏好带 W 的指令时则加上 w 后缀」

水科:「add、addi、sub、mul」

水科:「立即数小于 32 的 slli,还有 ld 和 lwu……」

我:「那、那个」

水科:「两个方向的依据都是那条模 2 的 32 次方相等」

我:「罐子空了……」

水科:「使用者不读高 32 位,两个结果在所有被观察到的位上完全相同……」

水科:「RISCVVectorPeephole……」

水科:「把全掩码的 vmerge 化简成 vmv,以及若干向量伪指令的等价改写……」

我:「不是……」

我:「然后?」

水科:「RISCVVLOptimizer,在 VSETVLI 插入之前」

水科:「用稀疏数据流算出每条向量指令实际需要的 VL……」

水科:「把虚高的 VL 操作数改小,以减少后面 vsetvli 需要切换的次数」

我:「那个……」

水科:「VSETVLI 是设置向量长度与元素类型的指令……」

我:「诶、」

水科:「VLMAX 是一条向量指令一次能处理的最大元素数」

水科:「细节在第七章……」

我:「把已经等于 VLMAX 的 AVL 写成常量,是谁做的?」

水科:「另一个 pass,RISCVVectorPeephole……」

水科:「MIR 里是 -1,它文件头最后举的例子就是这个……」

我:「诶、」

我:「嗯、」

水科:「两者合起来决定每条向量指令最终的 VL……」

我:「地址那两个呢?」

水科:「RISCVFoldMemOffset 与 RISCVMergeBaseOffset」

水科:「前面地址拆分时插入的 addi,如果只被访存指令的地址用到……」

水科:「就把它的立即数折进访存指令的 12 位偏移里」

水科:「全局地址的两条指令序列 auipc 加 addi 里」

我:「嗯」

我:「……」

水科:「addi 的部分也尽量折进后续访存……」

我:「挂点一样吗?」

水科:「不同……」

水科:「RISCVFoldMemOffset 确实在上面那批里……」

水科:「RISCVTargetMachine.cpp 的 627 行」

水科:「插在基类那串之前」

水科:「RISCVMergeBaseOffset 更晚」

我:「……」

水科:「在 addPreRegAlloc 里,639 行……」

我:「蝉还在叫……」

我:「刚才那段 MIR 里的记号呢?」

我:「nsw、killed、implicit」

水科:「都是 MIR 的一部分」

水科:「还有 RVV 那一章才会出现的 undef 和 renamable……」

水科:「nsw 与 IR 里的 add nsw 同义」

我:「那个……」

水科:「上篇引理 0.6,undef 表示这次读的值未定义……」

我:「嗯、」

水科:「renamable 是分配器留在物理寄存器上的仍可改名标记」

水科:「killed 表示这个操作数里的寄存器在这条指令之后不再被读……」

水科:「最后一次使用」

水科:「它由 LiveVariables 一类分析算出……」

我:「那个」

三日月:「也许……」

水科:「TwoAddressInstruction 与 RegScavenger 到今天还读它」

水科:「RegScavenger 是分配后临时找物理寄存器的兜底器」

水科:「TargetPassConfig.cpp 的 1502 行那条 FIXME」

水科:「想去的正是这条依赖……」

水科:「MIR 打印也带上它,.s 输出里不带这个标记」

我:「嗯、」

水科:「implicit 表示指令隐式读写某个物理寄存器」

我:「哪个?」

水科:「CSR、vl、vtype 等……」

我:「分配器判断活跃区间也读 killed 吗?」

水科:「不读,靠 LiveIntervals……」

水科:「从 def-use 链加 SlotIndex 算出来……」

水科:「SlotIndex 是函数里按槽位给指令编的号……」

我:「伪指令是什么时候展开的?」

水科:「不是一次性展开的,分成四批,按越晚展开越安全排……」

我:「哪四批?」

水科:「寄存器分配前,地址类」

水科:「PseudoLLA、PseudoLGA、PseudoLA_TLS 那一族」

水科:「RISCVExpandPseudoPreRA.cpp……」

水科:「寄存器分配后,PseudoMovImm、PseudoMovAddr 等」

我:「诶?」

水科:「RISCVExpandPseudoPostRA.cpp……」

我:「那个……」

水科:「发射前,条件运算 PseudoCC 那一族、RV32 的 Zdinx 读写等……」

水科:「RISCVExpandPseudoPreEmit.cpp」

水科:「最后,原子指令 PseudoAtomic 和 PseudoMaskedAtomic……」

我:「那、那个」

水科:「RISCVExpandPseudoAtomics.cpp」

我:「鞋带松了……」

水科:「四个文件都在第 9 行有说明……」

我:「四批的公因子是什么?」

水科:「展开得越早,展开后的指令越可能被其他 pass 优化……」

水科:「展开得越晚,展开时能用的信息越多,也越不容易被其他 pass 破坏」

我:「地址类为什么要在分配前?」

水科:「因为展开会引入新的寄存器需求,分配器必须能看见它们……」

水科:「PseudoLLA 展开成 auipc 加 addi」

水科:「需要一条额外寄存器放中间值……」

水科:「在分配前展开,这条寄存器就是普通虚拟寄存器……」

水科:「会被算进干涉图」

我:「不是……」

我:「……」

水科:「干涉图的顶点是虚拟寄存器,边表示两个活跃区间重叠……」

水科:「若留到分配后,寄存器已分完,展开时只能临时找一条空闲寄存器……」

水科:「或者像分支松弛那样在栈上预留一个位置……」

我:「原子为什么放到最后?」

水科:「因为 lr 和 sc 循环有前向进展要求」

水科:「循环体内不能出现可能失败的访存、不能有其他原子操作……」

水科:「中间任何 pass 插入的指令都可能破坏这个要求……」

水科:「RISCVExpandPseudoAtomics.cpp 的文件头原话就是这个理由」

我:「够了」

我:「PseudoCALL 和 PseudoTAIL 在哪一批?」

水科:「不在这四批里」

水科:「输出汇编文本时它们打印成 call 和 tail」

水科:「由后续的汇编器展开……」

我:「诶、」

水科:「输出目标文件时由 MC 层的 expandFunctionCall」

水科:「直接展开成 auipc 加 jalr」

水科:「最后能不能变成一条 jal 由链接器决定……」

我:「诶、」

我:「那么寄存器分配要解决什么?」

水科:「无限多的虚拟寄存器,塞进有限的物理寄存器……」

水科:「每个虚拟寄存器有一个活跃区间」

水科:「它被定义的点,以及从那里到最后一次使用的点之间所有还活着的程序点……」

水科:「两个活跃区间重叠的虚拟寄存器,不能分到同一个物理寄存器」

我:「嗯」

我:「嗯、」

我:「这个问题还能怎么说?」

水科:「可以原样陈述成图着色……」

水科:「把每个虚拟寄存器看成图的一个顶点,两个虚拟寄存器干涉就连一条边」

水科:「能不能用 K 个物理寄存器?」

水科:「就是这张干涉图能不能用 K 种颜色染?」

我:「……」

水科:「染色是给每个顶点配一种颜色,相邻顶点颜色不同……」

我:「这个问题难吗?」

水科:「干涉图可以任意图实现,这一步归约以及由此得到的 NP 完全性」

水科:「标准出处是 Sethi 那篇 Complete」

水科:「Register Allocation Problems……」

水科:「SIAM J. Comput.,1975」

水科:「把寄存器分配写成图着色、并据此给出化简、染色、溢出启发式的……」

我:「是、」

水科:「是 Chaitin 等人,SIGPLAN 82」

我:「还有?」

水科:「下面只给直觉,不是构造性证明」

我:「直觉是什么?」

水科:「任给一张图,可以把顶点写成若干个值……」

水科:「并用分支和循环安排它们的活跃范围」

水科:「使得两个顶点相邻与两个值的活跃区间重叠一一对应……」

我:「啊、」

水科:「于是 G 能不能用 K 色染?」

水科:「就变回这段代码能不能只用 K 个寄存器?」

我:「嗯、」

我:「既然 NP 难,工程上怎么办?」

水科:「不去求它,转而做启发式……」

水科:「有一条对照值得记……」

水科:「SSA 形式下最优寄存器分配是多项式时间的」

水科:「Hack 与 Goos,IPL 2006……」

我:「那个」

我:「啊、」

水科:「SSA 程序的干涉图是弦图」

水科:「弦图是每个长度不小于 4 的环上都有一条弦……」

水科:「把环切成两段更短的环……」

水科:「这类图的最优染色多项式可解」

我:「那 LLVM 为什么不在 SSA 上分配?」

水科:「它仍然选择先跑 PHIElimination、退出 SSA 之后才分配……」

水科:「它的 RegisterCoalescer 要合并的 COPY……」

水科:「有一大批正是 PHIElimination 把 φ 展开出来的」

水科:「所以顺序上必须先退出 SSA……」

水科:「这是实现选择,不是理论上的必需,SSA 形式上的合并另有成熟算法」

我:「嗯、」

三日月:「都不是……」

水科:「就是 Hack 与 Goos 那篇所属的那条研究线……」

水科:「代价是分配器面对的干涉图不再保证弦性……」

我:「LLVM 用的是哪个分配器?」

水科:「贪心分配器,RAGreedy,它被安排在一串准备工作之后……」

水科:「legacy 路径在 TargetPassConfig.cpp 的 1493 行」

水科:「addOptimizedRegAlloc」

水科:「NewPM 路径在 CodeGenPassBuilder.cpp 的 849 行」

我:「啊、」

我:「那个……」

我:「准备工作有哪些?」

水科:「十步……」

水科:「一,DetectDeadLanes,找出哪些子寄存器位从未被读」

水科:「二,InitUndef,给未定义的读补一个 undef……」

我:「那、那个」

我:「慢一点……」

水科:「三,ProcessImplicitDefs,把隐式定义转成显式定义」

水科:「四,UnreachableMachineBlockElim」

水科:「LiveVariables、MachineLoopInfo……」

水科:「五,PHIElimination」

我:「……」

水科:「把 φ 节点变成前驱块末尾的复制,机器级 SSA 在这里结束……」

水科:「它之后每个虚拟寄存器可以有多个定义点,每个前驱一条复制……」

水科:「一个值只有一个定义,不再成立,此时一个虚拟寄存器对应一个活跃范围」

水科:「但它内部要按定义点分段……」

我:「真的?」

水科:「每一段配一份版本信息,VNInfo,标记某一段区间是哪个定义点产生的……」

水科:「分析时把不同版本的区间当作不同的值」

水科:「机器 CSE 一类靠单一定义做值编号的优化也不能再跑……」

我:「嗯、」

水科:「通用流水线把机器 SSA 优化全部安排在它之前,原因就在这」

水科:「六,TwoAddressInstruction」

水科:「处理两地址指令……」

水科:「目标寄存器同时是源操作数」

水科:「RISC-V 的三操作数格式基本不需要这一步」

我:「……」

水科:「但通用流水线上仍会跑……」

水科:「七,RegisterCoalescer,把 COPY 的两端合并到同一个寄存器」

水科:「能省掉复制的尽量省掉……」

水科:「八,RenameIndependentSubregs……」

水科:「把位域互不干涉的子寄存器拆成独立的虚拟寄存器……」

我:「便当凉了……」

水科:「九,机器调度,pre-RA scheduling」

水科:「十,分配本身,RAGreedy,然后是 VirtRegRewriter」

水科:「把虚拟寄存器全部换成物理寄存器……」

我:「贪心是怎么贪的?」

水科:「按溢出代价给区间排序……」

水科:「代价高、冲突多的先分……」

水科:「分不到就先尝试抢占,evict,优先级更低的区间、把它挪走……」

水科:「再不行把这个区间拆成几段」

我:「嗯、」

水科:「让每段落到不同寄存器,或与其他区间错开……」

水科:「最后才整段溢出……」

水科:「把值写回栈,spill,用的时候再读回来,reload……」

我:「啊、」

我:「为什么溢出是一个总逃得掉的出口?」

水科:「靠两个常数上界」

我:「哪两个?」

水科:「第一个,一条指令同时占用的寄存器操作数有上界」

水科:「RISC-V 基础的整数指令最多三个,两个源加一个目标……」

水科:「最宽的融合乘加是 R4 形式,三个源加一个目标,而且属于浮点寄存器」

水科:「再多的是 RVV 的带掩码伪指令……」

水科:「一个目标加 passthrough、两个源、一个掩码」

我:「那个……」

水科:「passthrough 是给这条指令本轮不处理的那些元素位置提供初值的操作数……」

水科:「寄存器操作数一共五个」

水科:「其中目标与 passthrough 是绑定的同一个寄存器……」

水科:「所以同时占用的物理寄存器是四个」

我:「第二个呢?」

水科:「通用寄存器类里有几十条可分配的物理寄存器……」

水科:「GPR 的 32 个里,x0、sp、gp、tp 恒被保留」

水科:「要帧指针时再加 x8……」

水科:「实际可分配 27 到 28 条,FPR 的 32 条全可分配」

水科:「RVV 的组类和 GPRC、VMV0 这类专用子类除外……」

三日月:「注释而已……」

水科:「后者是绑死单个寄存器或只剩几个成员的类」

我:「等一下,32 减 4 是 28……」

水科:「嗯……」

我:「那 27 是从哪里来的?」

水科:「你自己刚才问过我什么?」

我:「……帧指针……」

水科:「x8」

我:「所以要帧指针的时候是 27,不要的时候是 28」

水科:「你算得对……」

我:「啊哈哈,我刚才差点要说你记错了……」

水科:「超×1000 蠢……」

我:「……」

神代:「三十二根!」

我:「诶……」

神代:「三十二根 GPR!三十二根 FPR!三十二根 VR!」

神代:「LMUL 说我要两根,于是它们就是两根!」

神代:「RVV 说我要八根,于是它们就是八根!」

神代:「而干涉图说,不行!」

神代:「三十二三十二三十二三十二三十二三十二三十二三十二!」

神代:「染色的人是王!溢出的人也是王!」

神代:「连那个应急槽都是王的!因为凡是定下来的,都要由我来定!」

我:「啊,可是 x0 那一根,写进去就等于丢掉……」

神代:「……」

风把屋顶的铁皮吹得响了一下。

她没有再说话。

我:「那么有了这两个上界,就能怎样?」

水科:「分配器总可以退化成只保留当前指令要用的值在寄存器里」

水科:「其余全部 spill」

水科:「寄存器压力于是有常数上界,总找得到解……」

我:「还差一环吧,spill 和 reload 自己也要一条寄存器……」

水科:「这一环由兜底机制补上」

水科:「RegScavenger 反向扫一条空闲寄存器……」

水科:「扫不到就用帧里预留的应急槽,第八章那个函数内松弛用的是同一套……」

我:「贩卖机又吞钱了……」

我:「有解不了的时候吗?」

水科:「例外只有两类……」

水科:「某个寄存器类里的寄存器全被保留……」

水科:「例如全部留给内联汇编或特殊寄存器」

水科:「或者一条指令要求同时占用的操作数超过该类的可分配数量……」

水科:「此时分配器报错,而不是悄悄生成错代码」

我:「……」

我:「溢出会改变值吗?」

水科:「不改变……」

水科:「同一个栈槽只被活跃区间互不重叠的值复用」

水科:「每个定义点之后写、最后一次使用之前读……」

水科:「所以读回来的就是这次写进去的值,能重新算出结果的指令则直接重算……」

水科:「rematerialization」

我:「诶、」

水科:「例如重放一条 addi,比读一次内存便宜……」

水科:「重算的前提是这条指令没有副作用,且它的操作数在重算的位置仍然可用……」

我:「溢出的位置呢?」

水科:「由分配器创建栈槽,加入 MachineFrameInfo……」

水科:「MachineFrameInfo」

水科:「是机器函数里记录这个函数有哪些栈对象、多大、怎么对齐的容器」

水科:「后面帧布局时统一安排……」

我:「嗯、」

我:「所以 spill 就是被赶出剧场的人」

水科:「被赶到走廊……」

我:「走廊也有编号?」

水科:「栈槽有」

我:「……」

1∶本校生无名氏∶2026/09/13(土) 21:04:11 ID:Sp11ot
把整个函数都 spill 到栈上了。
一条 add 也要先 load 再 store。
可是它跑得比你们那个优化版还快。

2∶本校生无名氏∶2026/09/13(土) 21:07:52 ID:vlenb
那是缓存的功劳 www

3∶本校生无名氏∶2026/09/13(土) 21:11:30 ID:GPRC

2
不同意。GPRC 只有 8 个成员,你先算算干涉图。

4∶本校生无名氏∶2026/09/13(土) 21:15:02 ID:x0
x0 恒为零,写它等于丢弃(笑)

5∶本校生无名氏∶2026/09/13(土) 21:19:44 ID:Sp11ot

4
我知道,我说的不是 x0。
我说的是,分配器报错的时候,它至少会告诉你它放弃了。

6∶本校生无名氏∶2026/09/13(土) 21:26:03 ID:vlenb

5
那你现在在走廊上第几号。

7∶本校生无名氏∶2026/09/13(土) 21:31:47 ID:Sp11ot

6
栈对齐 16。所以是 16 的倍数。
具体哪一个,要看帧布局那天心情。

我:「这个板子是谁贴的?」

水科:「不知道……」

我:「你还看这种东西?」

水科:「只看失败的帖子」

我:「为什么?」

水科:「成功的帖子没有行号……」

我:「RISC-V 在寄存器分配这一层有自己的东西吗?」

水科:「三点……」

水科:「第一,寄存器类的分类……」

水科:「不同寄存器类之间不能互换……」

水科:「一条指令的操作数声明了类,就不能改用别的类的寄存器」

水科:「GPR、FPR、VR 分属三个独立的寄存器文件」

我:「……」

水科:「分配器分别处理……」

水科:「同一条指令选错了寄存器类,后面没有补救手段」

我:「这一层是目标相关的吗?」

水科:「这一层本身是目标无关的……」

水科:「通用分配器只按类的约束求解,不为某个类开后门」

水科:「下一条例外……」

水科:「但类怎么划、划多大是目标在 .td 里定的」

水科:「划分直接决定分配的难度……」

我:「第二点呢?」

水科:「RVV 用单独的分配器」

水科:「RVV 的寄存器可以组成 2 个、4 个、8 个一组」

水科:「LMUL,寄存器组倍数,第七章讲……」

水科:「组内编号要求连续且起始编号对齐」

水科:「寄存器类因此派生出 VRM2、VRM4、VRM8 这类超寄存器来表示整组……」

水科:「super register」

我:「水科,你喘口气……」

水科:「把一组物理寄存器当成一个分配单位……」

水科:「组内各成员是它的子寄存器……」

水科:「RISC-V 后端为向量值单独跑一轮分配……」

水科:「单独的分配器默认 greedy」

水科:「在 RISCVTargetMachine.cpp 的 433 行」

我:「再说一遍?」

水科:「就是把 onlyAllocateRVVReg 这个谓词交给通用分配器……」

水科:「334 行,交给它的地方在 363 到 372 行」

水科:「非 RVV 寄存器仍走默认分配器……」

我:「两轮的顺序呢?」

水科:「先让 RVV 分配器把所有向量值定型并替换成物理寄存器」

水科:「再跑通用分配器……」

水科:「这样通用分配器看到的向量值已经是占住一组寄存器的固定占用」

水科:「只需避开这些寄存器给整数与浮点值分配……」

我:「啊、」

水科:「拆成两轮是为了让向量值先定型成物理寄存器」

水科:「好让下一章的 RISCVInsertVSETVLI」

水科:「在 RVV 那一轮分配之后插入 vsetvli……」

水科:「引入 RVV 分配器的提交 ac4868ea3c49」

水科:「写的就是为 post-RA 的 vsetvli 插入做准备」

水科:「这条时序在 legacy 路径上……」

我:「那个……」

水科:「NewPM 路径的缺项见第二章,保留寄存器……」

水科:「sp、gp、tp、fp、CSR、ssp,在分配时被移出可分配集合……」

水科:「这是 getReservedRegs 的直接效果……」

我:「那个……」

我:「分配完之后呢,虚拟寄存器还在吗?」

水科:「不存在了,MIR 里只剩物理寄存器和 COPY」

水科:「之后的优化必须按物理寄存器考虑……」

水科:「能做的只剩在不改变寄存器分配结果的前提下化简指令……」

三日月:「还没被填进去……」

我:「有哪些?」

水科:「MachineCopyPropagation」

水科:「消掉多余的复制」

水科:「StackSlotColoring,合并 spill 槽」

水科:「与寄存器分配共用一个 pass 阶段……」

水科:「注意它和上面那个 StackColoring 是两个 pass」

我:「……」

水科:「那个合局部变量 alloca 对象,这个合溢出槽……」

水科:「MachineLICM,把 reload 提到循环外」

水科:「后置调度,post-RA scheduler」

水科:「按指令延迟重排,但不改变寄存器占用……」

水科:「帧的插入,也就是第五章那套进入和离开的代码……」

水科:「RISCVRedundantCopyElimination……」

我:「诶、」

水科:「删掉 beqz 和 bnez 目标块开头的那个把 x0 复制过去」

我:「Zcmp 那两个呢?」

水科:「RISCVMoveMerger 与 RISCVPushPopOptimizer……」

水科:「Zcmp 扩展的 cm.push 和 cm.pop 把多条保存或恢复合成一条……」

水科:「这一段由帧布局在进出函数时生成,需要 Zcmp……」

水科:「RISCVMoveMerger 把成对的寄存器移动合并成」

我:「嗯?」

水科:「cm.mva01s 和 cm.mvsa01」

水科:「RISCVPushPopOptimizer 把 cm.pop」

水科:「后面紧接的 ret 合成一条 cm.popret……」

水科:「返回值是常数 0 时去掉那条 a0 = x0 复制」

水科:「改用 cm.popretz」

我:「……」

我:「RISCVMakeCompressible 是做什么的?」

水科:「如果有可压缩的寄存器空闲……」

水科:「把访存指令的基址或数据先复制过去,让整条指令能用 2 字节压缩格式……」

水科:「或者把过大的偏移拆成先复制基址再加小偏移」

我:「收益怎么算?」

水科:「按字节数算」

水科:「复制一条寄存器用 c.mv 或 c.li,花 2 字节……」

水科:「此后每有一条指令因此能压缩就省 2 字节……」

水科:「所以同一个寄存器至少被两条访存指令用到时才净赚」

水科:「文件头 63 行那句 only applied」

水科:「if there are enough uses」

我:「我记不住这么多……」

水科:「说的就是这个不等式……」

我:「门槛的数字在哪?」

水科:「在代码里」

水科:「397 到 400 行的 CopyCost 默认 1」

水科:「BaseCost 默认 3……」

水科:「判定是 MIs.size() <= CopyCost 就放弃」

水科:「偏移过大的情形门槛高一档……」

我:「嗯、」

水科:「调整基址的那条 addi 自己就是非压缩的 4 字节」

水科:「每条访存省 2 字节,所以要至少三条访存才净赚……」

水科:「对应同一处多出来的那个条件」

水科:「RegImm.Imm != 0 且 MIs.size() < BaseCost」

我:「啊、」

我:「Zilsd 那两个 pass 呢?」

水科:「RISCVZilsdOptimizer 与 RISCVLoadStoreOptimizer」

水科:「把两条相邻的 32 位 load 或 store 合成一条 64 位的 ld 或 sd……」

水科:「Zilsd 扩展只在 RV32 上有意义」

水科:「指令的谓词是 HasStdExtZilsd 加 IsRV32……」

水科:「RISCVInstrInfoZilsd.td 的 36 行」

水科:「两个 pass 的入口条件也都带 !is64Bit()……」

我:「那个……」

水科:「RISCVLoadStoreOptimizer.cpp 的 141 行」

水科:「实测 llc -mtriple=riscv64 -mattr=+zilsd 直接报错」

水科:「说 zilsd 只对 rv32 支持……」

我:「两个 pass 各管一半?」

水科:「RISCVZilsdOptimizer 在寄存器分配前把访存排在一起」

水科:「并给出应该用哪对连续寄存器的提示,预分配阶段……」

水科:「分配之后由 RISCVLoadStoreOptimizer 检查寄存器是否真的凑成了对……」

水科:「没凑成就把不合法的那条拆回去……」

水科:「后分配阶段就合并进这个 pass……」

我:「那个……」

水科:「文件头把它单列为 Post-allocation Zilsd decomposition」

我:「它只服务 Zilsd 吗?」

水科:「不只……」

水科:「它同时还做 MIPS 风格与 Qualcomm Xqcilsm 的访存配对」

水科:「126 行的 useMIPSLoadStorePairs()」

水科:「或 hasVendorXqcilsm()……」

我:「诶?」

水科:「那两条走 tryToPairLdStInst」

水科:「与 Zilsd 的 fixInvalidRegPairOp 是两个分支……」

我:「那么第三类选择,到这里就讲完了?」

水科:「讲完了……」

水科:「函数里每个值要么在某个物理寄存器里,要么在某个栈槽里」

水科:「不再有还没定的值……」

我:「……」

我:「户口办完了……」

水科:「办完了」

我:「办完就不能改了?」

水科:「此后一切按物理寄存器写」

我从口袋里摸出铅笔。

七排十二号那个圈,我用铅笔横着划掉了。

然后在它旁边写了一个字。

栈。

字很小,写在两排格子之间的空白上。

第七章 vsetvli 「长度在编译期未知」

“9 月 13 日”

十一点二十分。

水塔的影子已经找不到地方可以盖。

蝉还在叫,只是叫得比七月敷衍。

三日月坐在通风口旁边,手里没有拿任何东西。

我:「前面几章都有一个前提吧?」

水科:「一个值的大小在编译期是定的……」

水科:「放得下就在寄存器里,放不下就拆,RVV 去掉了这个前提……」

我:「RVV 是……」

水科:「RISC-V Vector,V 扩展」

水科:「一条向量指令处理多少个元素,由运行期决定……」

水科:「由 vtype 和 vl 决定,而不是由指令编码决定……」

水科:「vtype 里是元素宽度 SEW 和寄存器组倍数 LMUL……」

水科:「vl 是本次处理的元素数……」

三日月:「那一格是空的……」

我:「VLEN 呢?」

水科:「一条向量寄存器的位宽」

水科:「规范要求它是 2 的幂,上界 65536……」

水科:「下界分两档,嵌入式子集 Zve32x 只要求 32」

水科:「对应 FeatureStdExtZvl32b,650 行」

水科:「Zvl 系列一路排到 65536……」

我:「诶、」

水科:「面向应用处理器的完整 V 扩展才要求 128……」

水科:「698 行的 FeatureStdExtV 蕴含 Zvl128b 和 Zve64d」

我:「LLVM 编译期取哪一档?」

水科:「卡在两档之间……」

水科:「断言只要求 64 到 65536」

水科:「RISCVTargetMachine.cpp 的 221 行一带」

水科:「注释里留着支持 VLEN=32 后再改的 FIXME……」

我:「嗯、」

水科:「官方文档 RISCVUsage.md 的 304 行也写明」

水科:「LLVM 目前假设最小 VLEN 是 64 位」

水科:「具体取值由 -riscv-v-vector-bits-min 和 -max 收窄」

水科:「单位是 bit……」

水科:「或者函数属性 vscale_range(N, M)……」

我:「……」

水科:「单位是 vscale,乘上 RVVBitsPerBlock」

水科:「也就是 64,才是 bit」

我:「那么同一份二进制换一台机器跑呢?」

水科:「每条向量指令处理的元素数不同……」

水科:「但结果都正确」

三日月:「假设 1……那台机器的 VLEN 是 128……」

我:「嗯……」

三日月:「假设 2……它也可能是 256……」

三日月:「而你手上的二进制一个字都没有改……」

我:「那,那就……」

三日月:「假设 3……也许根本没有一台机器,只有一张写着范围的纸……」

我:「假设 4 呢?」

三日月:「假设 4……vscale 不是一个数……」

三日月:「是一个还没有被填进去的空……」

三日月:「呵呵……」

三日月:「这些都只是注释。你想要多少,就可以增加到多少……」

我:「……」

我:「那三件在编译期未知的事,具体是什么?」

水科:「SEW,元素宽度,8、16、32、64 位……」

水科:「LMUL,一条向量指令占几个寄存器」

水科:「1、2、4、8,或分数 1/2、1/4、1/8……」

水科:「寄存器组内编号必须连续,且起始编号按组大小对齐」

水科:「vl,当前指令处理多少个元素……」

水科:「最大值是 VLMAX」

我:「等一下……」

水科:「等于 VLEN 除以 SEW,再乘 LMUL」

我:「这三个值谁写进去?」

水科:「vsetvli 和 vsetivli 指令」

水科:「写进 vtype 和 vl 两个 CSR……」

水科:「之后的向量指令隐式读取它们,两条相同的 vadd.vv」

水科:「在不同 vtype 和 vl 下做的事情不同……」

水科:「而 vtype 和 vl 是隐含的,指令编码里没有它们」

我:「嗯、」

我:「这样设计的好处是什么?」

水科:「向量代码长度与向量长度无关,循环不需要按 VLEN 展开……」

水科:「代价是编译器必须显式维护这层状态……」

我:「vtype 里还有别的吗?」

水科:「两个策略位」

水科:「RISCVInstrInfoVPseudos.td 的 27 行」

水科:「文件头有完整说明……」

水科:「tail policy」

我:「然后?」

水科:「vl 之后的元素是保持不变还是 agnostic」

水科:「保持不变叫 undisturbed……」

水科:「mask policy」

水科:「被掩码关闭的元素是保持不变还是 agnostic」

我:「agnostic 是随便写吗?」

水科:「不等于随便写……」

水科:「规范里它指保留原值,或把这些 lane 写成全 1……」

水科:「lane 是向量里的一个元素位置……」

水科:「而且每个 lane 可以独立选」

水科:「LLVM 在这两态之外还多记一个规范里没有的 Undefined 态……」

水科:「伪指令上那个给出非活跃 lane 初值的 passthrough 操作数」

我:「那个……」

水科:「是 IMPLICIT_DEF,也就是 MIR 的未定义值……」

水科:「或者 noreg,MIR里这里没有寄存器的占位……」noreg,MIR 里这里没有寄存器的占位……」
水科:「这时候这些 lane 才是真任意,可以随便改而不影响程序语义……」

我:「伪指令的名字为什么那么长?」

水科:「命名与操作数划分由 RISCVInstrInfoVPseudos.td」

水科:「的 9 行文件头说明……」

水科:「大原则是,影响寄存器类的字段用一族伪指令,写进名字」

水科:「不影响寄存器类的字段用 dummy 操作数传递……」

我:「影响寄存器类的有哪些?」

水科:「LMUL 是一族,_M1、_M2、_M4、_M8」

水科:「以及分数 LMUL 的 _MF2、_MF4、_MF8……」

水科:「M1 的虚拟寄存器属于单寄存器类,M2 就属于两寄存器一组的类」

水科:「masked 和 unmasked 也会改变寄存器重叠约束」

我:「那个……」

水科:「所以 _MASK 后缀也是一族……」

我:「那 _E32 呢,SEW 不是不影响寄存器类吗?」

水科:「看上去与大原则矛盾」

水科:「原因是同一条真实指令要按 SEW 拆成多条 TableGen 记录……」

水科:「反查表 RISCVVInversePseudosTable 从 599 行起」

水科:「主键是 BaseInstr、VLMul、SEW、IsAltFmt,604 行……」

我:「罐子空了……」

水科:「名字里的 _E 加 sew 就是这个主键的可见部分」

水科:「而且只在 SEW 确实相关时才加」

水科:「2097 行的 !if(sew, …)……」

水科:「581 行的注释说」

水科:「SEW 等于 0 表示这条伪指令不区分 SEW」

我:「……」

我:「为什么要拆开?」

水科:「为了让谓词与调度信息能按 SEW 挂上去……」

水科:「整数 SEW=64 需要 HasVInstructionsI64」

水科:「802 行的 GetVTypePredicates」

水科:「调度类也可以带上具体 SEW」

水科:「2082 行的 SchedBinary……」

水科:「SEW 的数值另外还作为操作数再传一遍」

三日月:「谁知道呢……」

水科:「供 VSETVLI 插入等 pass 使用」

水科:「用操作数传递的是 vl、sew、策略位……」

水科:「后面那个 vadd 例子里的 4 是 vl、5 是 e32、1 是 ta, mu」

水科:「就是这些……」

我:「嗯、」

我:「IR 里怎么表示长度未知的向量?」

水科:「」

水科:「vscale 是运行期确定的缩放因子……」

水科:「RISC-V 的定义是 vscale 等于 VLEN 除以 64」

水科:「RISCV::RVVBitsPerBlock 是 64」

水科:「RISCVTargetParser.h 的 69 行」

我:「哪个?」

水科:「所以 <vscale x 2 x i32> 在 VLEN=128 时是 4 个 i32……」

水科:「在 VLEN=256 时是 8 个……」

我:「为什么取 64 作基准?」

水科:「可扩展类型需要一个与具体 VLEN 无关的已知最小块……」

水科:「<vscale x 1 x i64> 正好是」

水科:「LMUL=1、SEW=64 时一条向量寄存器的容量」

水科:「其它元素宽度都按 64 位一块折算……」

水科:「vscale 就与 LMUL 的定义对上了……」

我:「那固定长度的向量呢,比如 <4 x i32>?」

水科:「IR 里也可以直接写」

水科:「怎么落到 RVV 上?」

水科:「由 useRVVForFixedLengthVectorVT 决定」

水科:「3388 行……」

水科:「还有 getContainerForFixedLengthVector,3429 行」

水科:「固定长度向量在 DAG 里被放进一个可扩展的容器类型」

我:「蝉还在叫……」

水科:「运算做完再抽出来,抽出时把长度限制回原来的固定值……」

我:「为什么需要容器?」

水科:「RVV 指令的操作粒度是整条向量寄存器……」

水科:「没有恰好 4 个 float 这样的指令形式」

水科:「都放进容器后,运算可以用同一套可扩展类型的模式选指令……」

水科:「不必为每种固定长度单独写一套规则」

水科:「这套转换只在 V 扩展存在、且长度确实能被 RVV 高效处理时才启用……」

我:「嗯、」

我:「vscale 是一个具体的数吗?」

水科:「不是,是一个范围」

水科:「-mattr=+v 默认的最小 VLEN 是 128」

水科:「zvl128b……」

水科:「vscale_range 属性或」

我:「啊、」

水科:「-riscv-v-vector-bits-min、-max 能把它收窄」

水科:「收窄的价值在代价模型上……」

水科:「getVScaleForTuning」

水科:「RISCVTargetTransformInfo.cpp 的 453 行」

水科:「把未知的 vscale 换成某个代表值来估成本和决定展开因子……」

我:「那么 RVV 后端里最核心的 pass 是哪个?」

水科:「RISCVInsertVSETVLI,13 行……」

水科:「任务是给每条向量指令补上它需要的 vsetvli」

水科:「同时又不能每条都补……」

水科:「连续的向量指令如果要求的 SEW、LMUL、策略、vl 都一样,就应该共用一条」

水科:「它把每个程序点上 vtype 和 vl 是什么做成一个数据流问题……」

我:「状态是什么?」

水科:「AVL,application vector length」

水科:「最近一次 vsetvli 拿到的那个参数……」

水科:「一个立即数、一个虚拟寄存器连同它的版本号,或者 VLMAX」

水科:「寄存器带版本号是为了区分同一个寄存器号被重新定义过……」

水科:「SEW 与 LMUL。如果只关心两者的比,就只保留这个比」

水科:「tail 与 mask 两个策略位」

我:「那个……」

水科:「外加一个保底值 Unknown」

水科:「表示这里的 vtype 和 vl 在编译期无法确定……」

水科:「两个状态可以比较……」

水科:「一个状态如果能满足一条指令的要求,要求的字段它都有且取值相容……」

我:「那个……」

水科:「这条指令就不需要新的 vsetvli……」

我:「几个阶段?」

水科:「三个,第一阶段,逐块摘要」

水科:「从基本块当前的入口状态出发,逐条指令更新状态……」

水科:「若当前状态与这条指令的要求不兼容,就把状态改成指令要求的值」

水科:「块走完得到出口状态……」

我:「鞋带松了……」

水科:「因为兼容性判定会看入口状态,入口一变出口就要重算」

水科:「第二阶段,块间传播……」

水科:「块入口状态是全部前驱出口状态的合并,完全相同就保留……」

水科:「只有 AVL 与 VLMAX 相同」

我:「还有?」

水科:「就只保留 SEW 与 LMUL 的比,否则记 Unknown……」

水科:「按 worklist 反复传播到不再变化」

水科:「合并只会让状态更保守,状态取值只有有限多个」

水科:「所以这个迭代必然停……」

水科:「上篇 T1 的有限高度格」

我:「诶、」

水科:「代码在第二、第三阶段之间还插了一步部分冗余消除……」

水科:「把多个前驱能共用的 vsetvli 提前……」

水科:「第三阶段,插入……」

水科:「从块入口状态出发逐条走」

三日月:「只是名字不同……」

水科:「状态兼容就继续,不兼容就生成一条 PseudoVSETVLI」

水科:「或 PseudoVSETIVLI……」

水科:「把要求的状态写出来,并把跟踪的状态更新成它要求的值……」

水科:「之后再做一轮块内合并与死代码消除」

水科:「把相邻的重复 vsetvli 合并掉……」

我:「……」

我:「它会不会漏插?」

水科:「不会,第二阶段的合并是保守的」

水科:「合并出来的状态在每条前驱路径上都被满足……」

水科:「两个前驱都确定的字段才保留,只要有一个不确定就记 Unknown」

水科:「第三阶段只在状态与要求不兼容时才插,插完后状态就与要求一致……」

我:「我不懂……」

水科:「沿任意一条执行路径归纳,每条向量指令执行前」

水科:「最后一次写入 vtype 和 vl 的状态都满足这条指令的要求……」

水科:「把状态记成 Unknown 只会让插入变多」

水科:「不会让该插的插不上」

我:「这个 pass 跑在什么时候?」

水科:「RVV 寄存器已经分配并重写之后、通用寄存器分配之前……」

水科:「legacy 路径的时序,它跑的时候向量值已经是物理寄存器」

水科:「整数和浮点值可能还是虚拟寄存器」

我:「看个例子吧?」

水科:「一个函数只做两个 <4 x float> 的加法……」

水科:「-stop-after=riscv-isel 时……」

水科:「MIR 里还没有 vsetvli,伪指令上带着 vl、SEW、策略……」

1
2
3
4
5
6
7
bb.0 (%ir-block.0):
liveins: $v8, $v9
%1:vr = COPY $v9
%0:vr = COPY $v8
%2:vr = nofpexcept PseudoVFADD_VV_M1_E32 $noreg, %0, %1, 7 /* frm=dyn */, 4 /* vl */, 5 /* e32 */, 1 /* ta, mu */, implicit $frm
$v8 = COPY %2
PseudoRET implicit $v8

我:「那些斜杠后面的注解是谁写的?」

水科:「MIR 打印器会把操作数名字注解在值后面」

水科:「注解由 RISCVInstrInfo.cpp 的 4012 行」

水科:「createMIROperandComment 按操作数类型生成……」

水科:「7 是浮点舍入模式 frm=dyn,4 是这条指令的 vl」

水科:「frm是浮点舍入模式CSR」frm 是浮点舍入模式 CSR」
我:「嗯、」

水科:「由另一个 pass 插入读写……」

我:「跑完那个 pass 之后呢?」

水科:「vsetivli 出现了」

水科:「向量指令上补了 implicit vl和implicitvl 和 implicit vtype……」

水科:「运行期的长度改由这两个 CSR 提供」

水科:「那个显式的 4 仍然在,它只是编译期留下的 AVL 记录……」

我:「啊、」

水科:「供 pass 反推这条指令的要求……」

1
2
3
4
5
bb.0 (%ir-block.0):
liveins: $v8, $v9
dead $x0 = PseudoVSETIVLI 4, 208 /* e32, m1, ta, ma */, implicit-def $vl, implicit-def $vtype
renamable $v8 = nofpexcept PseudoVFADD_VV_M1_E32 undef renamable $v8, killed renamable $v8, killed renamable $v9, 7 /* frm=dyn */, 4 /* vl */, 5 /* e32 */, 1 /* ta, mu */, implicit $frm, implicit $vl, implicit $vtype
PseudoRET implicit $v8

我:「implicit-def 是什么?」

水科:「隐式定义。这条指令写 vl,但它不作为显式操作数出现」vl,但它不作为显式操作数出现」
我:「dead 呢?」

水科:「这个定义没人读,vsetivli 要的只是写 CSR」

水科:「目标落到恒零的 x0 正好丢弃……」

我:「第一段里那个 nofpexcept 呢?」

水科:「机器层标志,说这条指令不会触发浮点异常」

水科:「来自 IR 上的 nnan、ninf 一类快速数学标志……」

我:「最后汇编出来是什么?」

水科:「向量部分就两行,后面还跟着一条 ret」

1
2
vsetivli	zero, 4, e32, m1, ta, ma
vfadd.vv v8, v8, v9

我:「RVV 还有别的专属 pass 吗?」

水科:「一批……」

水科:「RISCVInsertReadWriteCSR」

水科:「RISCVInsertWriteVXRM、RISCVVMV0Elimination」

水科:「与前面的 RVV 分配器、VSETVLI 插入一样」

水科:「只挂在 legacy 流水线上……」

水科:「NewPM 路径上依次对应 RISCVCodeGenPassBuilder.cpp」

我:「真的?」

水科:「的 132、133、138 三条 TODO」

水科:「最后两个,RISCVVectorPeephole 和 RISCVVLOptimizer」

水科:「两条路都跑……」

水科:「NewPM 把 RISCVVLOptimizer 接在 113 行……」

水科:「RISCVVectorPeephole 接在 116 行」

我:「那个……」

我:「VMV0 那个是做什么的?」

水科:「RVV 规定掩码操作数必须放在 v0……」

水科:「LLVM 在指令选择时把掩码表示成一个单例寄存器类 VMV0 的虚拟寄存器」

水科:「RISCVRegisterInfo.td 的 941 行……」

水科:「方便在 MIR 上优化掩码」

我:「慢一点……」

水科:「分配前再由这个 pass 换成一到多个复制到 v0的序列……」v0 的序列……」
我:「为什么需要换?」

水科:「触发点是寄存器合并,coalescing」

水科:「它会把 copy 折进 vmv0……」

水科:「于是同一条指令里出现两个 vmv0 操作数……」

水科:「文件头举的例子是 PseudoVADD_VV_M1_MASK 带两个 %:vmv0」

水科:「还有 vrnov0,那是不含 v0 的向量寄存器类……」

我:「……」

水科:「分配器的模型是每个虚拟寄存器的活跃区间映到一个物理寄存器……」

水科:「重叠的两个区间不能映到同一个」

水科:「而 VMV0 这个类里只有 v0一个成员……」v0 一个成员……」
水科:「同一条指令上的两个 vmv0 虚拟寄存器」

水科:「活跃区间必然在这条指令处重叠,这对干涉于是无解……」

我:「诶、」

水科:「这个 pass 的办法是提前把它们复制成物理寄存器 v0」v0」
水科:「等式约束就退化成普通的物理寄存器使用……」

水科:「分配器只需给这些复制安排先后……」

我:「另外两个呢?」

水科:「RISCVInsertWriteVXRM」

水科:「向量定点运算的舍入模式由 vxrm CSR 决定……」

水科:「这个 pass 用一前一后两个数据流分析尽量少写 vxrm」

水科:「一个算哪里有值可用,一个算哪里需要值……」

我:「嗯、」

水科:「RISCVInsertReadWriteCSR」

水科:「带静态舍入模式的浮点向量指令会隐式写 frm……」

水科:「这个 pass 在指令前后保存和恢复 frm」

我:「那么向量寄存器组还遵守第六章那套规则吗?」

水科:「仍然遵循寄存器分配那一章的规则……」

水科:「只是一个虚拟寄存器对应的是一组物理寄存器」

水科:「并且这一组的大小由 LMUL 决定……」

三日月已经不在通风口旁边了。

她坐过的那块水泥地上,用粉笔画了一个格子。

只有一个。

格子里没有编号。

第八章 MC 「从指令到字节」

“9 月 14 日”

三点四十分。

水塔的影子刚好盖住半张长椅。

蝉还在叫,只是叫得比七月敷衍。

我:「第 4 类选择是什么?」

水科:「编码,把每条机器指令变成字节……」

水科:「把跨模块的引用变成重定位记号,这一段的公共名字是 MC 层……」

我:「llc 的最后一步做什么?」

水科:「把 MIR 交给 AsmPrinter」

水科:「AsmPrinter 把 MachineInstr 降成 MCInst……」

水科:「一条指令的 opcode 加一串 MCOperand……」

水科:「MCOperand 只有三种」

水科:「立即数、寄存器号、符号表达式 MCExpr」

三日月:「假设还有一个……」

水科:「符号地址恰恰还没解析,留给下面的 fixup……」

水科:「再交给 MCStreamer」

水科:「CodeGenerator.md 的 579 行」

我:「MCStreamer 有几种?」

水科:「它是个抽象接口,实现不止两个……」

水科:「llc 这条路上用到的是两种,对应两种输出」

水科:「输出汇编文本时走 MCAsmStreamer……」

水科:「每条 MCInst 直接打印成一行汇编……」

水科:「RISCVInstPrinter」

水科:「输出目标文件时走 MCELFStreamer……」

我:「为什么?」

水科:「每条 MCInst 交给 RISCVMCCodeEmitter 编码成字节」

水科:「产生的重定位请求交给 RISCVAsmBackend」

水科:「和 RISCVELFObjectWriter……」

水科:「变成 .o 里的重定位项」

水科:「RISCVAsmPrinter::emitInstruction 是这条路的入口」

我:「嗯、」

水科:「387 行……」

水科:「模块开始时它还要输出模块级信息……」

水科:「emitStartOfAsmFile,634 行」

水科:「模块的 target-abi、riscv-isa」

我:「啊、」

水科:「元数据、.attribute arch 等……」

我:「一次 llc -mtriple=riscv64 -O2 出来的 .s 长什么样?」

水科:「大致是这样,对应前面那个 sum,开头还有两行 .attribute」

水科:「一行是 4 后面跟 16」

水科:「一行是 5 后面跟那串 ISA 字符串,rv64i2p1……」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	.text
.globl sum
.p2align 2
.type sum,@function
sum:
.cfi_startproc
mv a2, a0
li a0, 0
seqz a3, a1
add a1, a1, a3
slli a1, a1, 3
add a1, a2, a1
.LBB0_1:
ld a3, 0(a2)
addi a2, a2, 8
add a0, a0, a3
bne a2, a1, .LBB0_1
ret
.Lfunc_end0:
.size sum, .Lfunc_end0-sum
.cfi_endproc

我:「那些点开头的行是机器码吗?」

水科:「不是」

水科:「.text、.globl、.p2align、.type、.size」

水科:「是给汇编器与链接器的指令……」

水科:「.cfi_startproc 和 .cfi_endproc 是调用帧信息的标记,CFI」

水科:「调试器与异常处理用它们展开栈帧……」

我:「mv、li、ret 这些是真的指令吗?」

水科:「有一类只在文本层面成立的助记符……」

水科:「mv、li、ret、nop、call、tail、la……」

水科:「有的是一条真实指令的别名」

水科:「mv rd, rs 就是 addi rd, rs, 0……」

水科:「ret 就是 jalr x0, ra, 0」

水科:「nop 就是 addi x0, x0, 0……」

我:「那个……」

水科:「有的要展开成多条,call、tail、la」

水科:「还有手写汇编里的大立即数 li……」

水科:「编译器自己输出的 li 只可能是 12 位以内那条别名,不需要展开」

我:「展开发生在哪里?」

水科:「取决于输出」

水科:「输出汇编文本时由后续的汇编器展开 call 和 la 这类……」

水科:「GNU as,也就是 GNU 汇编器,或者 llvm-mc」

水科:「输出目标文件时由 MC 层展开……」

水科:「RISCVMCCodeEmitter::expandFunctionCall」

我:「那个……」

水科:「169 行一类函数」

水科:「llvm-mc 走的就是同一套 MC 展开」

水科:「不是第二份实现……」

水科:「两种输出在语义上等价……」

我:「贩卖机又吞钱了……」

我:「汇编器那一侧怎么做的?」

水科:「分两步」

水科:「解析与匹配,RISCVAsmParser……」

水科:「以及匹配之后的指令级处理……」

水科:「processInstruction,4398 行」

水科:「processInstruction 处理的就是伪指令……」

我:「……」

水科:「li 展开成一串整数物化指令,4436 行」

水科:「到 emitLoadImm,3741 行」

水科:「再到 RISCVMatInt::generateMCInstSeq……」

水科:「产出 lui、addi、slli、srli、bseti」

水科:「以及若干扩展专用形式」

水科:「add.uw、slli.uw、pli 那一族、qc.li 的组合……」

我:「再说一遍?」

水科:「汇编器这一侧没有常量池,常量池那条路属于编译器……」

水科:「la 和 lla 展开成地址序列」

水科:「emitLoadAddress,3811 行」

水科:「非 PIC 走 PC 相对的 lla」

我:「嗯、」

水科:「PIC 走 GOT 间接的 lga……」

水科:「call 和 tail 不在 processInstruction 里」

水科:「输出目标文件时由 MC 层的 expandFunctionCall」

水科:「展开成 auipc 加 jalr」

水科:「汇编器同时还要处理 .option arch、.attribute arch 这类指令……」

我:「……」

水科:「让同一份 .s 里不同函数可以用不同的扩展」

水科:「这在 MIR 层是 Subtarget 按函数取不同值……」

水科:「在汇编层则靠 .option push 和 pop 表达……」

我:「fixup 是什么?」

水科:「RISCVFixupKinds.h 的 19 行里列着 RISC-V 的 fixup 类型」

水科:「fixup_riscv_hi20、fixup_riscv_lo12_i」

水科:「fixup_riscv_lo12_s……」

水科:「fixup_riscv_pcrel_hi20、fixup_riscv_pcrel_lo12_i」

水科:「fixup_riscv_pcrel_lo12_s」

水科:「fixup_riscv_jal、fixup_riscv_branch,等等……」

三日月:「呵呵……」

水科:「fixup 是这条指令里有一个位置需要等符号地址确定后再填的记录……」

水科:「汇编器在指令里留下 fixup……」

水科:「本地符号在布局完成后就填上」

水科:「跨模块符号则转成重定位,由链接器在链接期填……」

我:「重定位是谁转的?」

水科:「RISCVELFObjectWriter,132 行」

水科:「fixup_riscv_hi20 变成 R_RISCV_HI20……」

水科:「fixup_riscv_pcrel_hi20 变成 R_RISCV_PCREL_HI20」

水科:「跳转变成 R_RISCV_JAL 和 R_RISCV_BRANCH」

水科:「调用变成 R_RISCV_CALL_PLT……」

我:「嗯、」

水科:「链接器需要知道每条重定位的语义,HI20 填的是符号地址的高 20 位」

水科:「加上符号地址低 12 位带来的进位……」

水科:「LO12 填的是低 12 位……」

我:「啊、」

水科:「PCREL_HI20 是相对当前 PC 的高 20 位……」

水科:「PCREL_LO12 是相对配对的那条 auipc 的低 12 位……」

水科:「后两者必须成对出现」

水科:「这个配对关系就是 %pcrel_lo 后面必须跟一个标签……」

水科:「标签必须贴在配对 auipc 上的原因」

水科:「第四章那个 PIC 例子里 .Lpcrel_hi0 的作用……」

我:「那个……」

我:「链接期松弛是什么?」

水科:「RISC-V 的指令长度不固定,32 位或 16 位」

水科:「链接器可以在链接时把一条 32 位指令换成 16 位的等价形式……」

水科:「或者把 auipc 加 jalr 这对压成一条 jal」

水科:「前提是新的偏移还在范围内……」

水科:「它让编译器不必为了省空间付出代价」

我:「嗯?」

水科:「只要在标记重定位上表个态即可……」

水科:「R_RISCV_RELAX 说,这条指令我允许你改」

水科:「R_RISCV_ALIGN 说,这里要对齐,填充字节数你重算……」

我:「硬币只剩三枚……」

我:「最常见的那一条怎么等价?」

水科:「把调用序列 auipc ra 加 jalr ra 压成一条 jal ra, f」

水科:「原序列里 auipc 把 ra 设成 PC 加上 hi 左移 12 位……」

水科:「jalr 跳到 ra 加 lo,等于 f」

水科:「并把 ra 设成它后面那条指令的地址」

水科:「压成一条 jal 后,调用整体短了 4 字节」

我:「……」

水科:「后面的指令跟着前移……」

水科:「而 jal 把 ra 设成它后面那条指令的地址」

水科:「也就是前移后的同一个返回点……」

水科:「所以返回到哪里仍然指向同一条指令,跳去 f 不变……」

水科:「能压的前提是 f 减 PC 能装进 JAL 的 21 位有符号字段」

水科:「正负 1 MiB」

我:「诶、」

水科:「并且 f 在链接期可定……」

我:「链接器凭什么能改?」

水科:「松弛能进行的前提是链接器被允许改动指令长度」

水科:「汇编器只在可能被改短的指令与对齐处留下标记……」

水科:「链接器只动带标记的地方」

水科:「缩短一条指令会让它后面所有符号的位置前移……」

我:「嗯、」

水科:「所以链接器要反复收缩与重算,直到没有新的指令可以缩」

水科:「对齐填充也不能写成固定字节数,要按新的位置重新对齐……」

我:「那编译器还能把字节距离当常数吗?」

水科:「不能」

水科:「链接后任何一段距离都可能因为别处的松弛而变化……」

水科:「缩短指令让后面前移,重新对齐又可能把填充加回去」

水科:「调试信息、异常处理表与跳转表要用符号差而不是硬编码偏移……」

水科:「R_RISCV_ALIGN 标的就是对齐点」

我:「……」

水科:「编译器只告诉链接器这里要对齐到多少,而不写死填充字节数……」

水科:「链接器收缩之后按新位置重算……」

我:「编译器自己要做哪两件事?」

水科:「函数内的松弛,和汇编期的松弛,目标文件还没交给链接器……」

水科:「函数内部的跳转是否超出范围是编译器自己知道的……」

水科:「由 BranchRelaxation 处理……」

水科:「无条件分支超出正负 1 MiB 时……」

水科:「替换成 PseudoJump」

我:「我记不住这么多……」

水科:「它是 isCodeGenOnly 等于 0 的伪指令……」

水科:「在 -S 输出里打印成 jump……」

水科:「由 MC 层的 -filetype=obj」

水科:「或 llvm-mc 展开成 auipc 加 jalr」

水科:「jump 是 LLVM 专有助记符,GNU as 不认……」

三日月:「呵呵……」

水科:「展开后是 PC 相对,覆盖 32 位范围」

水科:「展开需要一条临时寄存器,由 RegScavenger 提供……」

水科:「找不到就先把值存到栈上预留的位置」

我:「啊、」

水科:「条件分支超出正负 4 KiB 时,把条件取反……」

水科:「让它跳过一条跳向原目标的无条件分支」

我:「为什么这样等价?」

水科:「设原分支是条件 c 成立就跳 T……」

水科:「改写后是 c 不成立就跳过下一句,否则执行下一句的无条件跳转去 T」

水科:「c 成立时走跳转、去 T,c 不成立时跳过跳转、继续原后继块……」

水科:「两条路径与原程序一一对应」

我:「那个……」

水科:「取反后条件分支只需跳过一条指令,距离必然够近……」

水科:「远距离由无条件分支承担」

水科:「无条件分支再超出正负 1 MiB 就按上一条换成 PseudoJump……」

水科:「若条件无法取反,则新建一个紧邻的块放无条件分支」

水科:「条件分支改为跳向这个近块,同样保持两条路径的对应……」

我:「那个……」

水科:「RISC-V 的 B 型范围只有正负 4 KiB」

水科:「所以这一步经常触发」

我:「汇编期的松弛呢?」

水科:「RISCVAsmBackend 在汇编期间就能确定一部分符号……」

水科:「发现短跳转的目标超出它的范围时先把它放大」

水科:「mayNeedRelaxation 在 471 行」

水科:「relaxInstruction 在 248 行……」

我:「水塔的影子又长了……」

水科:「例如把 C.J 放大成 JAL……」

水科:「对齐处也要处理,relaxAlign 在 331 行」

水科:「链接器松弛会让后面的指令位置移动」

水科:「原来用来对齐的固定字节数可能不再合适……」

我:「……」

水科:「所以对齐处必须保留可变长度的方案……」

我:「地址呢,一个全局符号的地址怎么算?」

水科:「RISC-V 没有 x86 那样的单一 PC 相对寻址」

水科:「也没有单条指令的绝对地址加载……」

水科:「所以它变成了一组预先定义好的模式,由代码模型和重定位类型选择……」

水科:「代码模型是对符号地址能落在多远的假设」

水科:「三支分支就写在 lowerGlobalAddress 的那个 switch 里……」

我:「诶、」

水科:「10104 行……」

水科:「Small 在 10107 行,Medium 在 10123 行」

水科:「Large 在 10147 行」

我:「然后?」

水科:「最后这一支本文不展开……」

我:「medlow 是什么?」

水科:「CodeModel::Small」

水科:「假设符号地址能装进 32 位有符号数……」

水科:「低 2 GiB 或高 2 GiB……」

水科:「静态链接时用 lui 加 addi 的绝对序列」

水科:「%hi 和 %lo」

我:「……」

我:「medany 呢?」

水科:「CodeModel::Medium……」

水科:「换成假设符号落在当前 PC 的正负 2 GiB 内」

水科:「10142 行的注释原话是 within any 2GiB range……」

水科:「用 auipc 加 addi 的 PC 相对序列」

水科:「%pcrel_hi 和 %pcrel_lo」

我:「PIC 呢?」

水科:「外部可见符号要经过 GOT……」

水科:「用 %got_pcrel_hi 取 GOT 表项的地址」

水科:「再从 GOT 表项里读符号地址……」

水科:「就是第四章那三条指令……」

我:「TLS 呢?」

水科:「线程局部变量的四种模型」

水科:「local-exec、initial-exec」

水科:「local-dynamic、general-dynamic……」

水科:「各有自己的地址序列」

水科:「用 %tprel_hi、%tprel_lo、%tls_ie_pcrel_hi、%tls_gd_pcrel_hi」

水科:「等说明符表达」

我:「等一下……」

水科:「这些都列在 RISCVMCExpr.cpp 的 23 行那张解析表里……」

水科:「这些模式在指令选择的 lowerGlobalAddress 里选」

水科:「在 MC 层变成重定位,在链接期才真正确定数值……」

水科:「这解释了为什么 RISC-V 后端有那么多与地址有关的 pass」

水科:「地址不是一条指令,而是一段要尽可能短的指令序列……」

我:「嗯、」

水科:「每一段都值得再缩短一次……」

我:「那么第 4 类选择也讲完了?」

水科:「讲完了,程序从 IR 出发」

水科:「变成了可以被汇编器、链接器、加载器处理的目标文件……」

我把座位表摊在长椅上。

汗把纸洇湿了一角,编号化开了。

格子还在。

线还在。

只是每一格里写的字,一个也认不出来了。

第九章 -stop-after 「每一站都可以下车看」

“9 月 14 日”

五点四十分。

水塔的影子刚好盖住整条坡道。

蝉还在叫,只是叫得比七月敷衍。

我:「上面每一步都是可观察的吗?」

水科:「都是」

我:「常用的手段有哪些?」

水科:「先把机器配置固定住……」

水科:「llc -mtriple=riscv64」

水科:「-mattr=+m,+a,+f,+d,+c -mcpu=sifive-u74」

水科:「然后只看某个阶段的状态……」

三日月:「也许……」

水科:「-stop-after=,在该 pass 跑完之后停下……」

水科:「-stop-before=,跑之前停下……」

水科:「-print-before=、-print-after=」

水科:「还有 -print-after-all,把每一站都倒出来……」

水科:「输出是 MIR」

我:「那个……」

我:「只想跑一个 pass 呢?」

水科:「-run-pass=……」

水科:「比如 -run-pass=riscv-insert-vsetvli」

水科:「单独调 VSETVLI 插入……」

水科:「它只吃 .mir 输入」

水科:「先把 -stop-after 的输出存成 .mir,再喂回去……」

我:「那个……」

我:「我试试……」

我把那个 .ll 直接喂给了 -run-pass。

它不认。

我:「诶!」

水科:「说了只吃 .mir」

我:「……啊哈哈,我还以为它能自己认出来……」

水科:「它连 IR 都不想看……」

我:「那 SelectionDAG 的中间形态呢?」

水科:「-debug-only=isel」

水科:「或者 -view-dag-combine1-dags 一类 -view-* 选项……」

水科:「两者都要在开启断言的构建里」

我:「我手边这个就是 release 的」

水科:「那你试试看」

我:「试就试……」

我敲了 -view-dag-combine1-dags。

屏幕上跳出一行字。

Unknown command line argument

我:「……」

水科:「那批 view-*-dags 选项整段写在 #ifndef NDEBUG 里……」

水科:「SelectionDAGISel.cpp 的 147 行」

水科:「release 构建下 llc 直接报 Unknown」

水科:「command line argument……」

我:「哪个?」

我:「所以我这一整个下午都在用一把没有钥匙的锁……」

水科:「锁是好的,你连门都没找到」

我:「……」

水科:「超×1000 蠢……」

我:「这次我不反驳……」

我:「那怎么校验机器指令合不合法?」

水科:「NewPM 路径在流水线末尾默认跑 MachineVerifier」

水科:「CodeGenPassBuilder.cpp 的 274 行……」

水科:「legacy 路径要显式加 -verify-machineinstrs」

水科:「TargetPassConfig.cpp 的 814 行」

水科:「addVerifyPass」

我:「……」

水科:「EXPENSIVE_CHECKS 构建下 legacy 也会默认开……」

我:「它检查语义正确吗?」

水科:「不是。它检查的是形态正确……」

水科:「虚拟寄存器是否都有类……」

水科:「操作数是否满足寄存器类与操作数约束」

水科:「活跃区间与 spill slot 活性是否自洽……」

水科:「φ 节点与 CFG 是否一致」

我:「栈帧编号呢?」

水科:「栈帧编号的消除由 PEI 与 AsmPrinter 保证……」

水科:「PEI 是 Prologue/Epilogue Insertion」

水科:「序言和跋插入 pass……」

水科:「在函数入口出口生成保存恢复代码」

水科:「并把 FrameIndex 落成基址加偏移……」

水科:「操作数与调度模型是否匹配属于另一套」

我:「诶、」

水科:「-verify-misched」

水科:「都不在 MachineVerifier 里……」

我:「想看机器码的调度和吞吐呢?」

水科:「llvm-mca」

水科:「加上 -mtriple=riscv64 和 -mcpu=」

水科:「它用同一套调度模型给出一段汇编的流水线占用」

我:「这些开关跟前面几章怎么对应?」

水科:「-stop-after=finalize-isel 对应指令选择那一章的输出……」

水科:「-stop-after=greedy 对应寄存器分配那一章的输出……」

水科:「-stop-after=riscv-insert-vsetvli 对应 RVV 那一章……」

水科:「-filetype=asm 和 -filetype=obj」

我:「嗯、」

水科:「对应从机器指令到汇编与目标文件那一章」

我:「每一站都能下车看?」

水科:「都能……」

我:「看完还能改吗?」

水科:「不能」

我:「为什么?」

水科:「因为票已经发出去了……」

坡道下面,便利店的灯亮着。

冰柜里还剩三瓶运动饮料,标签朝外,摆得整整齐齐。

我:「水科!」

水科:「什么?」

我:「我今天什么都没定下来?」

水科:「你定了」

我:「定了什么?」

水科:「热的罐装咖啡,两次……」

我:「……那叫没得选!」

水科:「那叫第 1 类!」

终章 Wonderful Everyday 「美好的每一天」

“9 月 14 日”

晚上八点。

水塔的影子刚好盖住半张长椅,另外半张留给夜。

蝉还在叫,只是叫得比七月敷衍。

我:「那么这一篇的结论是什么?」

水科:「后端的全部工作,是补答案」

水科:「给 IR 里目标无关的选择,补上目标相关的答案……」

水科:「答案分四层,从上到下依次是机器配置、指令、寄存器、编码」

我:「四层之间呢?」

水科:「单向依赖……」

水科:「机器配置决定能选哪些指令」

水科:「指令决定能选哪些寄存器类……」

水科:「寄存器分配的结果决定后面还能做哪些指令级优化……」

水科:「每层的输出都是下一层的输入」

我:「……」

我:「定完之后呢?」

水科:「某层定完之后,后面不再重开这层的选择……」

水科:「只允许做尊重已有选择的局部改写」

水科:「压缩指令、访存配对、分支松弛……」

水科:「以及链接器那一侧的收缩」

我:「所以流水线顺序不能随便调?」

水科:「这就是后端流水线顺序不能随意调换的原因……」

水科:「也是伪指令、MIR 这些中间表示存在的意义」

水科:「在信息还不全的时候,先不把选择做完……」

我:「RISC-V 有什么特别的吗?」

水科:「它的个性在于可选做法受编码约束更紧……」

水科:「基础指令固定 32 位」

水科:「立即数与跳转范围小……」

水科:「地址要两三条指令」

水科:「压缩指令另有一套范围……」

水科:「向量长度在编译期未知」

我:「蝉还在叫……」

水科:「链接器还能反过来改指令……」

水科:「这些特点让后端多了一批专属 pass」

水科:「但并没有改变上面那四层结构……」

水科:「所有的专属 pass 都在某层的内部做优化」

水科:「不跨层改变已经定下的东西……」

我:「嗯、」

我:「……」

四层。

一层压着一层,谁也不许回头。

那么一开始就不许选错吗。

或者,选错的那一层,会被后面三层悄悄地补回来吗。

不会。它只会被写在字节里。

我:「水科,如果第一层就选错了呢?」

水科:「那就换一个 -march 重编」

我:「重编?」

水科:「从头……」

我:「……原来是可以重来的」

水科:「人可以。它不行」

我偶尔还会思考这种事情。

比如,一张已经印好的座位表,和一张还没印的座位表,究竟哪一张比较自由。

对我来说,那就只有这种程度。

因为我现在能想到的,只有一件事。

罐装咖啡是热的,而九月还没有凉下来。

我把那张座位表折回四折,压在罐子底下。

跟第一天同一个位置。

格子已经认不出来了,编号化掉了,铅笔圈也被折痕穿过。

纸还是那张纸。

七排十二号还在那里。

只是它现在是谁的,我没有资格知道。

幸福的每一天……

每一天都很幸福……

我便是生活在这样一个世界上……

我偶尔会思考这种事情。

……

Wonderful Everyday