Skip to content

从虚拟名字交付一份可执行代码 ​

回到寄存器分配学习路线。这项练习把“得到颜色”继续推进到目标程序:同一份带分支和调用的IR,分别经过图分配、线性扫描、复制合并、区域分裂与物理溢出重写。每次改变都要交出实际块表,并说明哪个位置装着下一步需要的值。

下载纯Python核验器与完整参考输出。Python 3.10+运行python foundations-register-allocation-checker.py即可得到JSON;所有必要检查是显式检查,python -O应得到相同输出。使用importlib加载文件后,compile_all、split_regions、lower和两个解释器可分别调用。源码中的main_program()给出下列完整输入。

先固定机器和可观察结果 ​

物理寄存器共有四个:R0、R1可分配;S0、S1只作重载暂存器,不能跨原IR指令保存逻辑值。私有栈槽互不别名。二元指令先读取两个操作数,再写结果,采用数学整数。没有堆、I/O、异常、整数溢出或有限内存耗尽。

纯函数double由R0收参并返回两倍值,破坏R0、S0、S1,保留R1和私有槽。运行对象是顶层过程;若编入真实ABI中的被调用函数,还须另履行保存寄存器和栈布局约定。

比较的观察是返回整数和源分支真假序列。目标load、store和跳转是内部步骤,可以比源程序多。循环的每条源指令仍展开为有限条目标指令;解释器的步数上限只表示本次检查未完成,不把超限认作发散证明。

任务一:从活跃值建立约束 ​

text
E:
  a := input(a)
  flag := input(flag)
  t2 := a + 1
  t3 := a + 2
  if flag then T else F
T:
  t4 := t2 + t3
  goto J
F:
  t4 := t2 - t3
  goto J
J:
  v := t4
  t5 := double(v)
  y := a + t5
  return y

先不看候选位置,写出逐指令活跃集合。调用结束后仍需a,因此a必须与固定R0节点干涉。v若调用读完即死,不属于跨调用值;t5是调用新结果,也不能被误加同一条禁止边。

用图着色分配逐步简化。参考候选把a放slot0、flag放slot1,t2放R1,t3/t4/v/t5/y放R0。应核的普通干涉包括a与flag、t2、t3、t4、v、t5;flag与t2、t3;以及t2与t3。move的偏好不是另一条强制相等约束。

提交每次出栈选色及所有原边的最终位置检查。再对两色四环和三角形运行同一算法:四环虽没有初始低度节点,却可在反向选色时不spill;三角形至少要让一个名字离开寄存器。潜在spill不能提前算成一次必然的内存访问。

任务二:交出物理IR,不停在位置字典 ​

上面图候选经物理溢出重写后得到以下代码。逗号分隔操作数;ST的顺序为“槽、源寄存器”,LD的顺序为“目标寄存器、槽”。

text
E:
  IN S0, a
  ST slot0, S0
  IN S0, flag
  ST slot1, S0
  LD S0, slot0
  ADD R1, S0, 1
  LD S0, slot0
  ADD R0, S0, 2
  LD S0, slot1
  BR S0, T, F
T:
  ADD R0, R1, R0
  JMP J
F:
  SUB R0, R1, R0
  JMP J
J:
  CALL2
  LD S0, slot0
  ADD R0, S0, R0
  RET R0

a=3、flag=1时返回21;flag=0时返回1。更一般地,真分支为5a+6,假分支为a−2。两条路径都运行2次ST、4次LD;真路径执行16条目标指令,而源路径为11条。v:=t4的物理自复制已被删掉,CALL2前R0确实装着实参。

故意把a改分到R0并保留其他决定,先让干涉检查拒绝候选。再考察x:=7; dead:=9; return x:若错误地把二者都放R0而仍保留死写,目标会返回9。这个失败和“x与dead是否同时活跃”不同,检查必须考虑实际保留的定义写入。

任务三:让另一种启发式处理同一压力 ​

线性扫描按E、T、F、J布局,使用读点2i、写点2i+1。请核a=[1,23)、flag=[3,9)、t2=[5,15)、t3=[7,15),调用写点为21。

a先获得R1;t2到来时无空闲颜色,a结束更晚,于是a整段改到slot0。t3和当前最晚结束的t2都结束于15,严格更晚条件不成立,所以t3整段放slot1。最终flag在R0,其余短段复用R0;访存条数在本例恰与图候选相同,存取的逻辑名字不同。

把J末尾的y:=a+t5改成y:=t5+1,重新执行活跃分析、区间构造和扫描。a不再因为调用后的读取被保活,不能沿用旧区间后只手工放宽颜色。另把单色区间[0,2)与[2,3)交给扫描器,验证“到期”使用≤而不是<。

交错合并另有自己的输入任务:五节点路径a—e—c—b—d偏好a/c。删d后b从度数2降到1,高度邻居数已经从2降到1,允许安全合并;实现优先继续简化b,然后才实际合并a/c。把偏好换成a/b时,两者隔3条边,强合并会闭成奇环;实际轨迹应先删d,再冻结a/b,最后a与b异色。提交别名表和原边检查,不能只数被删move。

任务四:区域分裂必须交付每条边上的值 ​

调用split_regions得到按块分裂后的完整虚拟IR,再分配和lower。原块内名字改成x@块名,四个边适配块应具有下列同时赋值:

原边 适配块中的pcopy
E→T a@T←a@E,t2@T←t2@E,t3@T←t3@E
E→F a@F←a@E,t2@F←t2@E,t3@F←t3@E
T→J a@J←a@T,t4@J←t4@T
F→J a@J←a@F,t4@J←t4@F

a在T、F都只路过,没有当地使用,却仍要进入对应传值集合。分裂版本图分配把a@J放R1,a@E、a@T、a@F分别放私有槽;a的逻辑身份通过边复制连接。

a=3的真分支仍返回21,但本次分裂候选执行3次ST、5次LD、3次JMP、1次MOV,总计21条目标指令。对照任务二,它变慢了。全块切分让区域可独立选择位置,同时增加传值;这一结果不能隐藏在“分裂通常减少压力”的笼统说法后面。

删掉F→J上的a传递,假分支应暴露缺值;只运行真分支无法验收这个变换。再迁移到循环:初值a=1、b=2,每轮同时交换a/b并把n减1,结束返回a−b。n为偶数时应返回−1,奇数时返回1。先按虚拟同时赋值解释,再把R0/R1交换顺序化;用私有临时槽断环,不能把两条普通MOV顺序执行后宣称交换成功。原SSA消解中的并行复制不变量正是这里的工具。

任务五:用配方替代哪一份内存值 ​

text
c := 7
x := input(x)
u := x + c
y := u + c
return y

固定c在slot0,x/u/y在R0。普通重写包含一次ST、两次LD;允许常量重物化后,在两次使用前各执行IMM S1,7,不再保存或重载c,x=3仍返回17。动态指令从7条变为6条;两种指令的真实机器延迟不在本教学计数里。

将配方换成c:=x+1,随后把x改成9,再使用c。若原x=3,保存值应为4,在使用处按当前x重算会得到10。要扩展常量配方,必须同时保存原操作数身份或证明未改写,并满足纯性和异常条件。另检查被合并别名共享一个spill槽的情形:没有证明整个值族都可重建时,不能只删某个常量名字的定义store。

完成时应有的文件和解释 ​

交付源IR、活跃/干涉或区间事实、位置与别名表、区域改名和四条传值边、可直接解释的目标块表,以及逐输入返回/分支对照。下载JSON同时包含未分裂和已分裂的全部物理代码,可逐条核对,不需要猜测被省略的load/store。

参考主入口检查a从−20到20、flag取0/1,三种分配器在原版与分裂版上共492次源/目标对照;另核四环、三角形、保留死写与常量重物化。有限样例能发现实现错误,不能代替逐指令、逐边、调用边界的保持论证。最后报告实际ST/LD/分支/调用条数,再讨论质量;合法候选与较好候选各有自己的证据。