https://go-review.googlesource.com/c/go/+/694016
循环不变代码外提 (Loop-invariant code motion)
https://www.google.com/search?q=Loop+Invariant+Code+Motion
Loop Invariant Code Motion(循环不变代码移动)是一种编译器优化技术。其主要目的是提高程序的执行效率。
基本概念
- 循环不变代码:在循环体内每次迭代都不会改变其结果的代码。例如,计算一个常数值或获取某个不变的数组元素。
优化过程
- 识别不变代码:编译器分析循环,找出循环内每次迭代结果不变的语句。
- 移动代码:将这些不变代码移动到循环外部,只在循环开始时执行一次。
例子
假设有如下代码:
1 | for i in range(n): |
优化后,可以改为:
1 | result = compute(x) # 移到循环外 |
优势
- 减少了循环体内的计算量,从而提升了程序的运行效率。
- 降低了不必要的重复计算,节约了时间和资源。
这种优化在处理复杂循环时特别有效,常用于编译器的优化阶段。
下面是一个简单的 Go 语言示例,展示如何使用循环不变代码移动进行优化。
原始代码
1 | package main |
优化后的代码
1 | package main |
解释
在原始代码中,expensiveComputation(x) 在每次循环中被调用,而其结果在每次迭代中都是相同的。通过将这个计算移到循环外部,优化后的代码只进行一次计算,从而提高了性能。
分析器:
当然可以。使用 Go 语言自带的 go/analysis 包,我们可以编写一个自定义的静态分析器 (static analyzer) 来检测这种可以进行“循环不变量代码外提”的模式。
静态分析器是一种在不实际运行代码的情况下,通过分析源代码的结构(例如抽象语法树 AST)来找出潜在问题的工具。go vet 就是一个著名的静态分析器。
下面我将为你展示如何编写一个基本的分析器来检测这个问题。
分析器的核心逻辑
我们的分析器需要完成以下几件事情:
- 遍历代码,找到所有的
for循环。 - 对于每一个
for循环,确定在循环体内哪些变量被修改了。 这包括循环变量(如i)、在循环内部被赋值 (=,:=,++,--) 的变量。 - 在循环体内寻找函数调用 (
CallExpr)。 - 检查这些函数调用的所有参数。 判断这些参数是否依赖于在循环体内被修改的变量。
- 如果一个函数调用的所有参数都不依赖于循环内被修改的变量,那么这个调用就是“循环不变量”的,我们应该报告它。
分析器实现
下面是一个完整的、可运行的分析器代码。
licm_analyzer.go
1 | // package licm_analyzer defines an analyzer that detects loop-invariant code motion opportunities. |
如何运行分析器
保存分析器代码:将上面的代码保存为
licm_analyzer.go。创建
go.mod文件 (如果项目还没有的话):1
2go mod init licm.analyzer
go get golang.org/x/tools/go/analysis/singlechecker创建要分析的目标代码:创建一个名为
test.go的文件,包含我们的测试用例。test.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package main
import "fmt"
func expensiveComputation(x int) int {
// Assume this is a time-consuming calculation
return x * x
}
func anotherComputation(x, y int) int {
return x + y
}
func main() {
x := 10
n := 1000000
sum := 0
// Case 1: This should be detected
for i := 0; i < n; i++ {
result := expensiveComputation(x) // Invariant call
sum += result
sum++
}
// Case 2: This should NOT be detected
for i := 0; i < n; i++ {
// 'i' is modified in the loop, so this call is not invariant
result := anotherComputation(x, i)
sum += result
}
// Case 3: A more complex case, also should be detected
factor := 2
for j := 0; j < 100; j++ {
localSum := 0
res := anotherComputation(x, factor) // Invariant call
localSum += res
fmt.Println(localSum)
}
fmt.Println(sum)
}运行分析:在终端中,运行以下命令。这会编译并运行
licm_analyzer.go,并让它分析当前目录下的所有代码 (./...)。1
go run ./licm_analyzer.go ./...
预期输出
运行后,你应该会看到如下输出,准确地指出了可以优化的代码位置:
1 | /path/to/your/project/test.go:21:14: call to expensiveComputation appears to be loop-invariant |
分析器成功地找出了第一个和第三个循环中的不变量调用,并忽略了第二个循环中依赖于循环变量 i 的调用。
重要限制和说明
- 函数副作用 (Side Effects):这个简单的分析器有一个巨大的前提:它假设被调用的函数是纯函数,即函数的输出只依赖于其输入参数,并且没有副作用(如修改全局变量、进行I/O操作等)。我们的分析器无法检测副作用,因此可能会错误地标记一个有副作用的函数(如
fmt.Println("hello")),尽管移动它可能会改变程序的行为。在上面的代码中,我简单地排除了fmt.Println,但一个生产级的分析器需要更复杂的机制来处理副作用。 - 指针和别名:分析器没有进行深入的数据流分析,无法处理通过指针或别名修改变量的情况。
- 复杂度:这是一个教学目的的简化版实现。一个工业级的 LICM 分析器(就像编译器内置的那样)会复杂得多,需要进行控制流分析、数据流分析和别名分析来确保优化的绝对安全。
尽管有这些限制,这个例子完整地展示了利用 go/analysis 框架来构建强大的自定义代码检查工具的基本思想和过程。
原文链接: https://dashen.tech/2017/08/17/Loop-Invariant-Code-Motion/
版权声明: 转载请注明出处.