Loading [MathJax]/jax/output/CommonHTML/jax.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Scalar Evolution (SCEV)

Scalar Evolution (SCEV)

原创
作者头像
谛听
修改于 2023-10-13 09:22:46
修改于 2023-10-13 09:22:46
91900
代码可运行
举报
文章被收录于专栏:随意记录随意记录
运行总次数:0
代码可运行

1 什么是 SCEV

Scalar Evolution(SCEV)用于分析循环中的标量(scalar)是如何变化的(evolution)。

SCEV 是 LLVM 中一个很重要的 analysis pass,可识别 general induction variables。该 analysis 主要用于 induction variable substitution 和 strength reduction。

例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 是如何对循环中的变量进行建模的。

2 SCEV 的数学框架 -- Chains of Recurrences

SCEV 采用非循环表达式对循环中的变量建模,该表达式称为 Chains of Recurrences (CR)。

2.1 Basic Recurrences

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int f = k0;
for (int i = 0; i < n; i++) {= f ;
    f = f + k1;
}

其中, 是循环不变量, 的递推公式为:

basic recurrence = ,表示初值是 ,每次递增

BR 形式化表示:

  • 是初值,是一个常数
  • 是一个二元运算, ∈ {+, ∗}
  • 是步长,是一个常数。

例如,BR = {3, +, 7} 可以生成序列 3, 10, 17, ...

2.2 Chain Recurrences

与 basic recurrence 相比,chain recurrrence 中的步长可能不是常量,而是另一个 basic recurrence。

例如 {1, +, {3, +, 7}} 指一个序列的初始值是 1,以另一个 BR = {3, +, 7} 作为步长进行增加:

上述式子比较抽象,写成代码形式会更容易理解变量 i 是如何变化的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// SCEV for i: {1, +, 3, +, 7} 
int i = 1;
int j = 3;
while (i < n) {
    i += j;
    j += 7;
}

CR 形式化表示:

2.3 构建 CR

常用的运算规则:

  • 是循环不变的

例如 12 + {7, 3} => {19, +, 3}

  • 是循环不变的

例如 12 * {7, +, 3} => {84, +, 36}

例如 {7, +, 3} + {1, +, 1} => {8, +, 4}

如果 fh 为常数,则公式还可表示为

例如 {0, +, 1} * {0, +, 1} => {0, +, 1, +, 2}

有了上述规则,就可以通过已有变量的 CR 推出其它变量的 CR,例如:

前面的规则比较好理解,如果有兴趣的话,可以看下最后一个规则的推导:

,则

所以

参考文献 7 给出了另一种推导方式。

3 SCEV 应用示例

计算 sum:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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$。

体现在代码上:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)

使用如下命令可查看优化前的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ clang -emit-llvm -S sum.c -o sum.ll

使用如下命令可查看优化后的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ clang -emit-llvm -O3 -S sum.c -o sum.ll

使用如下命令可查看 SCEV 分析:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ opt -analyze -scalar-evolution sum.ll

使用如下命令可以得到更为简短的输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ opt -analyze -scalar-evolution -scalar-evolution-classify-expressions=0 sum.ll

4 LLVM 中的 SCEV

4.1 SCEV 接口使用示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int simpleLoop(int n) {
    int sum = 0;
    for (int i = 0; i <= n; i++) {
        sum += i;
    }
    return sum;
}
  • getSCEV(i) = <0, +, 1><nsw, nw>
  • getExitCount(latchBB) = n
  • getSCEVAtScope(sum, nullptr) = n * (n + 1) / 2,有助于减少一些小的循环
  • getLoopDisposition(n) = Invariant
  • isKnownNonNegative(i) = true

4.2 规范 loop

先将循环规范后,再供 SCEV 使用。

循环

header:循环入口节点

backedge:回到 header 的边

latch:backage 的起始节点

exiting:后继位于循环外部的节点

exit:前继位于循环内部的节点

规范后的循环

  • Preheader header 节点的唯一前继,支配(dominate)整个 loop
  • Single Backedge
  • Dedicated Exit Loop-closed SSA 在循环边界处为在循环内部定义但在循环外部使用的变量插入 phi 指令

4.3 SCEV 函数

getSCEV(V)

生成变量 V 的 SCEV 表达式。

getSCEVAtScope(S, L)

返回循环 L 中变量 S 的 SCEV 表达式。如果 S 位于 L 外,则可结合 getExitCount 计算 S 的退出值。

SCEV 表达式支持:

  • 整型和指针型,不适用于浮点型
  • 整数类型转换:zext, sext, trunc
  • 运算:min, max, add, mul, udiv,不适用于 sdiv

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)。

  • BE count 统计了 loop backedge 在循环退出前执行了多少次。
  • trip count 是 loop header 被执行的次数,通常被称为迭代次数。
  • 两者比较:tripe count = BE count + 1。如果循环在第一次迭代就退出,则 BE = 0,trip count = 1。使用 BE count 的原因是它的 bit-width 和控制循环的 IV(induction variable)宽度一样,而 tripe count 中的加 1 操作可能会导致溢出。

注意,如果循环异常退出,退出计数不会因此发生改变。如果循环从未从该出口退出过,该值可能是无限大,或者至少大于其它出口的退出计数。它只是一个近似实用的函数。

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 级别上的会有一些不同。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
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 级别。

SCEVExpander

有时候需要从 SCEV 表达式回到 IR 指令,比如在 IR 中计算退出计数或退出值,这可被 SCEVExpander 处理,它可生成一些必要的指令来计算 SCEV 表达式的值。

在扩展前,要检查两件事:

  • 扩展是否是安全的。一般如果表达式中包含除法,那么扩展是不安全的,因为无法证明除数不为 0。
  • 扩展的代价是否比较大。SCEV 表达式可能非常大,如果在执行转换时,该转换需要 50 条指令来计算 backedge taken count,可能是不值得的。

expander 尝试在两方面生成更加有效的代码:尽量将指令外提;尽量重复使用已有的指令。

expander 有多种模式来扩展操作,默认采用”canonical mode“,地址的表达会基于一个规范 {0, +, 1} 归纳变量。还有一种特殊的模式是 LSR 模式,会被 LSR pass 使用。

PredicatedScalarEvolution

有时候传入的 IR 不满足变换所需的某些先决条件,这种情况下,”predicted“ scalar evolution 允许我们假设某个谓词成立,并在该假设下计算 SCEV 表达式。

为了执行真正的转换,需要对循环进行版本控制:生成循环的两个副本,其中一个副本由假设的谓词保护。然后被保护的循环可基于这些谓词进行转换。

4.3 SCEV 实现

时间关系,没有读完 SCEV 源码。源码的注释中给出了如下参考文献:

  1. Chains of recurrences -- a method to expedite the evaluation of closed-form functions. Olaf Bachmann, Paul S. Wang, Eugene V. Zima
  2. On computational properties of chains of recurrences. Eugene V. Zima
  3. Symbolic Evaluation of Chains of Recurrences for Loop Optimization. Robert A. van Engelen
  4. Efficient Symbolic Analysis for Optimizing Compilers. Robert A. van Engelen
  5. Using the chains of recurrences algebra for data dependence testing and induction variable substitution. MS Thesis, Johnie Birch

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)

6(https://agosta.faculty.polimi.it/lib/exe/cotslides_llvm_2189e.pdf%3B?id=teaching%3Acompilers&cache=cache&media=teaching:cot_slides_llvm_2.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 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Markdown中的公式编辑, 看这一篇就够了!
在 {align} 中灵活组合 \text 和 \tag 语句。\tag 语句编号优先级高于自动编号。
数据STUDIO
2021/06/24
14.5K2
使用 Inkwell 生成 LLVM IR
本文中的例子拷贝自:https://pku-minic.github.io/online-doc
谛听
2023/06/13
1.1K0
llvm入门教程-Kaleidoscope前端-5-控制流
llvm是当前编译器领域非常火热的项目,其设计优雅,官方文档也很全面,可惜目前缺乏官方中文翻译。笔者在学习过程中也尝试进行一些翻译记录,希望能对自己或者他人的学习有所帮助。
hunterzju
2021/12/09
1.1K0
使用 LLVM 实现一个简单编译器
作者:tomoyazhang,腾讯 PCG 后台开发工程师 1. 目标 这个系列来自 LLVM 的Kaleidoscope 教程,增加了我对代码的注释以及一些理解,修改了部分代码。现在开始我们要使用 LLVM 实现一个编译器,完成对如下代码的编译运行。 # 斐波那契数列函数定义 def fib(x)     if x < 3 then         1     else         fib(x - 1) + fib(x - 2) fib(40) # 函数声明 extern sin(arg)
腾讯技术工程官方号
2021/09/18
3.2K0
【从零开始学深度学习编译器】十五,MLIR Toy Tutorials学习笔记之Lowering到LLVM IR
在上一节中,我们将Toy Dialect的部分Operation Lowering到Affine Dialect,MemRef Dialect和Standard Dialect,而toy.print操作保持不变,所以又被叫作部分Lowering。通过这个Lowering可以将Toy Dialect的Operation更底层的实现逻辑表达出来,以寻求更多的优化机会,得到更好的MLIR表达式。这一节,我们将在上一节得到的混合型MLIR表达式完全Lowering到LLVM Dialect上,然后生成LLVM IR,并且我们可以使用MLIR的JIT编译引擎来运行最终的MLIR表达式并输出计算结果。
BBuf
2021/11/19
1.2K0
C语言入门系列之5.循环控制结构程序
循环结构是程序中一种很重要的结构。 其特点是:在给定条件成立时,反复执行某程序段,直到条件不成立为止。 给定的条件称为循环条件,反复执行的程序段称为循环体。 C语言提供了多种循环语句,可以组成各种不同形式的循环结构:
cutercorley
2020/07/23
2.3K0
C语言入门系列之5.循环控制结构程序
[2] 使用 LLVM 实现一门简单的语言
IR 指中间表达方式,介于高级语言和汇编语言之间。与高级语言相比,丢弃了语法和语义特征,比如作用域、面向对象等;与汇编语言相比,不会有硬件相关的细节,比如目标机器架构、操作系统等。
谛听
2022/03/06
2.7K0
深入剖析 iOS 编译 Clang / LLVM
2000年,伊利诺伊大学厄巴纳-香槟分校(University of Illinois at Urbana-Champaign 简称UIUC)这所享有世界声望的一流公立研究型大学的 Chris Lattner(他的 twitter @clattner_llvm ) 开发了一个叫作 Low Level Virtual Machine 的编译器开发工具套件,后来涉及范围越来越大,可以用于常规编译器,JIT编译器,汇编器,调试器,静态分析工具等一系列跟编程语言相关的工作,于是就把简称 LLVM 这个简称作为了正式的名字。Chris Lattner 后来又开发了 Clang,使得 LLVM 直接挑战 GCC 的地位。2012年,LLVM 获得美国计算机学会 ACM 的软件系统大奖,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齐名。
用户7451029
2020/06/16
8.3K0
深入剖析 iOS 编译 Clang / LLVM
C语言万字基础笔记总结(一)
当运算符左右两个操作数类型不同时,编译器会将它们共同转换位某种数据类型,通常情况下,会向精度较大的那个类型转化。
远方的星
2021/08/02
9290
C语言万字基础笔记总结(一)
TF-char7-反向传播法
向量\left(\frac{\partial \mathcal{L}}{\partial \theta_{1}}, \frac{\partial \mathcal{L}}{\partial \theta_{2}}, \frac{\partial \mathcal{L}}{\partial \theta_{3}}, \ldots \frac{\partial \mathcal{L}}{\partial \theta_{n}}\right)叫做函数的梯度
皮大大
2021/03/01
3150
TF-char7-反向传播法
傅里叶变换相关公式
在学习高数的时候,就接触了傅里叶变换。也就记得是将一些周期函数表示成一系列三角函数的叠加,不是很理解这个变换的具体意义,就是觉的挺神奇的,可以求一些特殊的积分什么之类的。 到了学习信号与系统的时候,离散序列也可以傅里叶变换,还有一个叫离散傅里叶变换,那时学得很草,考完试之后都混在一起,不知道谁是谁了。
全栈程序员站长
2022/09/07
2.9K0
傅里叶变换相关公式
[C语言]分支循环语句
2. 循环执行语句: do while 语句、 while 语句、 for 语句;
IT编程爱好者
2023/04/12
8420
[C语言]分支循环语句
Rust实战系列-基本语法
本文是《Rust in action》学习总结系列的第二部分,更多内容请看已发布文章:
abin
2023/03/21
2.4K0
Rust实战系列-基本语法
一网打尽 Rust 语法
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder
前端柒八九
2024/04/30
2050
一网打尽 Rust 语法
Rust学习笔记(3)- 变量和可变属性
就会直接报错,会提示不能第二次给不可变的变量赋值(cannot assign twice to immutable variable)。 除非写成这样:
TestOps
2022/04/07
5130
Rust学习笔记(3)- 变量和可变属性
Rust学习笔记之基础概念
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「基础概念」的相关知识点。
前端柒八九
2023/03/23
7560
Rust学习笔记之基础概念
第6章 | 循环控制流,return,loop,函数,字段,运算符,类型转换,闭包
break 表达式会退出所在循环。(在 Rust 中,break 只能用在循环中,不能用在 match 表达式中,这与 switch 语句不同。)
草帽lufei
2024/05/08
1710
【Rust学习】03_常用编程概念
本章介绍了几乎所有编程语言中出现的概念以及它们在 Rust 中的工作方式。许多编程语言的核心有很多共同点。本章中介绍的概念都不是 Rust 独有的,但我们将在 Rust 的背景中讨论它们,并解释使用这些概念的约定。
思索
2024/07/12
2670
【Rust学习】03_常用编程概念
江哥带你玩转C语言 | 07 - C语言流程控制
流程控制基本概念 默认情况下程序运行后,系统会按书写顺序从上至下依次执行程序中的每一行代码。但是这并不能满足我们所有的开发需求, 为了方便我们控制程序的运行流程,C语言提供3种流程控制结构,不同的流程控制结构可以实现不同的运行流程。 这3种流程结构分别是顺序结构、选择结构、循环结构 顺序结构: 按书写顺序从上至下依次执行 选择结构 对给定的条件进行判断,再根据判断结果来决定执行代码 循环结构 在给定条件成立的情况下,反复执行某一段代码 ---- 选择结构 C语言中提供了两大选择结
极客江南
2021/07/11
1.7K0
Archived | 307-09-矩阵
定义矩阵A,B,其中A的大小为a \times b,B的大小为b \times c,对于矩阵C=AB中的每一个元素C(i.j),~i\in [1, a],~j\in [1,c],存在以下:
gyro永不抽风
2021/05/21
6260
相关推荐
Markdown中的公式编辑, 看这一篇就够了!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档