编译器优化--2--优化范围简介

编译器优化–2--优化范围简介

概述

代码优化的目标是在编译时发现有关程序运行时行为的信息,并利用该信息来改进编译器生成代码。在实现变换之前,编译器编写者必须理解合适可以安全地应用各种变换,以及何时能够预期在应用变换后获利

Note:

安全性:如果一个变换不会改变程序的运行结果,那么该变换就是安全的。

获利性:当在某个位置上应用一种变换可以带来实际的改进时,我们就说这种变化是有利可图的。

代码的低效性主要来源是对源语言抽象的实现。因为从源代码到IR的转换是一个局部过程,进行该转换时未能对外围上下文进行广泛分析,该转换生成的IR通常是为了处理各种源语言结构的最一般情形。在具有上下文知识的情况下,优化器通常可以判断代码是否需要这种完全的一般性。如果不需要,优化器可以用更受限,更高效的方式来重写代码。

优化机会的另一个重要来源在于目标机。编译器必须详细了解目标机影响性能的那些属性。诸如功能单元的数目和能力、内存层次结构中各个层次的延迟和宽带、指令集支持的各种寻址方式、罕见或复杂操作的可用性等问题,都会影响到编译器应该为某个给定应用程序生成代码的种类。

过去,大多数优化编译器都专注于提高编译后代码的运行时速度。但是,代码的改进也可以有其他形式,如生成占用更少空间的代码。其他情况下,用户可能想要针对指定的条件进行优化,如优化寄存器使用、内存使用、能耗或对实时事件的响应。

编译优化初窥和实践

循环的强度削减(Strength Reduction)的优化

优化前的IR:

; RUN: opt -mtriple=x86_64-unknown-linux-gnu -loop-reduce -lsr-insns-cost=false -S < %s | FileCheck %s
; Check LSR formula canonicalization will put loop invariant regs before
; induction variable of current loop, so exprs involving loop invariant regs
; can be promoted outside of current loop.

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

define void @foo(i32 %size, i32 %nsteps, i8* nocapture %maxarray, i8* nocapture readnone %buffer, i32 %init) local_unnamed_addr #0 {
entry:
  %cmp25 = icmp sgt i32 %nsteps, 0
  br i1 %cmp25, label %for.cond1.preheader.lr.ph, label %for.end12

for.cond1.preheader.lr.ph:                        ; preds = %entry
  %cmp223 = icmp sgt i32 %size, 1
  %t0 = sext i32 %init to i64
  %wide.trip.count = zext i32 %size to i64
  %wide.trip.count31 = zext i32 %nsteps to i64
  br label %for.cond1.preheader

for.cond1.preheader:                              ; preds = %for.inc10, %for.cond1.preheader.lr.ph
  %indvars.iv28 = phi i64 [ 0, %for.cond1.preheader.lr.ph ], [ %indvars.iv.next29, %for.inc10 ]
  br i1 %cmp223, label %for.body3.lr.ph, label %for.inc10

for.body3.lr.ph:                                  ; preds = %for.cond1.preheader
  %t1 = add nsw i64 %indvars.iv28, %t0
  %t2 = trunc i64 %indvars.iv28 to i8
  br label %for.body3

; Make sure loop invariant items are grouped together so that load address can
; be represented in one getelementptr.
; CHECK-LABEL: for.body3:
; CHECK-NEXT: [[LSR:%[^,]+]] = phi i64 [ 1, %for.body3.lr.ph ], [ {{.*}}, %for.body3 ]
; CHECK-NOT: = phi i64
; CHECK-NEXT: [[LOADADDR:%[^,]+]] = getelementptr i8, i8* {{.*}}, i64 [[LSR]]
; CHECK-NEXT: = load i8, i8* [[LOADADDR]], align 1
; CHECK: br i1 %exitcond, label %for.inc10.loopexit, label %for.body3

for.body3:                                        ; preds = %for.body3, %for.body3.lr.ph
  %indvars.iv = phi i64 [ 1, %for.body3.lr.ph ], [ %indvars.iv.next, %for.body3 ]
  %t5 = trunc i64 %indvars.iv to i8
  %t3 = add nsw i64 %t1, %indvars.iv
  %arrayidx = getelementptr inbounds i8, i8* %maxarray, i64 %t3
  %t4 = load i8, i8* %arrayidx, align 1
  %add5 = add i8 %t4, %t5
  %add6 = add i8 %add5, %t2
  %arrayidx9 = getelementptr inbounds i8, i8* %maxarray, i64 %indvars.iv
  store i8 %add6, i8* %arrayidx9, align 1
  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
  %exitcond = icmp eq i64 %indvars.iv.next, %wide.trip.count
  br i1 %exitcond, label %for.inc10.loopexit, label %for.body3

for.inc10.loopexit:                               ; preds = %for.body3
  br label %for.inc10

for.inc10:                                        ; preds = %for.inc10.loopexit, %for.cond1.preheader
  %indvars.iv.next29 = add nuw nsw i64 %indvars.iv28, 1
  %exitcond32 = icmp eq i64 %indvars.iv.next29, %wide.trip.count31
  br i1 %exitcond32, label %for.end12.loopexit, label %for.cond1.preheader

for.end12.loopexit:                               ; preds = %for.inc10
  br label %for.end12

for.end12:                                        ; preds = %for.end12.loopexit, %entry
  ret void
}

优化后的IR:

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

define void @foo(i32 %size, i32 %nsteps, i8* nocapture %maxarray, i8* nocapture readnone %buffer, i32 %init) local_unnamed_addr {
entry:
  %cmp25 = icmp sgt i32 %nsteps, 0
  br i1 %cmp25, label %for.cond1.preheader.lr.ph, label %for.end12

for.cond1.preheader.lr.ph:                        ; preds = %entry
  %cmp223 = icmp sgt i32 %size, 1
  %t0 = sext i32 %init to i64
  %wide.trip.count = zext i32 %size to i64
  %wide.trip.count31 = zext i32 %nsteps to i64
  br label %for.cond1.preheader

for.cond1.preheader:                              ; preds = %for.inc10, %for.cond1.preheader.lr.ph
  %lsr.iv = phi i64 [ %lsr.iv.next, %for.inc10 ], [ %t0, %for.cond1.preheader.lr.ph ]
  %indvars.iv28 = phi i64 [ 0, %for.cond1.preheader.lr.ph ], [ %indvars.iv.next29, %for.inc10 ]
  br i1 %cmp223, label %for.body3.lr.ph, label %for.inc10

for.body3.lr.ph:                                  ; preds = %for.cond1.preheader
  %scevgep1 = getelementptr i8, i8* %maxarray, i64 %lsr.iv
  br label %for.body3

for.body3:                                        ; preds = %for.body3, %for.body3.lr.ph
  %indvars.iv = phi i64 [ 1, %for.body3.lr.ph ], [ %indvars.iv.next, %for.body3 ]
  %scevgep2 = getelementptr i8, i8* %scevgep1, i64 %indvars.iv
  %t4 = load i8, i8* %scevgep2, align 1
  %0 = zext i8 %t4 to i64
  %1 = add i64 %indvars.iv28, %indvars.iv
  %2 = add i64 %1, %0
  %scevgep = getelementptr i8, i8* %maxarray, i64 %indvars.iv
  %tmp = trunc i64 %2 to i8
  store i8 %tmp, i8* %scevgep, align 1
  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
  %exitcond = icmp eq i64 %indvars.iv.next, %wide.trip.count
  br i1 %exitcond, label %for.inc10.loopexit, label %for.body3

for.inc10.loopexit:                               ; preds = %for.body3
  br label %for.inc10

for.inc10:                                        ; preds = %for.inc10.loopexit, %for.cond1.preheader
  %indvars.iv.next29 = add nuw nsw i64 %indvars.iv28, 1
  %lsr.iv.next = add i64 %lsr.iv, 1
  %exitcond32 = icmp eq i64 %indvars.iv.next29, %wide.trip.count31
  br i1 %exitcond32, label %for.end12.loopexit, label %for.cond1.preheader

for.end12.loopexit:                               ; preds = %for.inc10
  br label %for.end12

for.end12:                                        ; preds = %for.end12.loopexit, %entry
  ret void
}

优化前的CFG:

优化后的CFG:

我手动将前面的LLVM IR转为等价的源码,可能对于大家而言,结合CFG对比源码要看得更加清晰一点。

左图为优化前的源码,右图为优化后的源码。

从对比图中可以看出:

  1. 左侧的t5的计算已经提升到右侧由循环外侧的归纳变量(Induction Variable)lsr_iv来控制;
  2. 左侧内嵌套循环中对指针maxarray的操作,在右侧已经将公共子表达式提到内循环中;

以上是LLVM工程中测试循环强度削减的一个经典例子,所在文件路径为:llvm/test/Transforms/LoopStrengthReduce/X86/canonical.ll。

至正如我们所看到的优化后的代码,与优化前的对比,并没有产生多大的优化效果,或根本没有效果。于循环强度削减的优化为什么会产生这样的结果?我目前的***猜测***是:

  1. 这样的优化可能为后面的其他优化创造更多的优化机会;
  2. 我们看优化不能之对比源码,更多应该对比LLVM IR指令,看看操作指令的数量有没有减少,看看优化后的长延时操作指令,是否有更大的指令调度空间,以便CPU发挥除高的性能。
*Note:关于编译器优化方面,本人也是才疏学浅。如果你有更多编译器优化方面的见解和问题,欢迎与我探讨。*

优化的考虑

优化的核心之处都在于两个问题,即安全性和可获利性。编译器必须有一种机制来证明其应用的每个变换都是安全的,也就是说变换将保持程序原有的语义。编译器还要有理由确信应用某种变换是有利可图的(即变换会提高程序的性能)。如果上面两个条件不能同事满足,则不应该应用这种变换。

  1. 安全性

    ​ 程序员如何知道某种变换是安全的?换言之,程序员凭什么相信变换后的代码可以产生相同的语义?语义(meaning)通常定义为程序的可观察行为。对于批处理程序,即程序停止后的内存状态及产生的输出是相同的。程序停止的前一时刻,所有可见变量的值都应该是相同的。对于交互程序,其行为更加复杂,也更难以描述。

    此外,编译器还会慎重处理,避免发生脱节的情况。如原来的代码工作正确,而优化后的代码试图除以0或无限循环。

  2. 可获利性

    循环代码次数减少

    计算操作变少

    占用内存变少

    内存操作变少

    等等。。。

  3. 风险

    优化变换中引出的关于寄存器,内存地址计算等风险

优化的时机

一般来说,可供优化编译器利用的时机有几种不同的来源。

  1. 减少抽象的开销

    ​ 高级程序设计语言引入的数据结构和类型需要运行时支持。优化器可以通过分析和变换来减少这种开销。

  2. 利用特例

    ​ 通常,编译器可以利用操作执行时所处上下文的相关知识,来特化该操作。举例来说,一个C++编译器有事能够确定,对某个虚函数的调用总是使用同一个实现。在这种情况下,它可以重新映射该调用,减少每次调用的代价。

  3. 将代码匹配到系统资源

    ​ 如果程序的资源需求与处理器的能力不符,则编译器可能需要变换改程序,使其需求更加切合可用的资源。对某些科学计算库的变换就具有这种效果,变换成功地减少了每次浮点操作访问内存的次数。

优化的范围

优化可以再不同粒度或范围上运作。不同的粒度向优化器提供了不同的优化时机。一般来说,变换和支持变换的分析作用于四中不同的范围之一:局部的、区域性的,全局的或整个程序。

  1. 局部方法

    ​ 局部方法作用与单个基本程序块:最长的一个无分支代码序列。在基本程序块内,有两个重要的性质:第一,语句是顺序执行的。第二,如果一条语句执行,那么整个程序块必定也执行,除非发生异常。与更大的代码范围相比,这两个性质使得编译器能够利用相对简单的分析来证明更强的事实。因而,局部方法有时能够作出在更大范围上无法达到的改进。但是,局部方法只能改进在同一个基本程序块中的各个操作。

    ​ 基本程序块的执行模型简单,使得可以进行相当精确的优化分析。局部优化的方法很多,在这里仅简介两种:一个是值编号(value numbering),用于查找基本程序块中的冗余表达式,通过重用此前计算过的值来替换冗余的求值。另一个是树高平衡(tree-height balancing),用于重新组织表达式树,以揭示更多指令层次的并行性。

  2. 区域性方法

    ​ 区域性方法的作用范围大于单个基本程序块,而小于完整的过程。在下面的控制流图(CFG)中,

    ​ 编译器将整个循环 { B 0 , B 1 , B 2 , B 3 , B 4 , B 5 , B 6 } \{B_0, B_1, B_2, B_3, B_4, B_5, B_6\} {B0,B1,B2,B3,B4,B5,B6}作为一个区域考虑。有时候,在考虑整个过程相比,考虑完整过程代码的一个子集,能够进行更敏锐地分析并得到更好的变换效果。例如,在循环嵌套内部,编译器也许能证明一个大量使用的指针是不变量,尽管该指针可能在过程中其他地方修改。这样的知识能够用来进行一些优化,比如将该指针引用的值保持在寄存器中,等等。

    ​ 编译器可以用许多不同的方式来选择需要优化的区域。区域可以用某种源代码控制结构(如循环嵌套)定义。编译器可以考餐区域中形成扩展基本程序块(Extended Basic Block,EBB)的基本程序块集合。例子CFG包含3个EBB: { B 0 , B 1 , B 2 , B 3 , B 4 } \{B_0, B_1, B_2, B_3, B_4\} {B0,B1,B2,B3,B4} { B 5 } \{B_5\} {B5} { B 6 } \{B_6\} {B6}。虽然两个单程序块的EBB相对于纯碎的局部试图没有什么优势,但较大的那个EBB是可以提供区域性优化的时机的。最后,编译器可以考虑通过某种图论性质定义CFG子集,如CFG中支配关系强连通分量

    Note:

    1. 扩展基本程序块(EBB)一组基本程序块 β 1 , β 2 , β 3 , . . . β n \beta_1, \beta_2, \beta_3,...\beta_n β1,β2,β3,...βn,其中 β 1 \beta_1 β1具有多个CFG前驱节点,而其它每个 β i \beta_i βi都只有一个CFG前驱节点。
    2. 支配关系,在CFG图中,当且仅当从根节点到 y y y的每条路径都包含节点 x x x时,则 x x x支配 y y y

    ​ 区域性方法有几个强大之处。将变换的范围限制到小鱼整个过程的一个区域上,使得编译器将工作重点集中在频繁执行的区域上,例如,与围绕循环的代码相比,循环体的执行要频繁得多。编译器可以对不同的区域应用不同的优化策略。最后,对代码中有限区域的关注,通常是编译器可以推导出有关程序行为的更准确信息,而这又进一步暴露了改进和优化的时机。

    ​ 从局部优化到区域性优化,主要的复杂之处在于需要处理控制流的多种可能性。if-then-else语句有两种路径选择。循环末尾的分支可以跳转回循环起始处开始另一个迭代,也可以跳转到循环之后的代码。区域性优化技术有很多种,这里简介两种:一种是超局部值编号技术,是将局部值编号算法向更大的区域扩展。第二种是循环展开,它是最古老和最著名的循环变换,它对编译器生代码有着直接和间接的影响,循环的最终新能取决于所有直接和间接的影响。

  3. 全局方法

    ​ 这种方法也称为过程内方法,它使用整个过程作为上下文。全局方法的动机很简单:局部最优的决策,在更大的上下文中可能带来坏的结果。对于分析和变换来说,过程为编译器提供了一个自然的边界。过程是一种抽象,分装和隔离了运行时环境。同时,过程在许多系统中也充当了分离编译的单位。

    ​ 全局方法通常的工作方法是:建立过程的表示(如CFG),分析该表示,然后变换底层的代码。如果CFG有环,则编译器必须受限分析整个过程,然后才能确定在特定基本程序块的入口上那些事实是成立的。因而,大多数全局变换的分析阶段和变换阶段是分离的。分析阶段收集事实并对其进行推断。变换阶段使用这些事实来确定具体变换的安全性和可获利性。借助与全局视图,这些方法可以发现局部方法和区域性方法都无法发现的优化时机。

  4. 过程间方法

    ​ 这些方法也称为全程序方法,考虑的范围大于单个过程。任何涉及多于一个过程的变换,都认为是过程间变换。正如从局部范围移动到全局范围会揭示新的优化时机一样,从单个过程转移到多个过程也能够暴露新的优化时机。它也提出了新的挑战。例如:参数结合规则使得用于支持优化分析大大复杂化。

    ​ 至少在概念上,过程间肥西和优化作用于程序的调用图。有时候,这些技术会分析整个程序;在其他情况下编译器可以只考察源代码的一个子集。过程间优化的两个经典例子是内联替换(inline substitution)过程间常数传递(interprocedural constant propagation),前者将过程调用原地替换为被调用者过程体的一个副本,后者在整个程序中传播并合并有关常数的信息。

编译器在各种范围内进行分析和变换,从单个基本程序块(局部方法)到整个程序(过程间方法)。一般来说,随着优化范围加大,优化时机也会增多。但分析较大范围所得的有关代码行为的知识通常不是那么精确。因而,在优化的范围和生成代码的质量之间,并不存在一个简单的关系。如果在一般情况下,较大的优化范围能够带来较好的代码质量,虽然这个令人愉悦,但遗憾的是,这种关系并不成立。

参考:

《Engineering a Compiler》

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: GCC是GNU Compiler Collection的缩写,是一套开源的编程语言编译器。4.4.7-23是GCC的一个版本号。 GCC 4.4.7-23是GCC的第4.4.7版本的第23个更新版本。这个版本的GCC是在2017年10月发布的,是GCC 4.4系列的最终更新版本。GCC 4.4系列主要支持C和C++两种语言的编译。 GCC作为一款优秀的编译器,具有很多特性和功能。它提供了广泛的语言支持,包括C、C++、Fortran、Java、Objective-C、Ada和Go等,能够用于不同类型的程序开发。GCC还支持多种操作系统,包括Linux、Windows、Mac OS等。 GCC的主要功能是将源代码编译成可执行文件或库文件。它可以进行语法分析、语义分析、优化和代码生成等步骤,将高级语言源代码转化为特定目标平台上的可执行文件。GCC在编译过程中还提供了丰富的选项和参数,可以控制编译过程的行为,例如优化等级、目标平台的选择等。 GCC还提供了一些工具和库,用于程序开发和调试。例如,GCC提供了GDB调试器,可以帮助开发人员在调试时定位问题。此外,GCC还提供了一些用于优化和分析的工具,例如性能分析器和代码规范检查工具。 总之,GCC 4.4.7-23是GCC编译器的一个具体版本,具有广泛的语言支持和功能,可以帮助开发人员进行程序的编译、调试和优化。 ### 回答2: GCC是GNU编译器套件的缩写,是一种开源的编译器工具。gcc 4.4.7-23是指版本号为4.4.7,补丁级别为23的gcc编译器。 gcc 4.4.7-23是较旧的gcc版本,于2011年发布。它是gcc 4系列的一个分支,该系列是gcc非常稳定和广泛使用的版本之一。在4.4.7-23版本中,包含了一些已知的错误修复和改进,以提高编译器的稳定性和性能。 由于gcc是一个开源项目,它可以在多个操作系统上运行,如Linux、Windows和macOS等。gcc 4.4.7-23版本支持许多不同的编程语言,包括C、C++、Objective-C、Fortran、Java和Ada等。 gcc编译器具有强大的优化功能,可以将源代码编译为目标代码,以便在不同的架构上运行。它能够执行诸如语法检查、代码优化和生成可执行文件等任务。 然而,由于4.4.7-23版本相对较旧,可能不支持最新的语言特性和标准,也可能存在一些已知的问题。因此,在选择使用gcc版本时,需要根据具体的需求和项目要求进行评估。 总之,gcc 4.4.7-23是一个较旧但稳定的gcc版本,适用于旧的项目或对最新特性要求不高的项目。在使用时,需要根据项目需求选择合适的gcc版本。 ### 回答3: gcc 4.4.7-23是一个编译器的版本号。gcc代表GNU编译器集合,是一个广泛使用的开源编译器套件。4.4.7是编译器的主要版本号,表示该版本的gcc是从4.4.x系列发展而来的。每个主要版本可能包括多个次要版本和修订版本,这里的23即表示该gcc版本的修订版本。修订版本意味着在之前的版本基础上进行了一些改进或修复错误。 gcc 4.4.7-23是一个比较老的gcc版本,适用于早期的编程需求。由于gcc是开源的,它的版本号反映了它的开发历史和功能特性。较新的gcc版本通常包含更多的优化和功能改进,因此更适用于现代编程需求。 每个gcc版本都有其特定的用途和适应范围。gcc 4.4.7-23可能不支持一些新的语言特性或编程库,也可能存在已知的安全漏洞。因此,在选择gcc版本时,需要根据实际需求和项目要求进行评估和选择合适的版本。 总之,gcc 4.4.7-23是一个早期的gcc版本,用于编译代码并生成可执行文件。它的功能可能相对较旧,但对于一些特定的项目需求仍然有其存在和应用的价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值