- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
**摘要:**常量传播,顾名思义,就是把常量传播到使用了这个常量的地方去,用常量替换原来的变量。
本文分享自华为云社区《编译器优化那些事儿(2):常量传播》,作者:毕昇小助手。
例如
min = a;
if(min > b)
min = b;
return min;
转换为SSA形式为:
min = a;
if(min > b)
min2 = b;
return Φ(min, min2)
首先来认识一下什么是常量传播,常量传播也叫常量折叠,但有些资料中对它们的定义又是区分开来的。下面来看看它们分别是什么?
常量传播,顾名思义,就是把常量传播到使用了这个常量的地方去,用常量替换原来的变量。
x = 3;
y = 4;
z = x + y;
->
x = 3;
y = 4;
z = 3 + 4;
什么是常量折叠?常量折叠就是当运算符左右两边都是常数时,把结果计算出来替代原来的那部分表达式。
z = 3 + 4;
–>
z = 7;
现在的常量传播优化技术能同时实现上面介绍的传播和折叠的功能,所以现在通常不对它们加以区分,本文后续就把常量传播和折叠统称为常量传播了。
程序中的常量来源有3种
从上面简单的例子中可以看到,常量传播可以把原本在运行时的计算转移到编译时进行,减小了程序运行时的开销。同时常量传播还有助于实现其它的优化,比如死代码消除。
前面介绍的常量传播的例子非常简单而且直观。如果程序的控制逻辑比较复杂时,判断一个变量是否是常数,就不是一件简单的事了,需要借助数据流的分析才能判断某个变量是否是常量。而且这个判断是保守的,即不能充分证明某个变量是常量的话就认为它是变量。这种保守的分析结果是可以接受的,因为我们优化程序的时候在性能提升与程序语义保证时优先选择保证程序语义不变。
下面我们先初步学习一下数据流分析。
数据流分析常常是为了实现全局优化、过程间优化,或者程序静态分析而进行的分析技术。分析得到的信息可以支撑各种优化技术的落实。
考虑下面这条指令,我们能对它做什么优化呢?
z = x + y;
最基本的,有下面3种假设:
可以看到对每一种可能的优化,都需要一定的依据。这些依据就需要进行数据流的分析来获取。
下面我们介绍一下非常基础的3种数据流模式。
程序中的每一条语句都会对程序的状态产生影响,程序的状态包括了寄存器的值、内存的值、读写的文件等。对于特定的数据流分析,我们只关心对我们分析或者程序优化有用的那部分内容。比如对到达定值分析,我们只跟踪变量的定值情况,对可用表达式的分析,我们跟踪表达式的生成以及表达式分量的赋值情况,对于活跃变量我们关心变量的赋值和使用情况。
我们用转移函数来表示程序语句对程序状态的影响:
OUT=f_d(IN)OUT=fd(IN)
f_dfd 是语句d的转移函数。IN是语句d前面的程序状态,OUT是语句d之后的程序状态。
同样的,基本块对程序状态也有影响,基本块也有转移函数:
OUT = f_B(IN)OUT=fB(IN)
f_BfB 是基本块B的转移函数,IN是基本块B之前的程序状态(也可表示为IN[B]),OUT是基本块B结束后的程序状态(也可表示为OUT[B])。
不同的分析目的,转移函数不同,对于到达定值分析,如果遇到下面的一条语句
d: u = v + w
下面我们看一个具体的例子,在下面的这个流图中,每一个基本块生成的定值和杀死的定值已标记在图右侧。
图1 基本块的生成定值和杀死定值集合
对每个基本块,根据输入状态,应用转移函数,求解输出状态,循环进行此操作,直到所有的基本块的输出状态不再变化为止。
图2 数据流分析-交汇运算符的意义
对于上面的控制流图,B2前驱节点包括B1和B2自身。B2的输入状态需要汇合B1结束后的状态OUT[B1] 和 B2结束后的状态OUT[B2], 如何汇合呢?这取决于我们的分析目的,对于到达定值分析,我们要汇集所有路径上过来的定值集合,所以对前驱节点的输出做并集运算OUT[B1]∪OUT[B2];而对于可用表达式分析,只有每一条通往此基本块的路径上都有这个表达式的生成时我们才说这个表达式在此基本块上是可用的,所以交汇运算是求交集OUT[B1]∩OUT[B2]。交汇运算用符号∧表示。
对上面的这个控制流图中的B2基本块,在第一次分析时,它自身的输出是初始化的值∅, 第二次分析时,它的输出就是上一次分析后得到输出了,就可能不再是∅了。这样迭代多次后,它的输出不再变化。当所有的基本块的输出都不再变化时,我们说这个时候到了一个定点(fix point)。
当分析达到定点时,数据流分析的结果也就得到了。
说明:此算法是非SSA表示上的到达定值分析的算法,这里用这个相对来说比较原始的算法旨在介绍它最基本的思路;如果要在SSA形式上进行到达定值分析会更加高效,前提条件是需要把程序转换为SSA形式的表示。后续介绍的活跃变量分析,可用表达式分析也是如此。
下面我们用真实的程序对上“图1”表示的程序做到达定值分析,(这里仅列出主要的程序片段)
// 定义一个基本块的属性结构
struct N{
string name; // 基本块名称
bitset gen {}; // 生成的定值
bitset kill {}; // 杀死的定值
bitset in {}; // 输入定值集合
bitset out {}; // 输出定值集合
vector pre{}; // 前驱基本块
};
// 基本块的属性和初始化
shared_ptr entry, b1, b2, b3, b4, exit{};
entry = make_shared(N{"entry", 0, 0, 0, 0, {}});
b1 = make_shared(N{"b1", 0B0000111, 0B1111000, 0, 0, {}});
b2 = make_shared(N{"b2", 0B0011000, 0B1000011, 0, 0, {}});
b3 = make_shared(N{"b3", 0B0100000, 0B0000100, 0, 0, {}});
b4 = make_shared(N{"b4", 0B1000000, 0B0001001, 0, 0, {}});
exit = make_shared(N{"exit", 0, 0, 0, 0, {}});
// 构建基本块的前驱,形成CFG
entry->pre = {nullptr};
b1->pre = {entry};
b2->pre = {b1, b4};
b3->pre = {b2};
b4->pre = {b2, b3};
exit->pre = {b4};
// 到达定值分析过程
bool anyChange{};
vector BBs{b1, b2, b3, b4, exit};
do
{
anyChange = false;
for (auto B : BBs)
{
for (auto p : B->pre)
{
B->in |= p->out;
}
auto new_out = B->gen | B->in & ~B->kill;
if (new_out != B->out)
{
anyChange = true;
}
B->out = new_out;
}
} while (anyChange);
// 输出每个基本块的输入定值,和输出定值
for (auto B : BBs)
{
cout < < *B < < endl;
}
输出结果:
b1 0000000 1110000
b2 1110111 0011110
b3 0011110 0001110
b4 0011110 0010111
exit 0010111 0010111
第一列是基本块名称,第二列是输入定值集合的位向量,第三列是输出集合的位向量。
这样我们就得到了每一个基本块的输入定值集合和输出定值集合。例如对于基本块B4, 位向量<0011110>代表{d3,d4,d5,d6},是基本块开始处的到达定值集合,位向量<0010111>代表{d3,d5,d6,d7},是基本块结尾处的到达定值集合。有了这些信息就能很容易的得到在某一个程序点处p, 有哪些变量在何处被定值了。
把结果反馈到流图中如下:
图3 数据流分析-到达定值结果示例
类似的,我们用相似的方法可以进行活跃变量,可用表达式的分析。
一个变量x在程序点p上活跃,是说这个变量x从程序点p到程序结尾的路径上被使用,且在使用前没有对它进行新的赋值(没有被杀死)。
图4 数据流分析-活跃变量示例1
B1出口处的活跃变量只有b, 这是因为变量a再未被使用过,变量c被B2中的赋值杀死了。
B2出口处的活跃变量有b和c。因为只有b和c在后面的路径中被使用,且使用前未被重新赋值。
活跃变量分析的迭代算法
示例
图5 数据流分析-活跃变量示例2
上面的程序流图中,基本块B2, B3, B4的Use和Def为:
Use[B2] = ∅, Def[B2] = {b}
Use[B3] = {a}, Def[B3] = {a}
Use[B4] = {b}, Def[B4] = ∅
一个表达式是可用的,是说在通往程序点p的所有路径上都对那个表达式进行过计算,且计算后再未对表达式的分量进行赋值过。
图6 数据流分析-可用表达式示例1
图7 数据流分析-可用表达式示例2
\qquad for(除ENTRY之外的每个基本块B)\{ \\
\qquad \qquad IN[B]=\cap_{P是B的一个前驱}OUT[P]; \\
\qquad \qquad OUT[B] = e\_gen_B∪(IN[B]-e\_kill_B); \\
\qquad \} \\
} \
\end{array} $$
前面介绍了到达定值分析,活跃变量分析,可用表达式分析。它们都是非常基础的数据流分析,在很多优化过程中会使用到这些数据流分析的结果。现在我们再次回到常量传播的优化上来。常量传播也是需要进行数据流的分析。它非常类似于到达定值,但因为常数的性质,又有所不同。
图8 常量传播-半格
过程(函数)中的每一个变量都这样的一个半格,变量的值就是半格中的元素。在常量传播分析的初始阶段,所有的变量的值是不确定的,我们用UNDEF表示(对应格论中的最大值⊤); 随着我们的分析,有些变量的值是常数,它的值就可以对应到半格的中间那一行的某一个元素,比如常数2。随着分析的进行,一些变量的值不是常数时(比如定值来自不同的前驱,每个前驱中对该变量的定值不同),用半格最底下的NAC(Not a Constant)表示(对应格论中的最小值⊥)。
前面提到过,交汇运算是对不同集合的合并。对于常量传播,每个变量的交运算(Meet Operator) 是它的不同取值在半格中的最大下界。交汇运算的规则如下:
例子:
图9 常量传播-变量映射到半格的值
B2对x的定值为常数-2,B3对x定值为常数3,-2 ∧ 3就是在x变量的半格上取它们俩的最大下界,为NAC。
对语句 z = x + y,转移函数如下:
例如下面的一个流图,黄色标记表示因为程序语句的影响而发生变化的变量状态。变量x在基本块B2中的值是3, 在B3中的值是undef, 他们汇合后(最大下界)是常数3。
在B4中,x是常数3, x + 3 就是常数6,所以B4中的指令就可以优化为 y = 6了。
图10 常量传播-转移函数的示例
M[entry] == init
do
change = false
worklist < - all BBs; ∀B visisted(B) = false
while worklist not empty do
B = worklist.remove
visited(B) = true
m' = fB(m)
∀B' ∈ successors of B
if visited(B') then
continue
end
else
m[B'] ∧= m'
if m[B'] change then
change = true
end
worklist.add(B')
end
end
while(change == true)
示例
下面的程序代码
j = 1;
x = 10;
y = 1;
while(myRead()!=0){
x = x * y;
y = y * y;
j = j + y;
}
p = j + x + y;
printf("%d\n", p);
它的控制流图
图11 常量传播-示例的控制流图
对此控制流图应用上面的算法
⊤表示undef, ⊥表示NAC (not a constant)
在第三轮迭代时每一个基本块的输出不再变化,到达定点。可以看出在基本块B3出口处,只有x和y是常量,分别是10和1,j和p是非常量。
基于上面的分析,上述程序可以做出如下优化
图12 常量传播-程序优化的效果
毕昇和LLVM的SCCP是一个比较高级的常量传播的pass, 它使用的算法是 Sparse Conditional Constant Propagation。
它的原理和上述的简易算法类似,有2点区别:
好了,常量传播就为大家介绍到这里。常量传播涉及到数据流的分析,所以这篇文章在介绍常量传播时也介绍了一下数据流分析,以及常见的三种数据流分析的思路。
我的问题由两部分组成。 我注意到使用 cc 编译器的 sparc(sun) 上的 memalign(block_size,bytes) 不检查字节是否为 2 的幂,这与使用 mvsc 编译器的 int
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 6 年前。
当我尝试在我的 gwt-maven Projekt 上进行 maven-install 时,我得到了这个错误: [ERROR] Failed to execute goal org.apache.ma
gcc 有一个选项 -s 来生成汇编源代码。 csc(MS C# 编译器)或 dmcs(mono C# 编译器)是否等价?我的意思是那些编译器是否提供了一个选项来生成可以读取而不是执行二进制文件的 I
我在 matlab simulink 中有一个模型。我把matlab安装在D盘了。当我运行模型时,出现以下错误: Unable to locate a C-compiler required by S
我非常喜欢 Visual Studio 2012,因为 GUI 非常快速和灵活。问题是我需要 VS2010 的 VC++-Compiler。在 VS 2012 中设置旧的编译器、SDK 有什么可能吗?
就目前情况而言,这个问题不太适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、民意调查或扩展讨论。如果您觉得这个问题可以改进并可能重新开放,visit
我正在为类似 C 的语言开发编译器,但在语义分析和代码生成阶段遇到了一些困难。我的问题如下:1) 对于 if 语句,语法如下: if (expression) then statement1; sta
我想了解 php 编译器/解释器的工作原理。 我试图下载 php 源代码并试图了解它是如何工作的。我找不到合适的文档。如果有人可以阐明制作 php 编译器的模块以及 apache 服务器如何使用 ph
我有一些关于 python 的问题 为什么没有 python 编译器来创建本地代码?我找到了 py2exe 等,但它们只是随附了一个 python 解释器,因此,它又是执行代码的解释器。 是否无法创建
本文将是JVM 性能优化系列的第二篇文章(第一篇:传送门),Java 编译器将是本文讨论的核心内容。 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器
在 *nix 之类的系统或适当的工具包下是否有任何用于 ActionScript 3 的编译器来处理 Flash? 最佳答案 Flex SDK编译器 — mxmlc — 还将编译普通的 ActionS
我正在做一个C项目。但是其他人告诉我,由于没有C++编译器,所以无法构建它。 我不知道如何禁用C++的检测。这该怎么做? 最佳答案 检测C和C++工具链是CMake的默认行为。要禁用此行为,您需要手动
我正在寻找可以嵌入到我的程序中的 JIT 编译器或小型编译器库。我打算用它来编译动态生成的执行复数运算的代码。生成的代码在结构上非常简单:没有循环,没有条件,但它们可能很长(由 GCC 编译时只有几
多年来,我一直在 VB.NET 中使用 DEBUG 编译器常量将消息写入控制台。我也一直在以类似的方式使用 System.Diagnostics.Debug.Write。我一直认为,当 RELEASE
我了解编译器的前端和后端结构。但是,我不确定为什么编译器经常分为前端和后端。我相信有很多原因,你能给我几个吗?因为,大多数书籍/网站会告诉您它们是什么,但无法告诉您原因! 谢谢你。 最佳答案 前端处理
我有很多 JS 文件。其中一些相互依赖。其中许多依赖于 jQuery。我需要一种工具,它可以接受一个文件作为参数,传递地获取其所有依赖项,并以正确的顺序将它们编译成一个文件(基于依赖项) 依赖信息并不
我正在阅读著名的紫龙书第二版,但无法从第 65 页获取有关创建第一组的示例: 我们有以下语法(终端以粗体显示): stmt → expr; | if ( expr ) stmt | for ( opt
我正在寻找将 C# 语法编译为 native 代码(或者可能编译为 C++?)的选项。我对拥有正式成为该语言一部分的所有库不感兴趣,只是能够像编写 C++ 程序一样编写程序,但使用语言结构,例如部分类
编译器(例如:gcc)中的 -march 标志真的很重要吗? 如果我使用 -march=my_architecture 而不是 -march=i686 编译所有程序和内核,会不会更快 最佳答案 是的,
我是一名优秀的程序员,十分优秀!