Golang编译优化——公共子表达式消除

这篇具有很好参考价值的文章主要介绍了Golang编译优化——公共子表达式消除。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、概述

公共子表达式消除(Common Subexpression Elimination,CSE)也有书上称为冗余表达式消除,旨在减少程序中重复计算相同表达式的次数,从而提高程序的执行效率。

在程序中,如果同一个表达式在不同的地方多次出现并且具有相同的输入,则这个表达式就是一个公共子表达式。公共子表达式消除的目标是识别这些重复的表达式,并将它们计算一次,然后在需要时重用结果,而不是每次都重新计算。

以下是一个简单的公共子表达式:

x = b + c
y = a - d
z = b + c

在这个例子中,表达式 b + c 在两个地方都出现了,它是一个公共子表达式。如果程序执行这两个语句,那么每次都重新计算 b + c,这可能会浪费计算资源。通过公共子表达式消除优化,可以将这个表达式计算一次,然后将结果存储起来,以后需要时直接使用存储的结果,而不是重新计算。

通过公共子表达式消除优化后的代码如下所示,程序就不再重复计算表达式b + c,而是引用已经计算的值x

x = b + c
y = a - d
z = x

】关于公共子表达式的介绍,在《编译器设计》的局部值编号、可用表达式、缓式代码移动中都有与之相关的讲解。

二、公共子表达式消除

Golang中关于CSE的实现在文件src/cmd/compile/internal/ssa/cse.go中,算法的开始函数是cse(f *Func) 函数。CSE算法实现的步骤主要分为:

  • 初始划分等价值:算法会遍历函数中的每个基本块(Block),然后遍历每个基本块中的每个值(Value)。在遍历过程中,根据一组规则,将具有相同特征的值初始的划分为一组等价值,依赖规则在cmpVal(…)函数中。
  • 细分等价值:初始划分后,算法会进一步细分等价值,直到无法继续细分为止。细分等价值的过程主要是根据值的参数进行判断,如果一组等价值的参数不是等价值,则将其分割成不同的等价值。在细分等价值的过程中,算法会对每组等价值按照一定的顺序进行排序,以便进行比较和查找。
  • 替换重复表达式:细分等价值后,算法会对每组等价值选择一个代表值,然后将该等价值中的其他值替换为代表值。替换过程中,算法会检查值的参数是否符合支配关系,以确保替换后不会破坏程序的语义。在替换过程中,算法会记录替换的次数,以便在分析完成后进行统计和优化。

以下是我提取的 SSA IR 代码片段。接下来,将详细介绍 CSE 算法的实现步骤,并在解释过程中引入这段代码,以帮助理解。

b1:
    v1 = InitMem <mem>
    v5 = Const64 <int> [0]
    v6 = Const64 <int> [1]
    v7 = Const64 <int> [2]
    v8 = Const64 <int> [3]
    v9 = Add64 <int> v8 v7
    v10 = Less64 <bool> v5 v6
If v10 → b3 b2

b3: ← b1
    v13 = Add64 <int> v6 v7
Plain → b2

b2: ← b1 b3
    v19 = Phi <int> v9 v13
    v16 = Add64 <int> v6 v7
    v18 = Add64 <int> v7 v8
    v20 = Add64 <int> v19 v16
    v21 = Add64 <int> v20 v18
    v23 = MakeResult <int,mem> v21 v1
Ret v23

2.1 初始划分等价值

在cse(f *Func) 函数中,首先遍历所有基本块的所有值,只要值的返回值类型不是mem,将其都存入数组a中。类型相关的操作会有不稳定性。比如在一个代码中有v3 = Load v1v8 = Load v1两个值,v8的定义看似冗余,可以用v8 = v3去替换,实则不可。因为我们不确定数据从v3v8流动的过程中,是否有Store操作在v1地址写入了新的值。

IR片段中的值存入数组a中后如下:

a = {v5,v6,v7,v8,v9,v10,v13,v19,v16,v18,v20,v21}

以数组a为参数,调用partitionValues函数,对数组中的值排序后再进行初步分类。排序和分类依赖cmpVal(v, w *Value, …)函数,调用其依次比较vw的:opcode、auxint、参数个数nargs、如果值是Phi还需比较两个值是否在同一个块中、aux。如果这些属性全部相等,则vw可划分为一组初始等价值。

将IR代码片段带入到这部分算法中,重排后的数组a和初步划分得到的等价值数组partition如下。partition是个二维数组,每一项元素都是一组等价值,且任何一组等价值Value的个数都是大于1。一组等价值只有一条指令Value,说明程序中没有该指令的公共子表达式,不需要消除,所以也就没有必要将其加入到partition数组进行分析。

// 排序后的a
a = {v9,v13,v16,v18,v20,v21,v10,v19,v5,v6,v7,v8}

// 初步划分得到的等价值
partition = {
	{v9,v13,v16,v18,v20,v21},
}

2.2 细分等价值

细分等价值的过程主要是根据值的参数进行判断,如果一组等价值的参数不是等价值,则将其分割成不同的等价值。

2.2.1 给所有值标号

为了更好的完成这一过程,定义了一个数组valueEqClass,其下标Values.ID对应的值是对Value的一个标号。非等价值都有自己独一无二的标号(为-v.ID),而一组等价值的标号是相同的。valueEqClass数组中对值的标号,在细分等价值的过程中发挥着很重要的作用。

首先将遍历函数的所有值,执行valueEqClass[v.ID] = -v.ID,将每个值在数组valueEqClass中对应的项初始化为-v.ID。然后再遍历等价值数组partition,将一组等价值的Value在valueEqClass数组中对应下标的元素改成相同的值。

经过上面操作后,valueEqClass中的值如下所示。valueEqClass是个一维数组,我为了方便理解写成了下面这种形式,实际上写成[v1.id], [v5.id], ... ,[v21.id], [v23.id]这种形式更贴合其排列结构。

// 此时标号数组的值
valueEqClass[v1.ID] = -1
			[v5.ID] = -5
			[v6.ID] = -6
			[v7.ID] = -7
			[v8.ID] = -8
			[v10.ID] = -10
            [v19.ID] = -19
            [v23.ID] = -23
            [(v9,v13,v16,v18,v20,v21).ID] = 1

2.2.2 根据参数细分等价值

根据参数细分等价值,就是比较等价值的参数,如果一组等价值的参数是非等价值,则将其拆分成多组等价值。重复这一动作,直到所有的等价值都不可拆分。

下列代码就是重复拆分等价值直至不可拆分的逻辑。当遍历一次数组partition后,如果changed的值没有被改成true,说明等价值已经不可拆分(留意代码满足什么条件时不会改变changed的值)。遍历数组partition的每一组等价值时,对一组等价值做一下操作:确定值的参数位置按照参数的valueEqClass值为等价值排序寻找一组等价值的拆分点按照差分点拆分等价值

for {
	changed := false
	for i := 0; i < len(partition); i++ {
		// 确定值的参数位置
		// 按照参数的valueEqClass值为等价值排序
		// 寻找一组等价值的拆分点
		// 按照差分点拆分等价值
		changed = true
	}
	if !changed {
		break
	}
}

确定值的参数位置,是为了消除具有交换性的操作(加法、乘法等)给细分等价值带来的错误判断。如a + bb + a其实是等价的,但是算法判断时会误以为其不等价,我们要避免这种情况。代码中结构type opInfo structcommutative字段用来表示一个值的参数是否具有交换性,true为可交换,false为不可交换。

  • 如果一个值的参数不具有交换性,则不用对其进行任何操作;如v10 = Less64 <bool> v5 v6
  • 如果一个值的参数具有交换性,则将参数valueEqClass[value.ID]值较小的放在前面。如:
    v13 = Add64 <int> v6 v7
    valueEqClass[v6.ID] = -6
    valueEqClass[v7.ID] = -7
    	
    // 因为 -6 > -7 ,所以将两个参数的位置调换
    v13 = Add64 <int> v7 v6
    

对于等价值{v9,v13,v16,v18,v20,v21},确定参数位置前后的情况如下:

// 参数交换之前,及每个参数具体的valueEqClass值
v9 = Add64 <int> v8 v7		// (-8 -7)
v13 = Add64 <int> v6 v7		// (-6 -7 ) can commutative
v16 = Add64 <int> v6 v7		// (-6 -7 ) can commutative
v18 = Add64 <int> v7 v8		// (-7 -8)  can commutative
v20 = Add64 <int> v19 v16	// (-19 1)
v21 = Add64 <int> v20 v18	// (1 1)

// 参数交换之后
v9 = Add64 <int> v8 v7
v13 = Add64 <int> v7 v6     // commutative
v16 = Add64 <int> v7 v6     // commutative
v18 = Add64 <int> v8 v7     // commutative
v20 = Add64 <int> v19 v16
v21 = Add64 <int> v20 v18

按照参数的valueEqClass值为等价值排序。一组等价值对应的valueEqClass值是相等的,如{v9,v13,v16,v18,v20,v21}valueEqClass[(v9,v13,v16,v18,v20,v21).ID] = 1;而每一个值的参数的valueEqClass值不一定相等,所以需要对交换参数后的等价值排序。

对于两个等价值,按照参数的个数,排序规则如下:

  • 将两个值的第一个参数比较,valueEqClass值小的放在前面,大的放在后面,相等则保持位置不变。
  • 如果第一个参数相等,再比较第二个参数,valueEqClass小的放在前面,大的放在后面。
  • 第一、二个参数都相等,再比较第三个。最多只能有三个参数

对于{v9,v13,v16,v18,v20,v21}这一组交换参数后的等价值,排序前后的变化如下:

// 排序之前						
v9 = Add64 <int> v8 v7		// (-8 -7)
v13 = Add64 <int> v7 v6		// (-7 -6)
v16 = Add64 <int> v7 v6		// (-7 -6)
v18 = Add64 <int> v8 v7		// (-8 -7)
v20 = Add64 <int> v19 v16	// (-19 1)
v21 = Add64 <int> v20 v18	// (1 1)

// 排序之后						
v20 = Add64 <int> v19 v16	// (-19 1)
v9 = Add64 <int> v8 v7		// (-8 -7)
v18 = Add64 <int> v8 v7		// (-8 -7)
v13 = Add64 <int> v7 v6		// (-7 -6)
v16 = Add64 <int> v7 v6		// (-7 -6)
v21 = Add64 <int> v20 v18	// (1 1)

// 存放等价值的二维数组也会跟着变化
partition = {
	{v20,v9,v18,v13,v16,v21},
}

接下来就是在确定参数位置、且按照参数valueEqClass值排序后的等价值中查找拆分点。查找方式是:依次比较相邻的两个值的参数,如果值所有对应位置参数的valueEqClass值相等,则这两个值之间没有拆分点,否则这两个值之间就是一个拆分点; 将找到的拆分点存放在数组splitPoints中,等一组值的所有拆分点找完后,再利用splitPoints作拆分操作。

对于等价值{v20,v9,v18,v13,v16,v21},找到的拆分点是{1,3,5},所以数组splitPoints = {0,1,3,5}。拆分点就是等价值数组的下标。

最后是按照差分点拆分等价值,也就是将一组等价值按照拆分点拆分为多组等价值。如果一组等价值没有拆分点,则结束当前遍历不执行循环中的后续代码,即不进行拆分操作。不执行拆分操作,也就不会执行changed = true。如果所有的等价值都找不到拆分点,则changed = false的值不会改变,说明所有的等价值都不可拆分。

拆分等价值时,将存放所有等价值的数组partition的最后一项(最后一组等价值),移到当前遍历的等价值位置。实现代码如下:

partition[i] = partition[len(partition)-1]
partition = partition[:len(partition)-1]

再将拆分的多组等价值满足条件的组,从partition的最后一项位置(会将其覆盖,因为已经移到前面)开始追加(append)至其中。条件的具体限定如下:

  • 拆分的等价值只有一条指令,说明其已是非等价值,则不需要将其append至partition。原因之前已经解释过:一组等价值只有一条指令Value,说明程序中没有该指令的公共子表达式,不需要消除,所以也就没有必要将其加入到partition数组进行分析。执行valueEqClass[f[0].ID] = -f[0].ID将非等价值的标号改为其-v.ID
  • 如果拆分的等价值有多条指令,则给这一组值在valueEqClass对应的项上赋一个新的标号,并将其append至partition

将等价值{v20,v9,v18,v13,v16,v21}按照其拆分点{1,3,5}拆分后,变成四组值,分别是:{v20}{v9,v18}{v13,v16}{v21}。其中{v9,v18}{v13,v16}会被append到partition数组中,{v20}{v21}则会变成非等价值。此时标号数组valueEqClass和等价值数组partition的状态如下:

// 此时标号数组的值
valueEqClass[v1.ID] = -1
			[v5.ID] = -5
			[v6.ID] = -6
			[v7.ID] = -7
			[v8.ID] = -8
			[v10.ID] = -10
            [v19.ID] = -19
            [v20.ID] = -20
            [v21.ID] = -21
            [v23.ID] = -23
            [(v9,v18).ID] = 2
            [(v13,v16).ID] = 3
// 此时等价数组的状态
partition = {
	{v9,v18},
	{v13,v16}
}

第一次对partition遍历之前,其中只有一组等价值,且遍历过程对其进行了拆分。第二次对partition遍历之前,其中有两组等价值,遍历过程中分析发现其不可拆分。至此,所有的等价值都不可拆分,细分等价值任务已经完成。

// 第一次遍历之前的partition
partition = {
	{v20,v9,v18,v13,v16,v21},
}
// 第二次遍历之前的partition
partition = {
	{v9,v18},
	{v13,v16}
}

2.3 替换重复表达式

细分等价值得到的等价值数组partition,其中的每一组等价值都可以看作是一组重复的表达式。这些重复表达式并不能随意消除,要看其Value所在块的支配性,只有一个块b1支配另一个块b2(也就是b1b2的支配者)时,才能用b1中的Value消除b2中的Value。

假设有如下所示的CFG和Value,其中a、b、c、d均为常量。b1的支配者为b1b2的支配者为{b1,b2}b3的支配者为{b1,b3}b4的支配者为{b1,b4}

Golang编译优化——公共子表达式消除,Golang,编译器
可以用v1消除v4,即v5 = v1 + v2,因为v1所在块b1支配v4所在块b4v2v3之间无法消除,因为b2不支配b3b3也不支配b2

2.3 .1 按支配性排序并找出替换关系

关于支配性相关的知识,这里只介绍这么多。在《编译器设计》中总结过支配性、支配者树等知识,后面我会写文章来讲解Golang对这些内容的实现。

将等价值按照支配性排序。调用f.Sdom().domorder(v.Block)会返回一个值,排序则依赖这个值的大小。对于xyz三个基本块,作为参数分别调用domorder()函数,得出的值适用于如下规则:

  • 规则1:如果domorder(x) > domorder(y)x不支配y
  • 规则2:如果domorder(x) < domorder(y)domorder(y) < domorder(z)x不支配y,那么x也不支配z
  • 规则3:如果domorder(x) < domorder(y) < domorder(z)x支配y但不支配z,那么y也不支配z

检查两个块的支配关系。调用f.Sdom().IsAncestorEq(x, y *Block)检查在支配者树中x是否为y的祖先结点或者x等于y,也就是检查x是否支配y

既然可以直接检查两个块的支配关系,是否还有必要将等价值按照支配性排序呢?答案是有必要的,可以降低算法的时间复杂度。懒得解释了,很容易推出来,由O(n^2)降为O(n log n)

遍历等价值数组partition;将一组等价值e按照支配性排序,再遍历e中的值;如果e[i].Block支配e[j].Block0 <= i < j < len(e),则可以使用e[i]代替e[j]rewrite[e[j].ID] = e[i];重复以上过程直至partition遍历完成。

将我们的IR代码片段的等价数组带入此算法,得到的替换关系数组rewrite如下:

// 将partition带入到算法
partition = {
	{v9,v18},
	{v13,v16}
}
// 得出的替换关系数组
rewrite[v18.ID] = v9
	   [v16.ID] = v13

2.3 .2 进行替换操作

遍历所有块的所有值,根据替换关系数组rewrite替换Value的参数和ControlValues。以rewrite[v18.ID] = v9为例,说明v18可以被v9替换,因此:所有引用v18的Value的参数,都要替换成v9;所有以v18作为控制判断的ControlValues,也要替换成v9

完成替换操作后的IR代码片段如下。替换完成后,v16v18会变成死代码,在死代码删除时会将其删除。文章来源地址https://www.toymoban.com/news/detail-860939.html

b1:
    v1 = InitMem <mem>
    v5 = Const64 <int> [0]
    v6 = Const64 <int> [1]
    v7 = Const64 <int> [2]
    v8 = Const64 <int> [3]
    v9 = Add64 <int> v8 v7
    v10 = Less64 <bool> v5 v6
If v10 → b3 b2

b3: ← b1
    v13 = Add64 <int> v6 v7
Plain → b2

b2: ← b1 b3
    v19 = Phi <int> v9 v13
    v16 = Add64 <int> v6 v7             // become deadcode
    v18 = Add64 <int> v7 v8             // become deadcode
    v20 = Add64 <int> v19 v13           // v16 => v13
    v21 = Add64 <int> v20 v9            // v18 => v9
    v23 = MakeResult <int,mem> v21 v1
Ret v23

到了这里,关于Golang编译优化——公共子表达式消除的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Golang通过栈实现表达式运算(中缀表达式转后缀表达式解析语法)

    需求背景:将string表达式数组 [title == AUSU ( header == Wecome || brand != AC68U )] 解析并使用ES查询得到运算结果。 分析:带有括号的表达式,需要根据优先级解析,可将中缀表达式转换为后缀表达式,去除括号

    2024年02月14日
    浏览(50)
  • Golang 正则表达式

    正则表达式特点:用的时候不会,学会之后就忘。每次用到现学现看,照葫芦画瓢,用起来不舒服,整理常用的正则表达式用法,及Golang 正则库使用。 元字符 含义 样例 .(点) 匹配除n r 以外的任何字符 c.t ,匹配cat、cbt、cct等 [xy](中括号) 单字符匹配 或 的关系 c[abc]

    2024年02月11日
    浏览(26)
  • 【Golang】Perl 正则表达式语法的支持示例

    在 Golang 中,标准库的正则表达式包 regexp 是基于 RE2 语法的,并不直接支持 Perl 正则表达式的全部功能。虽然 Golang 的标准库并不直接提供对 Perl 正则表达式的支持,但是您可以使用第三方库来实现与 Perl 兼容的正则表达式功能。 一个常用的第三方库是 github.com/dlclark/regexp2

    2024年01月17日
    浏览(59)
  • 【编译原理】【词法分析】【正则表达式】【NFA】【DFA】【C++】正则表达式转DFA&NFA,判断字符串是否符合正则表达式的匹配算法

    显然,正则表达式、NFA、DFA的概念都很简单,所以直接上代码,注释应该解释地比较清楚, 没有万能头文件的自行替换需求库 ,如果有疑问的可以留言。 网盘链接 [自行补全]/s/1pbGT_wpB662TwFrnukXgGQ?pwd=TSIT 提取码:TSIT 原理可以参考这篇博客 传送门 本次程序由四个文件组成 文

    2024年02月11日
    浏览(88)
  • 深入探讨Lambda表达式转换为委托类型的编译过程

    了解了,如果要深入探讨Lambda表达式转换为委托类型的编译过程,我们需要关注C#编译器如何处理这个转换。这个过程涉及到编译时的类型推断、匿名方法的创建,以及生成对应的委托实例。我们来更详细地分析这个过程: 编译阶段 1. 解析Lambda表达式 词法分析 :编译器首先

    2024年02月20日
    浏览(35)
  • Visual Studio 2010 C++编译错误“表达式必须包含整数或枚举类型“

    Visual Studio 2010 C++编译错误\\\"表达式必须包含整数或枚举类型\\\" 在使用Visual Studio 2010编写C++代码时,有时候会出现这样的编译错误:“表达式必须包含整数或枚举类型”。这个错误通常是因为我们在写代码时使用了错误的数据类型或者运算符导致的。 下面我们来看一个例子: 在

    2024年02月08日
    浏览(45)
  • 编译原理:正则表达式/正规式转NFA(原理+完整代码+可视化实现)

    【本文内容摘要】 (1)从中缀表达式转换为后缀表达式 (2)从后缀表达式转换为NFA (3)打印NFA大致内容 (4)生成dot文件。 (5)完整代码 如果本文对各位看官有用的话,请记得给一个免费的赞哦(收藏也不错)! 下面链接详细讲述了如何从中缀表达式转换为后缀表达式

    2024年01月17日
    浏览(38)
  • C#正则表达式性能优化:[0-9] vs. \d,轻松提升匹配效率

      概述: 在C#中,正则表达式`d`相对于`[0-9]`可能效率稍低,因为`d`包含更广泛的Unicode数字字符。为提高性能,可使用`[0-9]`并结合编译优化。以下示例演示性能测试及优化,适用于提高正则表达式匹配效率的场景。 在C#中,正则表达式 d 涵盖更广泛的 Unicode 数字字符范围,

    2024年04月11日
    浏览(50)
  • 编译原理复习一:正则表达式-NFA NFA-DFA DFA最小化(附题目与答案 超详细)

    需要原卷和答案可以点赞关注收藏评论区留言私信 对题目解法有疑问也可留言 下面以具体考试题目来讲解编译原理考试中的重点题目,大致可以分为以下几道大题 1:正则表达式转换为NFA,NFA转换为DFA,DFA最小化 2:LR(0)分析,构造LR(0)自动机,进一步对SLR(1)进行分析,由于

    2023年04月22日
    浏览(31)
  • NIFI1.23.2_最新版_性能优化通用_技巧积累_使用NIFI表达式过滤表_随时更新---大数据之Nifi工作笔记0063

      nifi好用,但是对机器的性能要求也高,如果性能达不到,就会导致,问题发生,比如,队列里显示有内容,但是实际上队列是空的,清也清不掉,只能重启,很麻烦.   关于优化:1.配置前端页面刷新的间隔时间默认30秒,我们可以自己需要看的时候手动刷新我们改成300sec 2.修改CPU阻塞时间

    2024年02月06日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包