想象你走进一家图书馆的自习室,桌上只摆得下三本书,而你的研究课题需要同时参考二十本。图书管理员面临一个难题:哪些书值得放在桌面上随时翻阅?哪些书暂时不读、放回书架?更微妙的是——两本需要同时摊开对照的书,绝不能挤在同一张桌面;而一本已经合上、暂时不会再看的大部头,完全可以收进抽屉,把位置让给新书。这就是寄存器分配的本质:物理寄存器就像桌面上的几个固定格子,程序里的变量就像一本本书,编译器这位图书管理员要在有限的空间里做出最优的摆放决策,让程序跑得又快又稳。而它用来做决策的整套方法论,就是本文要讲的“活性分析、干涉图、图着色、溢出处理、线性扫描”等一系列算法。这些听起来高深的名字,其实都在回答同一个朴素问题:怎么把最需要的书放在最顺手的地方。
寄存器分配是编译器后端最核心的性能关卡之一,掌握它能让你真正理解程序从高级语言到机器码的转化魔法,也是从事编译器、虚拟机、AI 芯片工具链开发的必备功底。
可视化演示
寄存器分配将虚拟寄存器映射到有限物理寄存器,图着色是经典算法。本文详解活性分析与活跃区间计算、干涉图构建与简化... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
中间表示IR:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
分配问题
图书管理员的第一步,是先认清自己的处境:桌面只有三个格子,而书架上的书可能成百上千。在编译器眼里,程序早期阶段使用的“虚拟寄存器”就像书架上的书——编译器可以随心所欲地创建成千上万个,它们只是抽象概念,不占真实资源。而 CPU 提供的“物理寄存器”是实打实的电子电路,少则十几个,多则几十个。把虚拟寄存器映射到物理寄存器的过程,就是寄存器分配。
这个映射不是简单的一对一。物理寄存器数量 K 通常远小于虚拟寄存器数量 N,就像三格桌面永远放不下二十本同时要用的书。因此编译器必须在每个程序点做出判断:此刻哪些变量最值得“上桌”?如果两个变量的“生命期”(活跃区间)不重叠,它们就可以先后使用同一个物理寄存器——相当于上午占桌面的书,下午换成了另一本。这构成了寄存器分配的基本思路。
理解了这层关系,就像管理员明白了“桌面永远不够用”的事实。接下来的所有算法——活性分析、干涉图、着色、溢出——都是在回答“如何在稀缺资源下作出最优取舍”。这种“资源有限而需求无限”的困局,也是许多系统设计问题的共同底色。
- 符号 V虚拟寄存器集合,是程序抽象阶段产生的全部变量名,数量 N 可以很大。
- 符号 R物理寄存器集合,是 CPU 真实提供的硬件槽位,数量 K 固定且有限。
- 映射 M分配动作本身,为每个虚拟寄存器指派一个物理寄存器。
- 核心约束K 远小于 N,资源天然稀缺,映射必有竞争,这正是所有算法为之奋斗的原因。
活性分析
管理员如何知道哪本书正在被阅读?他观察读者的动作:如果一本书在某个时刻被翻开、之后还会被引用,它就是“活跃”的;如果一本书已经被合上、再也不会被打开,它就不占桌面。在编译器中,这个观察过程叫活性分析(Liveness Analysis)。一个变量在某个程序点是“活跃”的,意味着它的当前值在该点之后还会被读取。
为了高效计算活性,编译器把程序划分成“基本块”(顺序执行、中间无跳转的代码段),并在每个基本块的入口和出口各维护两个集合:use(块内先被读取后写入的变量)和 def(块内被写入的变量)。计算从出口向入口反向推进:一个块的出口活跃集合,等于它所有后继块入口活跃集合的并集;而入口活跃集合,等于本块 use 集合加上“出口活跃但未被本块重新定义”的部分。这个反向传播的过程,像管理员从闭馆时刻倒推整天的阅读轨迹。
活性分析的结果浓缩成一个关键产物——活跃区间(Live Interval)。一个变量从“被定义”到“最后一次被使用”的连续范围,就是它的活跃区间,形如 [定义点,最后使用点]。区间越长,说明变量“占桌”时间越久,对寄存器资源的消耗越大。管理员此时已经掌握了每本书的“在桌时段”,下一步就该规划哪些书不能同时占桌了。
- 符号 succ(B)基本块 B 的所有后继基本块,即程序执行完 B 后可能流向的块。
- 反向传播 out→in出口活跃集合从后继块的入口集合汇总而来。
- use[B]在本块内先被读取、后可能被改写的变量,它们一进块就活跃。
- 减去 def[B]本块内定义过的变量,其旧值在块入口不再需要,故从出口集合中剔除。
干涉图构建
管理员现在面临一个微妙问题:有两本书,读者需要同时摊开对照,它们绝不能挤在同一格桌面;而另两本书分属不同时段阅读,完全可以轮流使用同一格。在寄存器分配中,这种“不能共享同一物理寄存器”的关系叫干涉(Interference)。判断标准很简单:两个变量的活跃区间存在重叠,它们就互相干涉。
把所有变量看作图的节点,把干涉关系看作图的边,就得到一张干涉图(Interference Graph)。图的每个节点代表一个虚拟寄存器,每条边连接两个互相干涉的变量。干涉图是寄存器分配的“冲突地图”——地图上相连的城市不能共用同一座仓库,分配方案必须保证相邻节点各占不同的物理寄存器。
干涉图是一个无向图,它的结构直接决定分配难度。如果图很稀疏(大部分节点互不相连),那么物理寄存器够用;如果图很稠密,甚至出现完全图(任意两点都有边),那么需要的物理寄存器数至少是节点数。这个图的性质引出了经典解法——把分配问题转化为图着色问题,这正是 Chaitin 的开创性贡献。
- 节点集合 V图中每个节点代表一个虚拟寄存器(变量)。
- 边集合 E两个变量活跃区间重叠即生成一条无向边。
- 区间交集判断u 的活跃区间与 v 的活跃区间存在时间点重合。
- 构建方式逐个变量对检查活跃区间,重叠则加边,最终得到完整干涉图。
图着色算法
有了干涉图这张“冲突地图”,分配任务变得清晰:给每个节点涂一种颜色,相邻节点颜色必须不同,而颜色总数正好等于物理寄存器数量 K。这就是图着色问题。K 种颜色对应 K 个物理寄存器,颜色相同的节点(变量)可以共享同一个寄存器——因为它们互不干涉,就像不同时段使用同一格桌面的书。
图着色有一个漂亮的简化规则:若某个节点的度数(相连的边数)小于 K,即使它的所有邻居都用了不同颜色,也最多占用 K−1 种颜色,必然还剩至少一种颜色可以给它。因此这类节点可以“安全地”压入栈中先不处理,专注于处理更复杂的子图。这个过程叫简化(Simplify),它递归进行:不断把度数小于 K 的节点移出图、压栈,直到图为空或无法继续简化。
图着色是解决问题的漂亮透镜,但它有个坏消息:求最小颜色数是 NP 完全问题——就像管理员面对一张极其复杂的冲突地图时,找不到一次性算出最优方案的捷径。好在实际程序的干涉图通常不极端,简化—弹栈—染色的启发式流程在绝大多数情况下都能在 K 种颜色内完成分配。若实在无法完成,就轮到“溢出处理”出场了。
- 度数定义节点的度是它相连的边数,代表与它冲突的变量个数。
- 临界条件当节点的度数小于 K,即使邻居颜色全不同也填不满调色盘。
- 压栈保留把可简化节点暂时移出图,待子图全部着色后再弹出并分配颜色。
- 回溯染色按弹栈顺序为节点选色,每步只需避开已着色邻居的颜色。
溢出处理
假如简化到一半,剩余图中所有节点的度数都不小于 K——每种颜色都被邻居们占满了,桌面确实塞不下了怎么办?管理员只能做出无奈的抉择:把某本书从桌面撤走,放回书架(内存),等需要用的时候再取出来。在寄存器分配中,这个动作叫溢出(Spill)。溢出变量不再占用物理寄存器,它的读写被替换为内存访问指令(load/store)。
关键问题是:溢哪本书最划算?显然应该挑“最不常用”的那本。编译器用溢出代价(Spill Cost)来衡量:变量被引用的次数越多、所在循环嵌套越深,代价就越高。一条引用在深度为 d 的循环里,执行次数大约是外层的 10^d 倍,因此要给深度以指数级加权。选出代价最低的变量标记为溢出后,代码被重写:该变量的定义处插入 store 指令写回内存,使用处插入 load 指令读入临时寄存器,然后重新进行活性分析和着色,如此迭代直到着色成功。
溢出不是失败,而是一种务实退让。管理员把最冷门的书送回书架,腾出桌面给热门的书,整体效率反而更高。现代编译器将“少溢出、优溢出”作为核心目标——宁可让少数冷门变量慢一点,也要保证热点循环中的变量全部留在寄存器里。迭代的溢出-重写过程确保了每次重算都基于更真实的信息,最终让分配方案稳定收敛。
- 引用点集合 ref(v)变量 v 在代码中所有被读取和写入的位置。
- 深度加权因子循环嵌套越深,该行代码执行次数指数级增长。
- 频率系数 freq(p)引用点在静态执行路径上的相对频率,可由分支概率估算。
- 求和汇总把所有引用点的加权代价累加,值越小越适合作为溢出候选。
线性扫描
图着色算法虽然优雅,但构建干涉图往往需要 O(N²) 的时间,对 JIT 编译器(运行时即时编译)来说太奢侈了。想象管理员在高峰期临时接手图书馆:读者源源不断进来,每本书带着自己预期的“阅读时段”,他需要尽快安排桌面,来不及绘制完整的冲突地图。线性扫描算法就是这种“快而糙”的策略。
线性扫描的关键洞察是:既然着色本质上处理的是时间重叠问题,不妨把所有活跃区间按起点排序,然后沿时间轴从左到右扫描。扫描到某个区间的起点时,把它对应的变量放入一个“活动集合”(active set),并为它分配一个空闲寄存器;当某个活跃区间的终点已经过去,就把它的寄存器释放回池中,留给后续变量复用。这样一遍线性扫描就能完成大部分分配,复杂度接近 O(N)。
线性扫描的代价是可能错过最优解——因为它在每一个扫描点只看局部信息。但它的速度优势使其在 JIT 编译器、动态语言运行时中大放异彩。现代编译器常用策略是“双轨并行”:静态编译用精细的图着色追求极致性能,JIT 用线性扫描保证响应速度。管理员两手准备:时间充裕时精细规划,时间紧迫时快速决策。
- 区间起终点每个变量记作 [s_v, e_v],s 是定义点,e 是最后使用点。
- 扫描时刻 i沿时间轴按区间起点递增的方向移动游标。
- 活动集合判定起点已过且终点未到的变量在 active 中。
- 过期释放一旦 i 越过 e_v,立即归还其物理寄存器。
Chaitin实现
Chaitin 算法是图着色寄存器分配的开山之作,也是现代寄存器分配器的“标准配方”。它的核心思想是把分配过程组织成一个四步循环:简化(Simplify)、合并(Coalesce)、冻结(Freeze)、溢出(Spill)。简化和溢出我们已熟悉,合并与冻结是其点睛之笔。
合并针对“复制指令”优化:代码中常有 a = b 这样的移动指令,如果 a 和 b 不干涉,它们完全可以共用同一个物理寄存器,那条复制指令就能被删除。合并操作就是把这两个节点合成一个新节点。但盲目合并可能让节点度数飙升,导致原本可着色的图变得不可着色,因此 Chaitin 提出保守合并规则:只有当合并后节点邻居总数小于 K 时,才允许合并——这确保合并不会破坏可着色性。冻结则是在合并受阻时,暂时取消某些节点的合并资格,让简化继续推进。
完整的 Chaitin 流程是一个优雅的循环:先简化可安全压栈的节点,再尝试保守合并复制相关的节点,无法合并的就冻结一部分并继续简化,最后剩下的高难度节点只能标记溢出。每次溢出后代码重写、重新分析,循环往复直到成功。这套“先易后难、能合则合、不得已才溢”的策略,像极了管理员的决策智慧——先解决能确定的,再优化关联的,最后向现实妥协。
- 邻居集合 N(a)节点 a 的所有干涉邻居(与 a 有边相连的节点)。
- 合并后邻居a 与 b 合并成新节点后,邻居是两者邻居的并集。
- 安全性条件并集规模小于 K,则新节点一定还能找到空颜色。
- 冻结策略不满足条件时暂不合并,先把节点“冻结”起来绕过死局。
LLVM落地
理论最终要落地到真实世界的编译器。LLVM 作为现代编译基础设施的代表,其寄存器分配器是理解理论如何工程化的最好窗口。在 LLVM 中,代码经过优化后所有变量都被表示为“虚拟寄存器”,它们是无限编号的抽象资源;而目标架构(如 x86、ARM)提供有限的物理寄存器,并规定了各自的用途(参数寄存器、返回值寄存器、调用者保存/被调用者保存等)。分配器的任务就是在这两类寄存器之间架起桥梁。
LLVM 的默认分配器名为 Greedy(贪心),思路是“区间优先、涂色保底”:它先把所有活跃区间按长度排序,优先为长区间、高优先级的变量分配寄存器;新变量到来时,若空闲寄存器不够,则尝试从现有变量的区间里“挤”出空间,实在不行再做溢出决策。这种方式融合了线性扫描的速度和图着色的全局视角,是两者的工程化结晶。
从 LLVM 的实践中可以看到,寄存器分配早已不是单一的“图着色 vs 线性扫描”之争,而是多种策略的组合艺术。无论是静态编译器追求极致的运行性能,还是 JIT 希望在毫秒内完成编译,分配器都在“最优”与“最快”之间寻找平衡点。理解这些算法,你就掌握了编译器后端最核心的性能命脉——正如那位图书管理员,把每本书放在最合适的位置,整个程序的运行效率便在举手投足间大幅提升。
- 映射函数 M从一个虚拟寄存器到物理寄存器的指派。
- 合法性约束干涉图上的边强制两端变量颜色不同。
- 物理寄存器类不同架构的寄存器有专门用途,映射必须遵守类别规则。
- 分配目标在满足约束的前提下,使尽量多的变量留在寄存器中、尽量少的变量溢出。
AI 应用场景
深度学习编译器中的算子内核优化
阶段:AI 模型编译优化阶段
解决的问题:AI 框架(如 PyTorch、TensorFlow)训练出的模型在 GPU/TPU 上执行时,每个算子(如矩阵乘、卷积)都会被编译成内核代码。传统手动优化内核耗时巨大且需要硬件专家;AI 编译器利用寄存器分配算法自动化管理数百个虚拟寄存器的映射,让生成的内核充分利用硬件寄存器和缓存,大幅提升算子执行速度。
具体案例:TVM 或 MLIR 在将深度神经网络的卷积分块代码编译到 GPU 时,对每个线程块的寄存器用量进行分配。若寄存器溢出到本地内存,张量核心的吞吐可能下降 30% 以上。编译器通过活性分析和线性扫描快速分配线程私有寄存器,并优先保证内层循环的变量零溢出。
JIT 编译器的快速响应分配
阶段:模型推理的即时编译阶段
解决的问题:像 PyTorch 的 TorchScript 或 LLM 推理引擎(如 vLLM)在做运行时即时编译时,编译速度快慢直接决定首次推理延迟。传统图着色构建干涉图耗时长,不适合毫秒级编译窗口;线性扫描算法按区间排序一次扫描完成分配,兼顾编译速度与生成代码质量。
具体案例:某 AI 推理引擎在启动时对融合后的 Transformer 算子(Attention、FFN)即时编译,将 300 多个虚拟寄存器映射到 32 个物理寄存器,使用线性扫描在 2 毫秒内完成分配,同时利用活跃区间信息让关键张量留驻寄存器,首 token 延迟降低 40%。
AI 芯片工具链的指令集定制
阶段:AI 芯片的编译工具链开发阶段
解决的问题:为 AI 加速芯片设计编译器时,硬件寄存器堆数量、读写端口等约束与传统 CPU 不同。通用开源编译器无法直接适配,需要深度定制寄存器分配策略,让生成的指令序列适配硬件特有的寄存器结构与并行执行模型。
具体案例:某 AI 芯片公司基于 LLVM 定制后端,芯片提供 128 个向量寄存器但只有 16 个标量寄存器。工程师利用 LLVM 的 Greedy 分配器框架,为标量变量定义精细的活跃区间和溢出代价模型,配合 Chaitin 风格的保守合并,将标量溢出的访问冲突降低 50%,矩阵乘内核性能提升 1.8 倍。
Python 演示
class Interval: def __init__(self, name, start, end): self.name = name self.start = start self.end = end def interfere(a, b): return a.start < b.end and b.start < a.end def build_graph(intervals): graph = {v.name: set() for v in intervals} for i, a in enumerate(intervals): for b in intervals[i + 1:]: if interfere(a, b): graph[a.name].add(b.name) graph[b.name].add(a.name) return graph def greedy_color(graph, k=3): color_map = {} for node in graph: used = {color_map[nb] for nb in graph[node] if nb in color_map} color = next((c for c in range(k) if c not in used), None) if color is None: return None color_map[node] = color return color_map def main(): intervals = [ Interval('v1', 0, 5), Interval('v2', 2, 8), Interval('v3', 6, 10), Interval('v4', 1, 4), Interval('v5', 3, 7), ] graph = build_graph(intervals) result = greedy_color(graph, k=3) if result: for node, color in result.items(): print('{} -> R{}'.format(node, color)) else: print('need spill') if __name__ == '__main__': main()
class Interval定义虚拟寄存器的活跃区间,记录名字、定义点 start 和最后使用点 end。def interfere(a, b)判断两个区间是否有重叠,重叠即代表两变量互相干涉。graph = {v.name: set() for v in intervals}以变量名为键,初始化每个节点的邻居集合。for b in intervals[i + 1:]只检查未配对过的变量对,避免重复计算干涉边。if interfere(a, b):区间重叠则互加邻居,完成干涉图加边。used = {color_map[nb] for nb in graph[node] if nb in color_map}收集当前节点已着色邻居的颜色,作为禁忌色集合。next((c for c in range(k) if c not in used), None)从 K 种颜色里挑一个未被邻居用过的;挑不到就返回 None。if result:若所有节点都成功染色,打印每个变量映射到哪个物理寄存器;否则提示需要溢出。
常见误区
误区总看到程序抱怨寄存器不够,以为像内存条一样插上就能扩容,编译器应该能同时处理海量变量。
正解物理寄存器是 CPU 硬件的固定电路资源,数量由芯片架构决定,软件无法增删。编译器能做的不是增加寄存器,而是在有限数量内做最优映射。这就像图书馆自习室的桌面格子数是建筑结构定死的,管理员能做的只是决定哪本书上桌,而不是拆墙扩建。寄存器不够时,唯一的出路是“溢出”——把冷门变量放回内存。
为何常见:日常开发中内存看起来可以无限申请,容易把“内存”的弹性误套到“寄存器”头上,忽略了寄存器是 CPU 内部最稀缺的高速存储。
误区以为编译器像调试器一样,等程序跑起来后录制每个变量的使用情况,再根据运行日志决定寄存器分配。
正解活性分析是编译期的静态分析,通过数据流方程在代码上反向迭代计算,完全不需要运行程序。它基于“可能”而非“实际”:只要某个路径上变量后续可能被使用,就判定为活跃。这好比管理员根据读者预约单和借阅记录提前规划桌面,而不是等读者现场坐下才临时找书。
为何常见:“活跃”这个词带有动态感,容易让人误以为是运行时概念;实际上它是“数据流分析”这一静态家族的一员,从代码结构推导变量生命期。
误区既然把分配问题转成了图着色,就以为算法能算出“最少需要几个寄存器”的最优答案。
正解求一个图的最少颜色数是一个 NP 完全问题,任何已知多项式时间算法都不能保证最优。Chaitin 等算法采用的是启发式策略——简化、贪心染色,速度快但结果可能不是最优。这就像管理员凭经验排桌面,结果合理,却无法证明桌面上恰好是“最少需要的格子数”。实际中启发式已足够接近最优,且速度可接受。
为何常见:教材常把算法讲得很漂亮,容易让人误以为它万能;忽略了 NP 完全性意味着在规模大时“精确最优”是不现实的奢望。
误区看到“溢出”两个字就紧张,认为变量一旦被放到内存,整个程序的性能就完蛋了。
正解溢出的代价取决于被溢出变量的访问频率和所在循环深度。一个只在初始化阶段访问一次的变量溢出到内存,开销微乎其微;而把内层热循环的变量溢出,才会带来数量级的性能损失。优秀的分配器以溢出代价模型为依据,总把最冷门的变量驱逐出寄存器,因此合理的溢出对总体性能影响很小。这好比管理员把三年没人翻的旧书送回仓库,热门书架依旧繁忙,读者体验几乎无损。
为何常见:人们凭直觉认为“寄存器 > 内存”就是天壤之别,忽略了访问频率这一关键因素——冷门变量百年一遇的慢,根本拖不垮整体快节奏。
面试题
Q1. 寄存器分配要解决的核心问题是什么?
- A.减少程序中的循环数量
- B.将无限数量的虚拟寄存器映射到有限的物理寄存器← 正确答案
- C.增加 CPU 的寄存器个数
- D.把内存中的数据搬到硬盘
答案:B。寄存器分配的本质是资源映射:虚拟寄存器是编译器的抽象概念,数量无限;物理寄存器是 CPU 硬件资源,数量有限。它像图书管理员分配有限的桌面,而不是改变图书馆面积(增加寄存器)或搬运仓库(内存硬盘)。
Q2. 变量在某个程序点“活跃”意味着什么?
- A.变量在该点刚被赋值
- B.变量在该点的值之后还会被使用← 正确答案
- C.变量在该点被声明
- D.变量在该点被输出到终端
答案:B。活跃变量的定义是“当前值在未来还会被读取”。刚赋值但如果之后没人用,也不算活跃——就像一本书虽然摊开但再也不会被翻,完全可以收走。活性分析的基础是向后看:从未来找使用依据。
Q3. 以下关于干涉图的描述,正确的有哪些?
- A.节点代表虚拟寄存器(变量)← 正确答案
- B.边连接活跃区间有重叠的两个变量← 正确答案
- C.边的两端必须分配不同的物理寄存器← 正确答案
- D.干涉图一定是一棵树
答案:ABC。干涉图是无向图,节点是变量、边表示活跃区间重叠,相邻节点不能共用寄存器(颜色必须不同)。干涉图可能是任意结构,完全图、稠密图都很常见,不是树。它像冲突地图:连线的城市不能共用同一仓库。
Q4. 图着色中,K 代表什么?
- A.干涉图中节点的个数
- B.可用的物理寄存器数量← 正确答案
- C.变量的平均活跃区间长度
- D.基本块的数量
答案:B。图着色的目标是“用不超过 K 种颜色给所有节点染色”,K 正好对应用户可用的物理寄存器数。颜色就是寄存器,K 种颜色表示只有 K 个物理槽位,故 K 与节点数无关。
Q5. 为什么度数小于 K 的节点一定可以被着色?
- A.因为它没有邻居
- B.它的邻居即使颜色全不同也最多用掉 K−1 种颜色← 正确答案
- C.因为它一定在图的边缘
- D.因为它的活跃区间最短
答案:B。deg(v) < K 意味着 v 的邻居总数不超过 K−1。即使每个邻居都用不同颜色,也最多占用 K−1 种颜色,全套调色盘有 K 种,至少还剩一种可选。所以这类节点可以放心压栈、最后再染色。
Q6. 线性扫描算法中,用来维护当前活跃变量的核心数据结构是什么?
- A.active(活动集合)← 正确答案
- B.符号表
- C.调用栈
- D.哈希索引
答案:A。线性扫描沿时间轴从左到右推进,所有区间起点已过、终点未到的变量集中在 active 集合中,扫描到某个区间终点时把变量移出 active 并释放其寄存器。active 就是“当前正占着桌面的书”的名单。
Q7. Chaitin 算法中“保守合并”的判定条件 |N(a) ∪ N(b)| < K 主要目的是什么?
- A.减少图的边数
- B.保证合并后新节点仍然一定可着色← 正确答案
- C.加快活性分析的收敛速度
- D.优先合并活跃区间短的变量
答案:B。合并 a 和 b 后新节点的邻居数是二者邻居的并集大小。若并集小于 K,那么即使全部邻居颜色不同,新节点仍能找到空闲颜色,合并就不会破坏图的可着色性。这一条件宁可错过一些合并机会,也要守住“优化不改坏结果”的底线。
Q8. 变量发生溢出后,编译器在其读取和存储位置插入什么指令?
- A.load 和 store← 正确答案
- B.add 和 sub
- C.jump 和 call
- D.nop 和 halt
答案:A。溢出变量的值保存在内存中。读取时需要 load 指令把值从内存取回寄存器,写入时需要 store 指令把新值存回内存。就像从书架取书(load)和把书放回书架(store),都是围绕内存的搬运动作。
Q9. 选择溢出候选变量时,通常考虑哪些因素?
- A.变量的引用次数← 正确答案
- B.引用点所在的循环嵌套深度← 正确答案
- C.引用路径的执行频率← 正确答案
- D.变量的字母长度
答案:ABC。溢出代价计算的核心是“访问越频繁、循环越深、执行频率越高,溢出代价越大”。编译器优先溢出冷门、浅层循环、低频路径上的变量。变量名字长度与运行效率毫无关系,管理员不会因为书名字长就不让它上桌。
Q10. 以下哪些情况说明当前干涉图在 K 种颜色下可能需要溢出?
- A.简化过程中出现所有节点度数 ≥ K← 正确答案
- B.图中存在大小超过 K 的完全子图(团)← 正确答案
- C.图中恰好所有节点度数都等于 K−1
- D.图中没有任何边
答案:AB。所有节点度 ≥ K 意味着简化无法继续,必须溢出某些节点。大小 > K 的团(任意两点相连)至少需要团大小种颜色,超过 K 必然不可着色,也需溢出。而所有节点度 = K−1 的情形可能仍有解,无边的图最乐观,用 1 种颜色即可。管理员遇到“所有人都抢同一时段”才需要妥协,没人抢桌面的情况无需任何让步。