Scalar Evolution(SCEV)用于分析循环中的标量(scalar)是如何变化的(evolution)。
SCEV 是 LLVM 中一个很重要的 analysis pass,可识别 general induction variables。该 analysis 主要用于 induction variable substitution 和 strength reduction。
例子:
void foo(int *a, int n, int k) {
int t = 0;
for (int i = 0; i < n; i++) {
t = t + k;
}
*a = t;
}
t 的初值是 0,每次循环都会对 t 加 k,这便是 t 的演化。
但只有 t 的终值会被 *a = t
用到,那么是否有一种方法可以直接算出 t 的终值,而不必进入循环中一步一步去算?
下图使用了 SCEV 分析结果后进行优化的例子:
优化后的 IR 直接用 t = n * k
得到了 t 的终值,从而去掉了 for 循环。
另外一个例子:
优化后的 IR 用加法代替了乘法运算。
以上优化是如何是如何做到的?下一章看下 SCEV 是如何对循环中的变量进行建模的。
SCEV 采用非循环表达式对循环中的变量建模,该表达式称为 Chains of Recurrences (CR)。
int f = k0;
for (int i = 0; i < n; i++) {
… = f ;
f = f + k1;
}
其中, 和 是循环不变量, 的递推公式为:
basic recurrence = ,表示初值是 ,每次递增 。
BR 形式化表示:
例如,BR = {3, +, 7} 可以生成序列 3, 10, 17, ...
与 basic recurrence 相比,chain recurrrence 中的步长可能不是常量,而是另一个 basic recurrence。
例如 {1, +, {3, +, 7}} 指一个序列的初始值是 1,以另一个 BR = {3, +, 7} 作为步长进行增加:
上述式子比较抽象,写成代码形式会更容易理解变量 i 是如何变化的:
// SCEV for i: {1, +, 3, +, 7}
int i = 1;
int j = 3;
while (i < n) {
i += j;
j += 7;
}
CR 形式化表示:
常用的运算规则:
例如 12 + {7, 3} => {19, +, 3}
例如 12 * {7, +, 3} => {84, +, 36}
例如 {7, +, 3} + {1, +, 1} => {8, +, 4}
如果 f 和 h 为常数,则公式还可表示为
例如 {0, +, 1} * {0, +, 1} => {0, +, 1, +, 2}
有了上述规则,就可以通过已有变量的 CR 推出其它变量的 CR,例如:
前面的规则比较好理解,如果有兴趣的话,可以看下最后一个规则的推导:
设 , ,则 ,
所以
参考文献 7 给出了另一种推导方式。
计算 sum:
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i * i * i;
}
其中, 的 CR 可用如下式子简化
所以
下图中的方式也可以计算出 CR
第 1 行表示循环次数,第 2 行是 sum 的值,第 3 行是两次循环之间的 sum 的差值 ,第 4 行是两次循环之间 的差值 ,第 5 行是两次循环之间 的差值 ,第 5 行是两次循环之间 的差值 $\delta^4$。
体现在代码上:
int sum = 0;
int t1 = 1;
int t2 = 7;
int t3 = 12;
for (int i = 1; i <= count; i++) {
// sum += i * i * i;
sum += t1;
t1 += t2;
t2 += t3;
t3 += 6;
}
用加法运算取代了昂贵的乘法运算。
但是发现,求和过程中,虽然 sum 在不断变化,但是不需要追踪它的变化,只需要知道它的终值即可。
其实,有了 CR 后,我们可以直接用数学的方法推出该循环执行了 次后 是多少。例如, = {0, +, 3},可以直接算出循环 3 次后, = 9。对于一般情况 ,则第 次循环后 的值为
推导过程见参考文献 7。
可简单理解为: a_0 只有在最初时被加了 1 = 次 a_1 在每个循环中都被加了 i = 次 a_2 在第 2 个循环被加了 1 次,第 3 个循环被加了 2 次,...,共被加了 共被加了 所以
那么,对于 sum= {0, +, 1, +, 7, +, 12, +, 6},可以直接得到第 i 次循环后
编译器只需要生成类似生成计算上述式子的代码即可,从而将时间复杂度由 O(n) 降到 O(1)。
使用如下命令可查看优化前的代码:
$ clang -emit-llvm -S sum.c -o sum.ll
使用如下命令可查看优化后的代码:
$ clang -emit-llvm -O3 -S sum.c -o sum.ll
使用如下命令可查看 SCEV 分析:
$ opt -analyze -scalar-evolution sum.ll
使用如下命令可以得到更为简短的输出:
$ opt -analyze -scalar-evolution -scalar-evolution-classify-expressions=0 sum.ll
int simpleLoop(int n) {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return sum;
}
先将循环规范后,再供 SCEV 使用。
循环
header:循环入口节点
backedge:回到 header 的边
latch:backage 的起始节点
exiting:后继位于循环外部的节点
exit:前继位于循环内部的节点
规范后的循环
生成变量 V 的 SCEV 表达式。
返回循环 L 中变量 S 的 SCEV 表达式。如果 S 位于 L 外,则可结合 getExitCount 计算 S 的退出值。
SCEV 表达式支持:
SCEV 表达式可嵌套,叶子节点是常数或者“unknown”。
不同于 IR 指令,SCEV 表达式支持多个操作数,比如 %a + %b + %c。
SCEV 表达式在构建的时候就会被规范化,比如会将 %a + (%b + %c) 替换为 %a + %b + %c,会将 %a + 1 替换为 1 + %a,会将 %a - %b 替换为 (-1 * %b) + %a。所以给定一系列操作数和类型,只会对应唯一的 SCEV 表达式。如果想比较两个 SCEV 表达式是否相同,只需要比较它们的指针是否相同即可。
getExitCount(L, ExitBB, ExitKind)
对于循环 L,返回某个退出块 ExitBB 的退出计数(backedge-taken count, BE count)。
注意,如果循环异常退出,退出计数不会因此发生改变。如果循环从未从该出口退出过,该值可能是无限大,或者至少大于其它出口的退出计数。它只是一个近似实用的函数。
ExitKind 有三种模式: exact:仅适用于只有一个出口的循环; symbolic max:可以认为是所有出口中的最大退出计数; constant max:是 symbolic max 的常量上限,常见的值是 -1(或者在无符号数情况下是 UINT_MAX)。
有了 BE count,就可以算出”exit value“或”SCEV at scope" 。例如,有 SCEV 为 {2, +, 3},BE count 为 -1 + %n,那么退出值为 2 + 3 * (-1 + %n) 或 -1 + (3 * %n)
Predicate checking
LoopDisposition / Invariance / dominates
对于给定值,返回它是否”可能“是潜在循环不变的,是否是支配的,主要用于 SCEV 程序内部。
注意,这里的“潜在循环不变”比较微妙,SCEV 级别上的 domination / invariance 和 IR 级别上的会有一些不同。例如:
/*
for (int i = 0; i < end; i++) {
d = a / b;
}
*/
loop:
%iv = phi i32 [ 0, %preheader ], [ %iv.next, %loop.latch ]
%iv.next = add i32 %iv, 1
%cmp = icmp ne i32 %iv.next, %end
br i1 %cmp, label %loop.latch, label %exit
loop.latch:
%div = udiv i32 %a, %b ; operands defined outside of loop
br label %loop
udiv i32 %a, %b
不能被提到循环外,因为如果循环在第一次迭代就退出的话,udiv 是不会被执行到的。如果向 LoopInfo 询问 %div
对于 %loop
是否是循环不变的,答案是否。
但是,如果看 SCEV 表达式 %a /u %b
,那么它对于 %loop
是循环不变的,可以提到循环外面。所以 SCEV 级别上的 domination / invariance 要强于 IR 级别。
有时候需要从 SCEV 表达式回到 IR 指令,比如在 IR 中计算退出计数或退出值,这可被 SCEVExpander 处理,它可生成一些必要的指令来计算 SCEV 表达式的值。
在扩展前,要检查两件事:
expander 尝试在两方面生成更加有效的代码:尽量将指令外提;尽量重复使用已有的指令。
expander 有多种模式来扩展操作,默认采用”canonical mode“,地址的表达会基于一个规范 {0, +, 1} 归纳变量。还有一种特殊的模式是 LSR 模式,会被 LSR pass 使用。
有时候传入的 IR 不满足变换所需的某些先决条件,这种情况下,”predicted“ scalar evolution 允许我们假设某个谓词成立,并在该假设下计算 SCEV 表达式。
为了执行真正的转换,需要对循环进行版本控制:生成循环的两个副本,其中一个副本由假设的谓词保护。然后被保护的循环可基于这些谓词进行转换。
时间关系,没有读完 SCEV 源码。源码的注释中给出了如下参考文献:
getSCEV(V) 的实现与上述文献 1 中的逻辑有点像:
1 2018 EuroLLVM Developers’ Meeting: J. Absar
2(https://kristerw.blogspot.com/2019/04/how-llvm-optimizes-geometric-sums.html)
3(https://www.cnblogs.com/gnuemacs/p/14167695.html)
4(https://www.youtube.com/watch?v=y07BE1og4VI)
5(https://llvm.org/devmtg/2009-10/ScalarEvolutionAndLoopOptimization.pdf)
7(https://dl.acm.org/doi/pdf/10.1145/190347.190423)
8(https://www.npopov.com/2023/10/03/LLVM-Scalar-evolution.html)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有