“共同终点任务继续将两个版本分别做计数循环展开。这两个变换可以组合,因为外提后的每一支仍是同一有限计数模型;组合不会许可跨轮重排事件,也不能免掉展开后的剩余轮。”
形式陈述
一次循环执行几份完整体
循环展开把若干连续迭代放进一个较大的体,减少回到循环控制处的次数。复制体只是第一步;轮数不是展开因子的整数倍时,必须保留余下的迭代。[1] 本页给出固定正整数因子 k 的商—余数模板,而不把一般 while 的次数猜成一个已知整数。
输入是有限的顺序整数程序。表达式为常量、变量、加减乘、比较,以及正整数字面量作除数的向下取整商和余数;所有表达式纯、确定、总定义。语句含赋值、if、emit(tag,e) 和 repeat。每个 repeat 的体中允许嵌套 if,但不允许另一个 repeat、提前退出、调用或异常。程序的顶层可有多个按序执行的 repeat,也可用 if 选择某个 repeat;所有读取通过确定赋值检查。
repeat E { B } 在进入时求值 E 一次,要求结果 N≥0,并完整顺序执行 B 恰 N 次。N 是这次进入的捕获值,体内修改 E 读取的变量不影响它。repeat 后不能假定体中新定义的变量有值,因为 N 可以为0。所有合法执行有限结束。
沿用编译阶段语义保持的观察约定,本页观察是全部 emit 的有序列表及最终返回元组;内部控制步和新鲜变量不在观察中。这里的语言是确定且总终止的片段,所以目标须给出同一观察,不能只保留“如果碰巧都返回,则数值相同”。
商、余数和实际目标
对编译期给定的整数 k≥1,唯一写成
为当前 repeat 建立新鲜名字 n₀,在原来进入它的位置生成
n₀ := E
repeat (n₀ div k):
B 的第0份完整副本
B 的第1份完整副本
...
B 的第k−1份完整副本
repeat (n₀ mod k):
B
这里不是一个在运行时再解释“重复 k 次 B”的内层循环。生成器真的写出 k 份静态语法,再另留一份尾段体。所有副本保持原赋值目标、原表达式求值次序和原 if 结构;没有把各副本的同名赋值合并,也没有把它们并行运行。
源体如果包含 i:=i+1,目标在每一份副本中保留这条更新。外层整组循环具有自己的内部计数,不拿源 i 充当组号。因此即使 i 还被返回、被写入事件或以非仿射方式更新,变换也不用猜测它的退出值。只有捕获次数的 n₀ 是新增名字,且不得与任何源变量冲突。
参考器遍历实际 code,把每个原 repeat 都替换为这个模板;位于 if 中的循环仍留在那一支,不提前计算未选中分支的次数。生成的整组与尾段循环不再次展开。语法检查拒绝嵌套循环、非正因子或未定义读取;输出节点预算不足则明确报告 size_limit,并不给出半成品目标。
直觉
把十张按顺序处理的工单装成每包四张,可以得到两整包和两张尾单。包内仍依次处理第0、1、2、3张,第二包接着处理第4到第7张,最后处理第8、9张。换包装不能把尾单丢掉,也不能把每包中的“读旧值”和“写新值”按操作种类重新排列。
这解释了为什么循环携带依赖不妨碍本模板。后一轮本来就应读前一轮写出的累积值;完整副本顺序执行,恰好保留这条联系。需要额外依赖证明的是再做调度、向量化或并行化,不是单纯把连续轮写进同一个体。
例子与边界
十轮按四份展开
使用循环不变分支外提的共同例:s=1、i=2,mode>0 时每轮先 emit(before,i),再令 s:=2s+i,emit(after,s),最后 i:=i+1。源 repeat 捕获 N=10。取 k=4,则 q=2、r=2。
第一组包含原第0到3轮,结束 s=57、i=6;第二组包含原第4到7轮,结束 s=1013、i=10;尾段再执行第8、9轮,依次得到 s=2036、4083,最后 i=12。全部20项事件与源次序一致,尤其第二组的最后一项 (after,1013) 后面仍是 (before,10),而不是直接返回。
源循环控制共测试 N+1=11次。目标整组循环测试 q+1=3次,尾段测试 r+1=3次,共6次。保留的循环体条件 mode>0 仍执行10次:展开本身没有外提它。先外提再展开,才得到分支测试1次、循环测试6次的组合版本。
删除尾段的错误目标只执行8轮,返回 (1013,10),少了最后4项事件。若将每份副本的 i 更新提前到本份开头,第一条 before 就由2变成3,首轮 s 由4变成5。即使某个输入最终和偶然相等,事件迹已经给出直接反例。
次数必须在进入时捕获
在原体最后加 n:=0,仍从 n=10 进入。源次数已捕获,所以完整执行十轮,结束 n=0。正确展开先保存 n₀=10;整组后的余数依旧是 n₀ mod4=2。
错误目标若先按10计算两组,却在尾段用当前 n mod4,此时读到0,便丢掉最后两轮。无论 E 是否看起来很便宜,都不能用重复求值 E 代替保存它的入口值。相同道理适用于原次数表达式含多个会被体内赋值的变量。
短循环、零轮和因子一
N<k 时 q=0,整组体不执行,尾段执行全部 N 轮。N=0 时两个目标循环各做一次失败测试,源只有一次;事件都为空,返回原初始化结果。本例返回 (1,2),但循环测试1对2,不能声称展开对所有输入都减少控制成本。
k=1 时 q=N、r=0。本模板仍保留空执行的尾段循环,所以控制测试为 N+2,比源多1。它是语义正确但通常没有收益的边界输入;参考器不偷偷删除这次测试再把实测数归给原模板。k=0、负数及布尔值不是合法展开因子。
N=8、k=4 时 r=0,整组两轮后仍检查一次空尾段,总测试3+1=4。N=3、k=8 时整组一次失败、尾段四次测试,总计5,对源4。不存在一个忽略 q、r 和额外入口的“总是减少 k 倍”精确公式。
哪些语言行为没有被覆盖
带 break 的体不能直接套用无条件连续副本。例如源第一轮遇到 break 就应退出;若目标继续执行同组剩余副本,会增加源没有的事件。continue、抛异常和提前返回也需要明确的跳转修复。本参考器将它们作为不支持的语法拒绝,不把它们当普通空操作。
数学整数保证捕获、商余数和循环控制没有溢出。若改用固定宽度计数器,必须重新证明 N、组数、计数更新和边界算术可表示;不能先溢出再靠尾部取模来补救。对体内操作而言,完整保序复制没有做算术重结合,但这并不使未定义行为、设备读取或线程交互自动落入本文语言。
推论与应用
实例双射给出精确覆盖
给源轮编号 t=0,…,N−1。目标第 g 个整组中的第 j 份副本对应
这些编号恰覆盖0,…,qk−1,每个一次:同一整数除以 k 的商和余数唯一,所以两个不同 (g,j) 不会映射到同一个 t。尾段第 j 轮对应 t=qk+j,0≤j<r,恰覆盖余下 qk,…,N−1。两段不相交且并集为全部源轮。
执行次序也保持。整组按 g 增长,每组内副本按 j 增长,因此 t 连续递增;整组最后一轮若存在是 qk−1,尾段从 qk 开始。q=0 或 r=0 时相应区间为空,同一论证仍成立。
完整体的顺序不能被丢掉
记完整体 B 的语义为 F,它把当前源变量环境和已有事件列表映射为执行一轮后的环境和追加后的列表。F 可以包含条件,也可以使本轮结果依赖前轮值;不要求不同轮互相独立。源语义是 F 连续复合 N 次。
目标整组连续应用 F 共 qk 次,尾段再应用 r 次,合起来恰是同样顺序的 N 次。按前述实例编号归纳,每完成一个原体,双方源变量和事件前缀相同。新鲜次数变量没有被 B 写入,不会影响源表达式;初始化与返回又相同,所以最终观察相同。
这个证明不使用加法交换律,也没有许可把 F 拆成几类语句后分别重复。若把 B 拆为 A;C,通常 (A;C);(A;C) 不等于 A;A;C;C。前者是本页保序展开,后者属于需要额外依赖条件的重排。循环分块改变块与坐标的排序时也有自己的依赖义务,不能由这里的覆盖双射替代。
精确控制成本与静态代码增长
一次进入的循环测试数为
净减少量是 q(k−1)−1,符号可以为负。体内每条赋值、事件和实际选中的条件仍执行与源相同的次数;另增加一次次数捕获赋值,以及商、余数各一次计算。这里不合并源 i 递增,也不删除尾段控制。
若一个原体共有 b 个语句节点,包含 if 及其两支的全部静态节点,源 repeat 子树为1+b节点。目标子树恰为3+(k+1)b节点:一个捕获、两个 repeat、k份组体加一份尾体。空体 b=0 时输出为3节点,生成器直接使用空元组,不进行 k 次空复制;这保证巨大的 k 不会仅因空体而触发无用编译循环。
本例原体 b=6,k=4,code 从7增到33节点。先外提时两专用体各4节点,总code12;再分别展开后总code48。最终机器码大小可能受后续折叠和布局影响,本页节点数仅是实际输出IR的可核指标。对二进制编码的 k,显式生成 k 份体本来就可能相对输入长度指数增长,所以不能宣称变换关于输入位长总为多项式。
令 S、T 为输入和输出的完整语法大小,v 为源变量数,L 为原 repeat 数。结构复制本身工作与输出空间都是 O(S+T)。参考器还有确定赋值和变量集合检查;在散列模型下,一个保守时间、空间上界分别为 O((S+T)(v+L+1)+1)。这些界不追求紧致,但把集合开销计入;新鲜名计数器在各循环间连续前进,不为每个名字重扫已经跳过的前缀。大整数运算和标识串长度另计。
预算检查先用各原体的 b 算出目标 code 节点数,再决定是否物化副本。超限仅意味着这次资源政策不接受输出规模,不是否定保持定理。执行器另有逐语句/控制访问预算;耗尽返回明确状态和已发生的事件前缀,不给默认返回值,也不把两次耗尽当作等价证书。
终点任务要求提交四个实际程序的完整观察、11/6控制数、7/12/33/48节点数,并用外部修改过的错误目标重跑。测试检验实现是否符合模板;对所有合法次数和体的保证来自上面的双射与语义复合证明,不来自抽样次数。
参考资料
[1] Frances E. Allen、John Cocke,A Catalogue of Optimizing Transformations,1971,原扫描pp.7–8,Loop Unrolling:顺序复制、控制成本和代码空间。本文不执行其中“仅在独立时可并行”的后续变换。
[2] David F. Bacon、Susan L. Graham、Oliver J. Sharp,Compiler Transformations for High-Performance Computing,1994,§6.3及§6.3.1,印刷368–369页,Figure22的余数尾段。本文采用独立的非负次数商余数模板和事件观察,未复刻原数组例或其机器周期估算。