4 中端优化(机器无关优化)
编译器的前端将源代码形式的程序转换为某种中间表示(Intermediate representation IR)。后端将IR程序转换为某种可以直接在目标机上执行的形式。编译器优化的任务是转换前端产生的IR程序,以提高后端生成的代码的质量,使编译后的代码执行得更快速或者使可执行程序在运行时耗费较少的资源或占用较少的内存空间。所有这些目标都属于优化的领域。
中端优化,又称为机器无关优化。能够在大多数目标机上改进代码的变换被认为是与机器无关的。编译器的机器无关优化是一系列在编译过程中应用的优化技术,这些技术不依赖于特定的硬件架构,而是专注于提高程序的执行效率、减少内存使用或降低能耗。
这些优化技术可以在编译器的不同阶段实施,从前端的语义分析到后端的代码生成。通过这些机器无关优化,编译器能够生成更高效、更紧凑的代码,同时保持程序的可移植性。开发者可以通过选择合适的编译器优化级别和选项来进一步提升程序性能。
在编译器的优化过程中,中端优化涉及到多种技术和策略,有些优化是有依赖性的,有些优化是需要进行多次执行,不同的优化方案,优化顺序也有所不同,因此中端优化没有一个固定的流程。中端优化主要分为数据流分析和标量优化两个部分,这是紧密相关但又有所区别的两个概念。下面详细解释它们之间的区别和联系:
数据流分析:关注的是数据(如变量和值)在程序控制流中的流动情况。它分析在程序的每个点上,哪些数据是已知的、活跃的或可达的。在分析范围上说通常跨越整个函数或多个基本块,分析全局的数据流动和依赖关系。就输出结果来说输出的是数据的状态信息,如变量是否活跃、表达式是否可用等。数据流分析是针对全局的。
标量优化:关注的是对单个变量或标量值的优化,包括消除不必要的计算、替换常量、优化表达式等。通常局限于单个基本块或较小的代码片段,优化局部的计算和存储操作。输出的是优化后的代码,如替换了常量的表达式、消除了死代码等。标量优化是局部的。
数据流分析为标量优化提供了必要的信息。例如,通过活跃变量分析,标量优化可以识别出哪些变量的值在特定点上是不必要的,从而进行优化。标量优化通常在数据流分析之后进行。数据流分析的结果被用作标量优化的输入,帮助标量优化更有效地进行代码转换。两者都旨在提高程序的性能和效率。数据流分析通过提供数据状态信息帮助标量优化做出更明智的决策,而标量优化则直接改善代码的执行效率。在编译器的优化过程中,数据流分析和标量优化往往是迭代进行的。标量优化可能会改变数据的流动,从而需要重新进行数据流分析。数据流分析提供了宏观的数据状态视图,而标量优化则关注于微观的代码细节。两者相互补充,共同实现编译器的优化目标。
虽然可以将编译器中端优化分为数据流分析和标量优化两个部分来讨论,但它们在实际的编译过程中是相互依赖和交织在一起的。数据流分析提供了优化所需的关键信息,而标量优化则利用这些信息来直接改进代码。因此,将它们视为编译器中端优化的两个互补的方面可能更为准确。
4.1 数据流分析
数据流分析是编译器优化中的一个核心概念,它涉及对程序执行过程中变量的可能状态进行分析,以识别和实现代码优化的机会。
数据流分析的目标是理解和预测程序在运行时的行为。这涉及到分析程序中所有可能的执行路径,并从中提炼出关键信息。程序的执行可以视为在不同状态之间的转换,而数据流分析就是在这些转换中寻找模式和机会以优化程序执行。
数据流分析是编译器优化的基石,它为识别和实现代码优化提供了必要的信息和框架。通过深入理解程序的执行路径和状态变换,编译器能够实施强度削弱和归纳变量删除等技术,优化循环执行,提升程序性能。这些技术不仅减少了计算量,也提高了代码的效率和可维护性,是提升软件性能的关键策略之一。
数据流分析是一种静态分析技术,它可以通过分析程序的数据流来提供关于程序行为的信息。在中间代码优化中,数据流分析可以用于识别循环不变量、死代码等,并进行相应的优化。
4.1.1 迭代数据流分析
在GCC编译器中,迭代数据流分析是一种重要的优化技术,它通过多次迭代来提高数据流分析的精度和优化效果。迭代数据流分析的目的是更准确地确定变量的定义和使用,以及它们之间的依赖关系,从而使得编译器能够进行更有效的指令调度和代码优化。GCC编译器中的迭代数据流分析是通过一系列的算法和步骤来实现的,流程解释:
- 开始:启动GCC编译器,开始编译过程。
- 构建GIMPLE表示:GCC将源代码转换为GIMPLE中间表示形式,这是GCC特有的中间表示,适合于进行优化。
- 构建控制流图CFG:在GIMPLE基础上,构建控制流图(CFG),标识基本块和控制流边。
- 初始化数据流信息:为每个基本块初始化数据流信息,如in和out集合。
- 选择分析方向:根据需要进行的分析类型(如活跃变量分析、到达定义分析等),确定是进行前向分析还是后向分析。
- 按照逆后序遍历CFG:对于前向分析,按照逆后序遍历CFG,这有助于提高迭代效率。
- 按照前序遍历CFG:对于后向分析,按照前序遍历CFG。
- 更新数据流信息:在遍历过程中,根据数据流方程更新每个基本块的数据流信息。
- 工作列表是否为空:检查是否还有基本块需要更新数据流信息。
- 完成:当工作列表为空,表示所有相关基本块的数据流信息已更新完毕,迭代结束。
- 输出优化结果:将优化结果应用于代码,生成优化后的代码。
这个流程提供了一个高层次的视图,展示了迭代数据流分析在GCC中的实现流程。具体的实现细节可能会因GCC版本和具体优化而异。
GCC中的迭代数据流分析是一个复杂的过程,涉及到多个步骤和算法。主要在编译器中端使用,通过迭代地更新数据流信息,直到达到不动点,GCC能够为后续的编译优化提供准确的数据流信息。这些信息对于提高程序的性能和资源利用率至关重要。
迭代数据流分析在GCC中的优化主要表现在以下几个方面:
全局公用子表达式消除:GCC尝试移动那些仅仅被自身存储kill的装载操作的位置,将循环内的load/store操作序列中的load转移到循环外面,减少不必要的操作。
存储操作的移动:当一个存储操作pass在一个全局公用子表达式消除的后面,这个pass将试图将store操作转移到循环外面去。如果与GCSE配合使用,可以提高运行效率。
消除不必要的load操作:GCSE pass将消除在store后面的不必要的load操作,这些load与store通常是同一块存储单元(全部或局部)。
删除空指针检查:通过对全局数据流的分析,识别并排除无用的对空指针的检查。编译器假设间接引用空指针将停止程序。
寄存器重分配(regmove):编译器试图重新分配move指令或者其他类似操作数等简单指令的寄存器数目,以便最大化的捆绑寄存器的数目。
死代码删除:通过迭代数据流分析,识别并删除那些不会被执行的代码,减少不必要的计算和内存访问,提高程序的效率。
循环展开:将程序中的循环结构展开为多个循环迭代,减少循环的开销和迭代的次数,提高程序的效率。
常量传播和复写传播:将程序中的变量替换为其对应的常量值,避免不必要的计算和内存访问,提高程序的效率。
数据预取:通过数据预取的方式,提前将需要用到的数据加载到缓存中,减少CPU等待数据的时间,提高程序的执行效率。
代码移动:通过函数重排的方式,改变函数调用顺序,让程序在访问数据时更加连续,提高程序的性能。
GCC中的迭代数据流分析代码主要位于GCC源代码的中端部分,后端也会有(尤其指令调度)具体涉及到的数据流分析框架和实现代码分布在不同的文件中。以下是一些关键的文件和它们的作用:
dom_walker 类:这个类是GCC中用于执行数据流分析的核心类之一。它通过深度优先搜索(DFS)算法遍历函数的支配树,并对每个基本块(basic block)调用回调函数来执行数据流分析。具体的实现代码可以在mark_def_dom_walker::walk()函数中找到,该函数从函数的入口基本块开始,根据DFS算法遍历支配树,并执行数据流分析的相关操作。
mark_def_dom_walker::before_dom_children:这个函数是dom_walker类的回调函数之一,它在遍历支配树的过程中被调用,用于处理每个基本块之前的数据流分析工作。
这些文件和类构成了GCC中迭代数据流分析的核心框架,它们负责在编译过程中收集和分析程序的数据流信息,以便于进行后续的优化。具体的实现细节和代码结构可能会随着GCC版本的更新而有所变化,但上述提到的文件和类是理解GCC中迭代数据流分析的关键部分。
迭代数据流分析即为深度优先搜索算法,在GCC优化中广泛使用。
4.1.2 静态单赋值分析
静态单赋值形式(static single assignment form,通常简写为SSA form或是SSA)是中介码(IR,intermediate representation)的特性,每个变数仅被赋值一次。在原始的IR中,已存在的变数可被分割成许多不同的版本,在许多教科书当中通常会将旧的变数名称加上一个下标而成为新的变数名称,以至于标明每个变数及其不同版本。
编译器最佳化的算法,可以借由SSA的使用,达到以下的改进:
常数传播(constant propagation)
值域传播(value range propagation)
稀疏有条件的常数传播(sparse conditional constant propagation)
消除无用的程式码(dead code elimination)
全域数值编号 (global value numbering)
消除部分的冗余 (partial redundancy elimination)
强度折减(strength reduction)
暂存器配置(register allocation)
将代码转换为 SSA 形式通常包括以下步骤:
构建控制流图(CFG): 解析原始代码并构建其控制流图。
插入 Φ 函数: 在控制流图中找到所有合并节点,为每个变量插入必要的 Φ 函数。它的作用是解决在多个前驱基本块(basic block)中对同一变量的多个定义情况下,如何选择正确的值赋给该变量的问题。
重命名变量: 遍历控制流图,为每个变量赋予唯一的名称(即使一个变量在不同位置被重新定义也会有不同的名称)。
SSA的优点:
简化数据流分析: 由于每个变量只赋值一次,数据流分析(如活跃变量分析、到达定义分析等)变得更为简单和高效。
便于优化: SSA 形式使得许多编译器优化变得更容易实现,如常量传播、死代码消除、全局值编号等。
消除歧义: 在非 SSA 形式中,不同的变量赋值可能导致歧义。在 SSA 形式中,每个变量都有唯一的定义,消除了这种歧义。
4.1.3 过程间分析
编译器中的过程间分析是为了解决单过程分析和优化中知识的缺失,是由分析和变化的区域中调用位置的存在引起的,主要涉及对程序中跨函数或过程的分析和优化。编译器在过程间分析方面必须解决的第一个问题是调用图的构建。如下图可以根据这个程序构建出这个调用图。
编译器进行过程间分析是为捕获程序中所有过程的行为,并将这些知识施用到各个过程内部的优化中。为进行过程间分析,编译器需要访问程序中的所有代码。典型的过程间问题需要编译器建立一个调用图,如上图所示,并用直接从各个过程推导出的信息来标注调用图,还要围绕该图传播这些信息。
分析过程间信息的结果将被直接应用到过程内分析和优化中。编译器通过过程间分析,可以提高程序的执行效率和性能。过程间分析属于区域优化及全局优化。
流程解释:
预处理和解析阶段:GCC首先进行预处理和解析,构建函数和变量的声明以及控制流图(CFGs)。
构建调用图(Call Graph):在这一阶段,GCC构建程序的调用图,标识函数之间的调用关系。
分析阶段:在分析阶段,GCC增量构建调用图,并且只分析从程序入口点可到达的函数。同时,进行局部分析以驱动过程间优化,并估计函数体大小以备内联使用。
优化阶段:在优化阶段,GCC执行一系列优化操作,包括回收未分析函数和数据结构的内存、发现局部函数、构造内联计划以及移除不可达函数。
扩展阶段:在扩展阶段,GCC按调用图的逆DFS顺序处理函数,应用过程间优化(如内联),并将函数传递给后端进行实际优化和编译。
结束:完成所有过程间分析和优化后,结束IPA流程。
这个流程提供了一个高层次的视图,展示了GCC中过程间分析的实现流程。具体的实现细节可能会因GCC版本和具体优化而异。
GCC中的过程间分析(Inter-procedural Analysis,IPA)优化是编译过程中的一个重要部分,它涉及多种优化技术,主要目的是提高程序的执行效率和减少内存使用。以下是GCC中一些具体的IPA优化方法:
调用图构建(Call Graph Construction):构建程序的调用图,标识函数之间的调用关系。
符号分析(Symbol Analysis):分析程序中的符号,如函数和变量,确定它们的作用域和生命周期。
跨函数优化(Cross-function Optimization):基于调用图和符号分析的结果,执行跨函数的优化,如内联(Inlining)、常量传播(Constant Propagation)、死代码消除(Dead Code Elimination)等。
IPA常量传播(IPA Constant Propagation):发现函数总是以某些已知常量值被调用,并据此修改函数。
IPA内联(IPA Inlining):处理函数内联,使用全程序知识来决定哪些函数可以内联。
IPA纯/常量分析(IPA Pure/Const Analysis):标记函数是否为纯函数或常数函数,基于此信息进行优化。
IPA标量替换聚合(IPA Scalar Replacement of Aggregates):将聚合参数替换为表示原始聚合部分的参数集,将通过引用传递的参数转换为直接传递值。
IPA构造函数/析构函数合并(IPA Constructor/Destructor Merge):合并静态对象的多个构造函数和析构函数为单个函数。
IPA函数摘要(IPA Function Summary):提供函数分析,收集函数体大小、执行时间和帧大小的估计信息。
IPA去虚拟化(IPA Devirtualization):基于类型继承图的分析,将虚拟函数调用转换为直接调用。
IPA相同代码折叠(IPA Identical Code Folding):发现具有相同语义的函数和只读变量,并进行折叠。
IPA类型逃逸分析(IPA Type Escape Analysis):分析变量是否逃逸到函数外部,以优化内存访问。
IPA指针分析(IPA Pointer Analysis):进行指针分析,以优化指针相关的代码。
IPA自动FDO(IPA AutoFDO):使用AutoFDO配置文件数据来指导优化。
IPA树剖面(IPA Tree Profile):为调用图中的所有函数执行剖面引导优化。
这些优化技术共同作用,使得GCC能够在编译时对程序进行全面的分析和优化,提高程序的性能和效率。通过这些优化,GCC能够减少程序的执行时间,降低内存使用,并提高代码的可维护性。
4.2 标量优化
编译器中的标量优化主要关注单个变量(标量值)的操作优化,不涉及向量化或并行处理。这些优化通常在编译器的中端进行,目的是提高程序的执行效率和减少资源消耗。以下是一些常见的标量优化技术:
常量传播(Constant Propagation):将编译时常数传递到程序的各个部分,以减少运行时的计算。
常量折叠(Constant Folding):在编译时计算表达式的值,如果表达式的所有操作数都是常量。
死代码消除(Dead Code Elimination):移除程序中不会被执行或不影响程序输出的代码部分。
死存储消除(Dead Store Elimination):移除对不再使用的变量的写入操作。
公共子表达式消除(Common Subexpression Elimination, CSE):识别并消除重复的计算,将重复的表达式替换为它们的计算结果。
强度削弱(Strength Reduction):将计算成本高的运算转换为成本较低的运算,例如将乘法转换为加法,乘2改为移位运算。
循环不变代码外提(Loop Invariant Code Motion, LICM):将循环体内不依赖于循环变量的计算移到循环体外。
循环展开(Loop Unrolling):增加循环体的大小,减少循环控制的开销。
循环翻转(Loop Reversal):改变循环的迭代方向,有时可以提高缓存的局部性。
循环剥离(Loop Peeling):将循环的前几次迭代分离出来,以处理边界条件或异常情况。
条件分支优化:优化条件分支,减少分支的开销,例如通过分支预测或分支合并。
数组边界检查消除:如果编译器可以证明数组访问不会越界,则移除边界检查。
冗余 load/store 消除:移除不必要的内存加载和存储操作。
寄存器分配优化:优化变量到寄存器的映射,减少内存访问。
指令组合(Instruction Combining):合并多个指令到一个或几个指令中,以减少指令的数量和提高执行效率。
算术优化:优化算术表达式,例如通过使用更高效的算法或避免不必要的精度损失。
函数内联(Function Inlining):将小函数的代码直接插入到调用点,以减少函数调用的开销。
这些标量优化技术可以单独使用,也可以组合使用,以实现更好的优化效果。编译器的优化器会根据程序的特性和目标平台的特点,自动选择最合适的优化策略。如果按照大的分类来说的话,无非就是两种,一类为消除无用和冗余代码,另一类为代码移动和转换。
4.2.1 消除无用和冗余代码
编译器通过多种技术来达到消除无用和冗余代码的目的,以提高程序的执行效率和减少资源消耗。
消除无用代码和去除冗余操作为编译器优化的重要组成部分。无用代码删除(Dead Code Elimination)是指识别并移除程序中永远不会被执行到的代码段,这通常涉及到对程序的控制流图(CFG)进行分析,标记可达和不可达的代码块,并删除那些不可达的代码。冗余操作的消除则涉及识别并优化那些不必要的计算或执行,例如,识别并重组代码以避免重复计算固定值,或者通过合并冗余的分支指令来提高效率。
具体来说,编译器在处理程序时,会进行以下操作:
合并冗余分支指令:通过分析程序的中间表示(IR)和CFG,编译器可以合并那些执行相同操作或产生相同结果的冗余分支指令,从而减少不必要的代码执行。例如,在表达式 a = b * c + d * e; f = b * c; 中,b * c 是重复计算,可以将其存储在临时变量中。
消除不可达代码:编译器通过执行可达性分析来确定哪些代码块是永远不会被执行的,这些代码块被标记为不可达,并在编译过程中被移除。
去除冗余操作:编译器能够识别并优化那些对程序逻辑没有贡献的操作,例如,识别并删除那些对变量没有影响的操作,或者优化循环中不变的重复计算,将其提前计算以减少运行时的计算量。例如,如果存在连续的赋值 a = 1; a = 2;,则第一个赋值可以被移除,因为第二个赋值覆盖了前一个赋值的结果。
这些优化措施共同作用,旨在提高程序的性能和效率,同时减少资源的浪费。
综上所述,编译器通过复杂的分析和转换技术,能够有效地消除无用和冗余的代码,从而提高程序的运行效率和资源利用率。
4.2.2 代码移动和转换
在编译器的中端优化中,代码移动和转换是常见的优化技术。以下是一些具体的例子:
代码移动(Code Motion)
- 循环不变量代码移动(LoopInvariant Code Motion):
循环不变量代码移动是指将循环中每次迭代都执行但与循环变量无关的代码移动到循环外部。例如,如果有一个循环中的表达式x * x,其中x是循环不变量,编译器可以计算一次x * x的结果,并在循环中使用这个结果,从而减少重复计算。 - 代码外提(Loop Hoisting):
代码外提是指将循环体中与循环控制无关的代码提取到循环外部。例如,如果循环中有对全局变量的访问或计算,而这些操作与循环的迭代次数无关,编译器可以将这些操作移到循环外部执行。
代码转换(Code Transformation) - 常量折叠(Constant Folding):
常量折叠是一种在编译时计算常量表达式的优化技术。例如,如果代码中有int x = 3 + 5;,编译器可以优化为int x = 8;,直接将结果8赋值给x。 - 复写传播(Copy Propagation):
复写传播是指在程序中找到可以替换为其对应值的变量或表达式,从而减少不必要的计算和内存访问。例如,如果代码中有int x = 1 + 2; int y = x * x;,编译器可以优化为int y = 9;,直接使用x的值3进行计算。 - 死代码删除(Dead Code Elimination):
死代码删除是指在程序中删除不会被执行的代码,从而减少不必要的计算和内存访问。例如,如果代码中有if (false) { x = 2; },编译器可以删除这个if分支,因为它永远不会被执行。 - 循环展开(Loop Unrolling):
循环展开是指将程序中的循环结构展开为多个循环迭代,从而减少循环的开销和迭代的次数,提高程序的效率。例如,如果代码中有for (int i = 0; i < n; i++) { a[i] += b[i]; },可以被循环展开为for (int i = 0; i < n; i += 4) { a[i+1] += b[i+1]; a[i+2] += b[i+2]; a[i+3] += b[i+3]; },每次迭代对应4个数组元素的计算。
这些优化技术可以显著提高程序的执行效率和性能。编译器通过自动分析程序代码,识别出可以优化的部分,并应用相应的优化策略。
4.3 中端优化部分代码解析
中端优化部分的数据结构,前端从源代码优化到AST/GENERIC,中端从AST/GENERIC到GIMPLE,再从GIMPLE->RTL。后端从RTL转化为汇编的过程。
4.3.1 中端优化的数据结构
为了处理不同的前端语言及其相应的AST/GENERIC,GCC引入了一种与前端语言无关的中间表示GIMPLE。每种语言的前端处理系统都应该将该语言对应的AST_GENERIC转换成GIMPLE,从而便于GCC在GIMPLE的基础上进行统一的中间处理和系统优化。
在从AST向GIMPLE转换的过程中,GIMPLE的生成先后经历了两个阶段,分别称为高级 GIMPLE (High-Level GIMPLE)和低级 GIMPLE(Law-Level GIMPLE)。在执行GIMPLE处理过程pass_lower_cf之前,GIMPLE的形式为高级GIMPLE,高级GIMPLE保留了一些高级控制流结构,执行了该处理过程之后,GIMPLE就被完全转换成低级GIMPLE,低级GIMPLE更接近于目标代码。高级GMIPLE中包含了一些例如GIMPLE_BIND等表示作用域的语句,还有一些例如 GIMPLE_TRY等嵌套的表达式等;低级GIMPLE中就不存在GIMPLE_BIND、GIMPLE TRY这些语句了。详细信息可以参见 GCCinternals。
1.GIMPLE数据结构的存储
GCC使用union gimple_statement_d来表示各种各样的 GIMPLE语句。gimple则是一个指而该联合体union gimple_statemente_d的指针,该联合体的成员变量包括了 struct gimple_statement_base、struct gimple_statement_with_ops、struct gimple_statement_with_memory_ops等二十多种存储结构体。也就是说在GCC中,就是使用这些结构体来表示和存储所有的GIMPLE语句、而且都可以使用 union gimple对其进行统一的描述,首先来看 struct gimple的定义。
struct GTY((desc (“gimple_statement_structure (&%h)”), tag (“GSS_BASE”),
chain_next (“%h.next”), variable_size))
gimple
{
/* [ WORD 1 ]
Main identifying code for a tuple. /
ENUM_BITFIELD(gimple_code) code : 8;
/ Nonzero if a warning should not be emitted on this tuple. /
unsigned int no_warning : 1;
/ Nonzero if this tuple has been visited. Passes are responsible
for clearing this bit before using it. /
unsigned int visited : 1;
/ Nonzero if this tuple represents a non-temporal move. /
unsigned int nontemporal_move : 1;
/ Pass local flags. These flags are free for any pass to use as
they see fit. Passes should not assume that these flags contain
any useful value when the pass starts. Any initial state that
the pass requires should be set on entry to the pass. See
gimple_set_plf and gimple_plf for usage. /
unsigned int plf : 2;
/ Nonzero if this statement has been modified and needs to have its
operands rescanned. */
unsigned modified : 1;
/* Nonzero if this statement contains volatile operands. */
unsigned has_volatile_ops : 1;
/* Padding to get subcode to 16 bit alignment. */
unsigned pad : 1;
/* The SUBCODE field can be used for tuple-specific flags for tuples
that do not require subcodes. Note that SUBCODE should be at
least as wide as tree codes, as several tuples store tree codes
in there. /
unsigned int subcode : 16;
/ UID of this statement. This is used by passes that want to
assign IDs to statements. It must be assigned and used by each
pass. By default it should be assumed to contain garbage. /
unsigned uid;
/ [ WORD 2 ]
Locus information for debug info. /
location_t location;
/ Number of operands in this tuple. /
unsigned num_ops;
/ [ WORD 3 ]
Basic block holding this statement. /
basic_block bb;
/ [ WORD 4-5 ]
Linked lists of gimple statements. The next pointers form
a NULL terminated list, the prev pointers are a cyclic list.
A gimple statement is hence also a double-ended list of
statements, with the pointer itself being the first element,
and the prev pointer being the last. */
gimple *next;
gimple GTY((skip)) prev;
};
该结构体是所有GIMPLE存储结构体的“基类”,描述了GIMPLE语句的基本特性,例如,GIMPLE_CODE、操作数个数、源文件中的位置以及语法块信息等,其中的code 字段描述的是所存储的GIMPLE语句的类型(即GIMPLE_CODE),这些GIMPLE_CODE的值由枚举类型 enum gimple_code 描述;num_ops 字段给出了该 GIMPLE 语句操作数的个数;bb字段给出了该GIMPLE语句所在的基本块(basic block)信息;block 字段则描述了该 GIMPLE语句所在的词法语句块信息。
其他结构体都是在这个结构体基础上进行扩展。不同类型的GMIPLE语句使用对应的结构体进行存储。如下代码所示:gimple_statement_with_ops_base继承自gimple, gimple_statement_with_ops 继承自gimple_statement_with_ops_base。
/ Base structure for tuples with operands. /
/ This gimple subclass has no tag value. /
struct GTY(())
gimple_statement_with_ops_base : public gimple
{
/ [ WORD 1-6 ] : base class /
/ [ WORD 7 ]
SSA operand vectors. NOTE: It should be possible to
amalgamate these vectors with the operand vector OP. However,
the SSA operand vectors are organized differently and contain
more information (like immediate use chaining). */
struct use_optype_d GTY((skip (“”))) use_ops;
};
/ Statements that take register operands. /
struct GTY((tag(“GSS_WITH_OPS”)))
gimple_statement_with_ops : public gimple_statement_with_ops_base
{
/ [ WORD 1-7 ] : base class /
/ [ WORD 8 ]
Operand vector. NOTE! This must always be the last field
of this structure. In particular, this means that this
structure cannot be embedded inside another one. */
tree GTY((length (“%h.num_ops”))) op[1];
};
在gcc/gimple.h中,有如下26种类型,每种类型都分别对应某一种结构体。
(2)GIMPLE处理及其优化
GCC在完成前端的词法/语法分析后,获得了源代码相对应的抽象语法树AST/GENERIC,然后将其转换为对应的GIMPLE序列。随后GCC对GIMPLE间表示形式行了一系列的处理,包括GIMPLE的低级化(lowering)、GIMPLE优化以及RTL(RegisteTansfer Language)生成等。这些处理过程中,尤其是优化处理纷繁复杂,为了便于组织GCC将这些处理划分成一个一个的处理过程,每个处理过程完成一种特定的处理,其输出结果将作为下一个处理过程的输入(有些类似于Unix/inux系统中的管道处理的概念)。
描述Pass的核心数据结构在class opt_pass,继承自gcc/tree-pass.h的struct pass_data。
GCC系统中根据Pass处理的对象及其功能的不同,将pass分成了4大类,分别为GIMPLE_PASS、 RTL_PASS、 SIMPLE_IPA_PASS 及 IPA_PASS。其中GIMPLE_PASS中间表示为GIMPLE处理对象,RTL_PASS的处理对象则是RTL中间表示,SIMPLE_IPA _PASS和IPA_PASS的处理对象也是GIMPLE中间表示,但其功能主要集中在过程间分析的处理上,即函数间的变量传递和调用关系等。
在GCC中,struct pass_data 是一个用于描述编译过程中的一个阶段(通常称为“pass”)的数据结构。每个pass都是编译过程中的一个步骤,它们可以对代码进行分析、转换或优化。struct pass_data 通常包含以下字段:
struct pass_data
{
enum opt_pass_type type;//表示pass的类型,例如GIMPLE_PASS、RTL_PASS、SIMPLE_IPA_PASS或IPA_PASS等
const char *name; //表示pass的名称
optgroup_flags_t optinfo_flags;// 标志位,用于控制优化信息的输出。
timevar_id_t tv_id;//时间变量ID,用于性能分析。
unsigned int properties_required;//执行该pass所需的属性集合。
unsigned int properties_provided;//执行该pass所提供的属性集合
unsigned int properties_destroyed;// 执行该pass所破坏的属性集合
unsigned int todo_flags_start;// 执行该pass之前需要执行的标记
unsigned int todo_flags_finish;// 执行该pass之后需要执行的标记
};
在GCC的源代码中,struct pass_data 用于定义一个pass的元数据,而具体的pass实现通常会继承自opt_pass类,该类继承自pass_data,并添加了执行pass所需的虚拟方法。
GCC中预定义了3个Pass链表,分别为struct opt_pass *all_passes,all_ipa_passes 和all_lowering_passes,其中all_lowering_passes链中的Pass 主要是完成函数的 GIMPLE序列低级化处理,all_ipa_passes主要完成IPA优化,而all_passes主要完成GIMPLE及RTL的各种优化及其相关处理。这3个链表中分别包含了不同数量和内容的Pass,这些Pass是否执行一般由该Pass中的gate()函数来决定,同时也依赖GCC编译时所使用的优化选项,例如“-O1”“-O2”或不使用“-O”选项等。
4.3.2 中端优化的重要函数
在GCC编译器中,中间优化的入口函数是gcc/gimplify.c中gimplify_function_tree。这个函数负责将高级语言的抽象语法树(AST)转换为GIMPLE,GIMPLE是一种中间表示形式,它是一种与前端语言无关的三地址代码表示,适合进行优化处理。
gimplify_function_tree函数首先会检查函数体是否已经存在,如果不存在,则调用gimplify_body函数来处理函数体。gimplify_body函数会进一步调用gimplify_parameters来处理函数参数,以及gimplify_stmt来处理语句。这些函数共同完成了从AST到GIMPLE的转换过程,为后续的优化阶段打下基础。
在GCC中,进入gimplify_function_tree这个入口函数后,有许多其他的优化函数和选项,它们在不同的优化级别下被启用,以提高代码的性能和效率。以下是一些关键的优化选项 gcc –help=optimizers:
- 常量传播和折叠:
fconstprop
:在编译时计算出表达式的常量结果。fdevirtualize
:尝试消除虚函数调用。
- 死码消除:
fdelete-null-pointer-checks
:在已知指针非空的情况下,删除对空指针的检查。flifetime-dse
:消除不再使用的变量和内存分配。
- 循环优化:
floop-optimize
:执行循环优化,如循环展开和循环翻转。floop-interchange
:交换循环的顺序以提高缓存利用率。
- 指令调度和重排:
fschedule-insns
:重新排列指令以提高执行效率。freorder-blocks
:重新排列基本块以提高局部性。
- 内联函数:
finline-functions-called-once
:内联只被调用一次的函数。
- 寄存器分配:
frename-registers
:在寄存器分配后重命名寄存器,以减少虚假依赖。
- 代码生成:
fweb
:建立缓存器网络,提高缓存器使用率。fselective-scheduling
:选择性地对指令进行调度。
- 向量化:
ftree-vectorize
:在trees上执行循环向量化。
- 其他优化:
fstrength-reduce
:通过使用低成本的操作替换高成本的操作来简化代码。fpeephole
:在编译的最后阶段进行小的优化。
这些优化函数和选项在不同的GCC优化级别(如-O1
、-O2
、-O3
)下被启用。例如,在-O3
级别下,GCC会启用更多的优化选项,以牺牲编译时间为代价来提高程序的运行速度。这些优化选项可以通过gcc -Q -O2 --help=optimizers
命令来查看当前优化级别的所有优化开关 。
此外,GCC还提供了一些特定的编译器标志来控制优化行为,例如__attribute__((optimize("O0")))
可以用来指定某个函数使用O0
优化级别,而不管整个程序的优化级别如何。这些选项和函数共同构成了GCC编译器的优化体系,使得开发者可以根据需要选择不同的优化策略。
4.4 中端优化方案
编译器优化对程序性能的影响是显著的。通过编译器优化,可以提高程序的运行速度、减少内存使用、降低能耗,并提升整体性能。
4.4.1 选择一种优化机制(这个地方后半段写的不好呀)
在使用GCC进行性能优化时,可以通过启用和配置各种优化选项来提升编译器生成代码的性能。以下是一些常见的GCC优化选项和策略:
- 优化级别选项:
-O1: 基本优化,主要减少代码大小和执行时间。
-O2: 更高级的优化,启用几乎所有不涉及空间-时间权衡的优化。
-O3: 启用所有-O2优化,并增加一些更激进的优化,如循环展开和函数内联。
-Os: 优化代码大小,启用-O2的大部分优化,但禁用那些会增加代码大小的优化。
-Ofast: 启用所有-O3优化,并增加一些不严格遵循标准的优化。
-Og: 为调试优化,提供合理的优化水平,同时保持良好的调试体验。 - 函数内联:
-finline-functions: 考虑所有函数进行内联,即使它们没有声明为inline。
-finline-small-functions: 内联小函数。
-finline-functions-called-once: 内联只被调用一次的静态函数。
-finline-limit=n: 控制可以内联的函数大小。 - 循环优化:
-funroll-loops: 展开循环。
-fpeel-loops: 剥离循环。
-funswitch-loops: 进行循环切换优化。
-floop-interchange: 交换循环嵌套顺序。 - 数据流优化:
-fmerge-constants: 合并相同的常量。
-fgcse: 全局公共子表达式消除。
-fdelete-null-pointer-checks: 删除无用的空指针检查。 - 寄存器优化:
-fomit-frame-pointer: 在不需要帧指针的函数中省略帧指针。
-fira-algorithm=algorithm: 使用指定的着色算法进行寄存器分配。 - 条件优化:
-fif-conversion: 将条件跳转转换为无分支等价物。
-fif-conversion2: 使用条件执行(如果可用)来转换条件跳转。 - 内存优化:
-fstrict-aliasing: 启用严格的别名分析。
-fstrict-overflow: 启用严格的溢出检查。
-fstack-protector: 启用栈保护。 - 链接时优化(LTO):
-flto: 启用链接时优化,允许跨文件的全局优化。 - 配置特定处理器:
-march=cpu-type: 生成特定处理器的代码。
-mtune=cpu-type: 为特定处理器优化代码。 - 并行编译:
-fopenmp: 启用OpenMP并行编程支持(目前RISC-V没有这个优化选项)。
示例命令:
gcc -O3 -march=native -flto -funroll-loops -fstrict-aliasing -o my_program my_program.c
这个命令启用了高级优化级别-O3,针对本地处理器进行优化,启用了链接时优化,展开循环,并启用了严格的别名分析。
优化器对任何给定代码的有效性都取决于它对该代码应用的优化序列,即所用的特定变换以及应用这些变换的顺序。传统的优化编译器已经向用户提供了几种可供选择的序列(例如,-O0,-O1,-O2,…)。这些序列提供了编译时间和编译器试图执行的优化量之间的权衡。但优化工作增加并不保证真能带来性能提高。
任何给定变换的有效性都取决于几种因素,由此导致了优化序列问题的出现。
(1) 该变换针对的时机在代码中出现了吗,如果没有,那么变换是无法改进代码的。(2) 此前的某个变换是否隐藏或掩盖了当前变换所需的时机,例如,LVN中对代数恒等式的优化可以将2*a转换到一个移位操作,该优化将一个可交换操作替换为一个更快速的非交换操作。此后,任何需要交换性以实现其改进的变换,可能都会因此前应用的LVN而损失某些优化时机。(3)是否有其他变换已经消除了当前变换所针对的低效性?不同变换的效果可能是彼此重叠且各具特异性的,例如,LN实现了全局常量传播的部分效果,而循环展开则实现了类似于超级块复制的效果。编译器编写者可能需要加入两个变换,以实现二者不重叠的那部分效果。变换之间的相互作用使得难于预测应用任何单一变换或变换序列带来的改进。
一些研究编译器试图发现良好的优化序列。其采用的方法在粒度和技术方面各有不同。各种不同的系统可能会寻找程序块层次、源文件层次和全程序层次上的优化序列。这些系统大多数都已经使用了优化序列空间上的某种搜索技术。
潜在优化序列的空间很大。例如,如果编译器从15种变换组成的变换池中选择一个长度为10的变换序列,则可能产生10种可能的序列,编译器很难全方位地探索如此巨大的变换序列空间。因而,编译器在搜索好的变换序列时,将采用启发式技术对搜索空间的较小部分进行抽样。通常,这些技术分为3类: (1)改编遗传算法,使之充当某种形式的智能搜索;(2)随机化搜索算法;(3)统计机器学习技术。所有这三种方法都显现了前景。
尽管受到搜索空间规模巨大的限制,调优良好的搜索算法还是可以通过对搜索空间的100~200次。
探测找到良好的优化序列。虽然这一数字还不实用,但更进一步的改进很有可能将探测的数目降低到实用水准。
在这里,好的优化序列是指生成的结果在最好的5%结果以内的序列。
这些技术的一种应用是推导编译器的命令行标志(如-02)所用的优化序列。编译器编写者可以整体考虑一组颇具代表性的应用程序,从而发现良好的通用优化序列,然后将这些序列用作编译器的默认优化序列。一种更激进的方法(已经用于几种系统)是,推导少量良好的优化序列分别用于不同的应用程序集,在实际编译时,可以让编译器分别尝试这些序列并采用最优结果。
4.4.2 具体的优化方案
要进一步提升GCC的性能,可以通过修改GCC的中间优化阶段代码来实现。以下是一些具体的优化方案和步骤: - 向量化优化
向量化可以显著提高循环的执行效率。可以通过修改GCC的向量化模块来增强向量化能力。
修改步骤:
(1)定位向量化代码:GCC的向量化代码主要在gcc/tree-vect-loop.c和gcc/tree-vect-stmts.c中。
(2)增强向量化能力:对speccpu2006代码中的更多的数据操作或者更复杂的循环操作进行分析,不断探索新的向量化场景,并通过算法增强来提升向量化的效果,对复杂的控制流(如分支、跳转)和函数调用进行优化,增加更多的向量化模式,优化向量化决策逻辑。
// 示例:增强向量化决策逻辑
if (is_simple_loop(loop) && supports_vectorization(loop)) {
vectorize_loop(loop);
} - 循环展开和剥离
循环展开和剥离可以减少循环控制开销,提高指令级并行性。
在编译器优化中,循环展开(Loop Unrolling)和循环剥离(Loop Peeling)是两种常用的技术,用于提高程序的执行效率。下面分别解释这两种技术:
循环展开(Loop Unrolling)
循环展开是一种减少循环迭代次数和减少循环控制开销的优化技术。它通过复制循环体(Loop Body)的代码来减少循环迭代的次数。这样做的目的是减少循环的迭代次数,从而减少循环控制的开销,如比较和跳转指令。
例如,考虑以下循环:
for (int i = 0; i < n; i++) {
A[i] = B[i] + C[i];
}
循环展开后,如果展开因子为2,代码可能变为:
for (int i = 0; i < n; i += 2) {
A[i] = B[i] + C[i];
A[i + 1] = B[i + 1] + C[i + 1];
}
这样,每次循环迭代都会处理两个元素,减少了循环的迭代次数,从而减少了循环控制的开销。
循环剥离(Loop Peeling)
循环剥离是另一种循环优化技术,它通过将循环的前几次迭代(或最后一次迭代)从主循环中分离出来,以减少循环体内的条件判断,从而提高执行效率。
例如,考虑以下循环:
for (int i = 0; i < n; i++) {
if (i == 0) {
// 仅在第一次迭代时执行的操作 }
A[i] = B[i] + C[i];
}
在这种情况下,循环剥离可以将第一次迭代从主循环中分离出来:
// 剥离出来的第一次迭代
if (n > 0) {
A[0] = B[0] + C[0];
}
// 主循环
for (int i = 1; i < n; i++) {
A[i] = B[i] + C[i];
}
这样做的目的是减少循环体内的条件判断,因为剥离出来的代码只执行一次,而主循环不再需要这个条件判断。
循环展开和循环剥离都是编译器优化技术,用于提高程序的执行效率。循环展开通过减少循环迭代次数来减少循环控制开销,而循环剥离通过分离出循环的前几次迭代来减少循环体内的条件判断。这两种技术可以单独使用,也可以结合使用,以实现更好的优化效果。
修改步骤:
(1)定位循环优化代码:GCC的循环优化代码主要在gcc/tree-ssa-loop-ivcanon.c和gcc/tree-ssa-loop-manip.c中。
(2)增加展开和剥离逻辑:可以增加更多的循环展开和剥离模式,优化展开和剥离决策逻辑。
// 示例:增加循环展开逻辑
if (should_unroll_loop(loop)) {
unroll_loop(loop);
} - 数据流分析
数据流分析是很多优化的基础,可以通过增强数据流分析能力来提高整体优化效果。在GCC编译器中进行数据流分析的优化,可以通过增加更多的数据流分析模式和优化数据流分析算法来实现。以下是一些方法和策略:
(1)增加数据流分析模式:
静态单赋值(SSA):SSA是一种数据流分析算法,它将程序转换为每个变量只被赋值一次的形式。这有助于进行更有效的数据流分析和优化,如常量传播、复写传播、死代码消除等。
基于DAG的优化算法:通过构建表达式的有向无环图(DAG),可以共享重复的表达式,减少程序中的重复计算。
基于模板匹配的优化算法:这种算法通过匹配特定的代码模板来识别优化机会,如循环展开、强度削弱等。
(2)优化数据流分析算法:
迭代方法:使用迭代方法来求解数据流方程,直到达到不动点。这种方法可以逐步逼近想要的in和out的值。
超优化+泛化:结合超优化技术和泛化技术,可以自动改进编译器中的窥孔优化。超优化使用昂贵的搜索技术寻找编译器未发现的优化,而泛化则将特定优化变成广泛可用的。
(3)利用数据流分析结果:
强度削弱:将循环中的高成本操作(如乘法)替换为低成本操作(如加法),特别适用于归纳变量。
归纳变量删除:在循环中,如果存在多个步伐一致的归纳变量,可以尝试只保留一个,删除其余的,以简化循环结构和减少计算量。
(4)自动化优化流程:
自动化地利用数据流事实:使用超优化器,如Souper,利用LLVM数据流分析结果进行优化。这样可以复用数据流分析的形式化,并通过泛化引擎支持数据流分析。
(5)实现新的数据流分析:
整数范围分析:证明变量的值总是在某个范围内,从而避免不必要的边界检查。
已知位分析:证明所有执行中SSA值的单个位是0或1,这对于优化位操作密集的代码非常有用。
(6)控制流约束:
到达-定值分析:确定程序中变量的定值在程序的哪些点上是可用的。这种分析对于优化编译器的决策过程至关重要,比如在进行常量合并或者识别未初始化的变量时。
通过这些方法,GCC编译器可以更有效地进行数据流分析,从而实现更高级的代码优化。
修改步骤:
(1)定位数据流分析代码:GCC的数据流分析代码主要在gcc/tree-ssa-dce.c和gcc/tree-ssa-pre.c中。
(2)增强数据流分析能力:可以增加更多的数据流分析模式,优化数据流分析算法。
// 示例:增强数据流分析算法
if (is_redundant_code(stmt)) {
remove_redundant_code(stmt);
} - 链接时优化(LTO)(目前riscv没有这个选项)
LTO可以在链接阶段进行全局优化,进一步提高程序性能。LTO 的基本原理是将各个编译单元(通常是各个源文件)在编译过程中生成的中间表示(如GIMPLE)保留到链接阶段,然后在链接阶段对整个程序进行全局优化,持续对 LTO 进行改进,提升优化的深度和广度,增强编译速度和内存效率。
修改步骤:
(1)定位LTO代码:GCC的LTO代码主要在gcc/lto目录下。
(2)增强LTO能力:可以增加更多的全局优化模式,优化LTO决策逻辑。
// 示例:增强LTO决策逻辑
if (is_eligible_for_lto(file)) {
perform_lto(file);
} - 内联优化
内联可以减少函数调用开销,提高程序性能。
修改步骤:
定位内联代码:GCC的内联代码主要在gcc/tree-inline.c中。
增强内联能力:可以增加更多的内联模式,优化内联决策逻辑。
// 示例:增强内联决策逻辑
if (is_small_function(func) && should_inline(func)) {
inline_function(func);
}
通过上述修改,可以显著提升GCC的性能。需要注意的是,每次修改后都需要进行充分的测试,以确保修改不会引入新的问题。其实就是识别更多的可优化的模式来进行优化。