GCC优化浅析

一、前言

GCC(GNU Compiler Collection)是一个免费的编译软件,能够支持Ada、C、C++、Fortran、Java和Objective-C多种语言,并且适用于多个平台包括x86、ARM、MIPS、PowerPC等等。GCC的功能是相当强大的,本文主要集中在GCC的代码优化方面进行探讨,努力将优化算法和具体实现讲述清楚完全。本文规划如下:第二部分是GCC的简单介绍,包括一些简要历史和体系结构;第三部分是本文的重点,即代码优化;第四部分是结论,主要是对工作进行总结和一些感悟。

 

二、GCC介绍

2.1 GCC历史

GCC的官方主页是http://gcc.gnu.org,在该网站上你可以找到GCC的相关信息和各个版本源代码下载。GCC3.0被认为是现代版本,由于包括了C++编译器。目前可用的最新版本是GCC 4.6.1,而本文的代码示例也是采用的GCC 4.6.1。GCC项目组正在进行4.7.0的开发工作,如果感兴趣可以在官网上看到相关信息。

2.2 GCC体系结构

要想探讨GCC的代码优化,就不得不介绍GCC的体系结构,可以说正因为GCC有了这样的体系结构,所以它才采用了相应的优化算法。本文先介绍GCC的体系结构,能够先从总体上把握GCC的实现策略,从而进行代码优化方面的探讨。

上图是GCC体系结构的简单示意图。GCC为了实现其多语言、多应用平台支持分为三部分:前端(Front End)、中端(Middle End)和后端(Back End)。前端:实现了GCC的多语言支持,将其所支持的语言先翻译成抽象语法树(AST),通过AST生成GENERIC表达,这样就使得各种语言形成统一的表示,这样的策略是有其优点的,在后面会介绍到;中端:GENERIC比较复杂,所以GCC将GENERIC转变为GIMPLE,由GIMPLE转变为SSA(static single assignment),如图所示,这时就要结果多遍优化(opt pass N),形成优化后的un-SSA,实际上GCC优化不仅仅在生成un-SSA,在中间的一些过程中可能就涉及到优化,最后生成RTL(register transfer language);后端:为了实现多平台支持,GCC通过中端生成的RTL根据不同的平台生成相应的机器码。这样GCC就实现了编译,这个过程看起来很简单,但是实际上是相当复杂的,特别是在某些优化,GCC要遍历二三十遍。实际上该图没有完整的表达GCC的体系结构,例如对于Fortran来说,不会生成GENERIC而是一种相应的特殊表达。当然,这不影响总体。

我们将上面尚未了解的名词放一放,先介绍下GCC的目录结构。GCC顶级源目录主要结构如下:

boehm-gc

Java运行时支持

config

配置文件

contrib

别人贡献的代码

fixincludes

修正的头文件

gcc

GCC本身,包括前端、中端、后端和优化

gnattools

Ada工具

include

头文件

还有一些C、C++等运行时支持。本文主要涉及的是gcc目录,该目录下文件结构如下:

ada

Ada语言前端

config

平台相关的配置信息

cp

C++语言前端

doc

Gcc文档

fortran

Fortran语言前端

java

Java语言前端

ginclude

头文件

objc

Objective-C 语言前端

objcp

Objective-C++语言前端

po

翻译,多语言信息

testsuite

测试包

       阅读代码时,根据相关的目录结构,会更见便捷。下面对gcc重要的结构进行介绍。

   GCC预处理后,为源文件的每个函数生成抽象语法树(AST),每一个AST是一定数量的树结构的链接节点,这与编译原理中所说的经过词法分析和语法分析生成的相似。

   GENERIC是一种通用表示,由AST得到,引入GENERIC的原因是GCC要加入SSA支持,但是SSA没有增加到RTL上,而是想增加在Tree上,然而GCC不同的前端使用不同的Tree,如果直接给各个语言前端增加SSA支持的话就太繁琐。所以GCC引入了GENERIC Tree。

GIMPLE将GENERIC转化为三地址的形式(即操作数个数小于3),这是因为优化经过简化的GIMPLE上更为容易。GENERIC到GIMPLE的lower主要是:(1)指令都被转化为三地址的形式;(2)所有变量使用同一个命名空间;(3)控制流降低,指令序列+jump,指令序列仍然被组织成Tree。

SSA全称Static Single Assignment,翻译过来就是静态单一赋值。Gcc的优化从这里开始,gcc要在SSA树上进行多于20遍的不同优化。在经过SSA优化后,树被转变回GIMPLE,该GIMPLE用于生成register-transfer(RTL)。RTL是基于带有无限数量的寄存器的抽象目标机的硬件语言。

RTL例子:

 (set (reg/v:SI 59 [ b ])

(plus:SI (reg/v:SI 60 [ a ]

(const_int -1 [0xffffffff]))))

三、代码优化

至此,可以进入核心部分,GCC如何实现代码优化的。总体上来讲,GCC的优化大致分为两部分:(1)作用于GIMPLE/Tree-SSA上的全局优化,大约100遍;(2)作用于RTL上的标量优化,大约70遍。GCC从其诞生至今,历经了16年,实现了大量的只在文献中的优化算法。由于GCC实现的优化算法是如此之多,本文仅仅介绍其中的几个。

3.1自动向量化(auto vectorization)

3.1.1 原理:

很多早期的超级计算机使用向量指令,向量运算以流水线化的方式执行,该向量的元素被串行获取,对不同元素的计算相互重叠。在先进的向量计算机中,向量运算可以链接起来:当生成结果向量的元素时,它们立刻被另一个向量指令的运算消耗掉,不需要等待所有的结果都计算完成。另一类指令级并行是SIMD,SIMD指令制定了对连续内存位置执行的相同运算。这些指令从内存中并行加载数据,把它们存放在宽寄存器中,并使用并行硬件来计算它们。很多媒体、图形和数字信号处理应用可以利用这些运算。

GCC将串行指令变成向量指令的依据如下例:

Example1:

int a[N], b[N], c[N];

foo () {

  int i;

 

  for (i=0; i<N; i++){

    a[i] = b[i] + c[i];

  }

}

Example1中a[i]=b[i]+c[i],数组a、b和c没有依赖关系,完全可以实现向量化,循环如下:

   for (i=0; i<N; i+=VF) {

      a[i:i+VF] = b[i:i+VF] + c[i:i+VF];

   }

其中VF是向量指令的操作数个数,经过这样的优化在向量处理机上,运行效率得到了VF倍的提升。

GCC中得向量自动化是在循环优化实现的,这是可以想象的,根据上面的示例可知,循环中存在着向量化的潜力。然而,要使得指令有标量转变为向量是极为不容易的,这需要很多相关性分析,才能找到正确的向量化。

3.1.2 现状:

在主要的GCC开发trunk中,有一个开发分支(autovect-branch)。该分支就是GCC中实现自动向量化的工程,主要的开发者为:Dorit Naishlos、Olga Golovanevsky、Ira Rosen等。然而,该分支的最近一次更新在2009-12-03,说明对于自动向量化已经很完善了,并且该分支似乎已经停止了。

3.1.3 代码分析:

这里将会探讨GCC如何实现自动向量化。该优化实现在tree-vectorizer.c、tree-vect-loop.c、tree-vect-loop-manip.c、tree-vect-slp.c、tree-vect-stmts.c、tree-vect-data-refs.c和tree-vect-data-refs.c等文件中。其中SLP(Superword Level Parallelism)超长指令字。

对于循环和基本块向量化,GCC实现了三种:循环向量化(迭代间并行);循环明确SLP(loop-aware SLP,迭代内并行);基本块并行化(Basic block,循环外)。下图是简要的流程刻画:

     tree-vectorizer.c:

     loop_vect()  loop_aware_slp()  slp_vect()

          |        /           \          /

          |       /             \        /

          tree-vect-loop.c  tree-vect-slp.c

                | \      \  /      /   |

                |  \      \/      /    |

                |   \     /\     /     |

                |    \   /  \   /      |

         tree-vect-stmts.c  tree-vect-data-refs.c

                       \      /

                    tree-vect-patterns.c

以vectorize_loops()函数为例进行分析。代码如下:

unsigned

vectorize_loops (void)

{

  unsigned int i;

  unsigned int num_vectorized_loops = 0;

  unsigned int vect_loops_num;

  loop_iterator li;

  struct loop *loop;

 

  vect_loops_num = number_of_loops ();

 

  /* Bail out if there are no loops.  */

  if (vect_loops_num <= 1)

    return 0;

 

  /* Fix the verbosity level if not defined explicitly by the user.  */

  vect_set_dump_settings (false);

 

  init_stmt_vec_info_vec ();

 

  /*  ----------- Analyze loops. -----------  */

 

  /* If some loop was duplicated, it gets bigger number

     than all previously defined loops.  This fact allows us to run

     only over initial loops skipping newly generated ones.  */

  FOR_EACH_LOOP (li, loop, 0)

    if (optimize_loop_nest_for_speed_p (loop))

      {

       loop_vec_info loop_vinfo;

 

       vect_location = find_loop_location (loop);

       loop_vinfo = vect_analyze_loop (loop);

       loop->aux = loop_vinfo;

 

       if (!loop_vinfo || !LOOP_VINFO_VECTORIZABLE_P (loop_vinfo))

         continue;

 

       vect_transform_loop (loop_vinfo);

       num_vectorized_loops++;

      }

 

  vect_location = UNKNOWN_LOC;

 

  statistics_counter_event (cfun, "Vectorized loops", num_vectorized_loops);

  if (vect_print_dump_info (REPORT_UNVECTORIZED_LOCATIONS)

      || (num_vectorized_loops > 0

         && vect_print_dump_info (REPORT_VECTORIZED_LOCATIONS)))

    fprintf (vect_dump, "vectorized %u loops in function.\n",

            num_vectorized_loops);

 

  /*  ----------- Finalize. -----------  */

 

  mark_sym_for_renaming (gimple_vop (cfun));

 

  for (i = 1; i < vect_loops_num; i++)

    {

      loop_vec_info loop_vinfo;

 

      loop = get_loop (i);

      if (!loop)

       continue;

      loop_vinfo = (loop_vec_info) loop->aux;

      destroy_loop_vec_info (loop_vinfo, true);

      loop->aux = NULL;

    }

 

  free_stmt_vec_info_vec ();

 

  return num_vectorized_loops > 0 ? TODO_cleanup_cfg : 0;

}

该函数实现了循环的向量化。主要过程:

(1)初始化阶段:定义必要的变量,调用number_of_loops函数得到循环的个数,若循环个数为0,返回0;否则冗余级。为stmt_vec_info创建哈希表。

(2)分析阶段:初始化完成后,进入循环分析阶段。该阶段通过调用optimize_loop_nest_for_speed_p函数分析循环是否可以向量化。如果能够向量化,则调用vect_transform_loop将该循环向量化。

(3)收尾阶段:该阶段调用mark_sym_for_renaming函数,SYM被update_ssa重命名;通过destroy_loop_vec_info和free_stmt_vec_info_vec函数将中间过程用到而随后不需要的数据删除。

   对于loop-aware SLP和basic block向量化于循环向量化分析类似,都要经过三种阶段,区别是循环分析的方法和内容不同。具体的分析方法还是比较复杂的。

   对于向量化,GCC还有一种优化方法“用于向量化的Tree级if转换(Tree level if-conversion for vectorizer)。该过程变现为这样的形式,是向量化能够对语句和可用的向量操作进行一一映射。该过程位于tree-if-conv.c,并由pass_if_conversion来描述。该过程如下:

Ø  确定循环是否是if转换

Ø  以宽度优先遍历所有的循环基本块

                                    i.             移除条件表达(在基本块末端),并且将条件传播到目的基本块。

                                  ii.             用使用当前基本块的条件修改表达式替换修改的表达式。

Ø  合并所有的基本块。

下图是该优化的转换样例:

INPUT

     -----

 

     # i_23 = PHI <0(0), i_18(10)>;

     <L0>:;

     j_15 = A[i_23];

     if (j_15 > 41) goto <L1>; else goto <L17>;

 

     <L17>:;

     goto <bb 3> (<L3>);

 

     <L1>:;

 

     # iftmp.2_4 = PHI <0(8), 42(2)>;

     <L3>:;

     A[i_23] = iftmp.2_4;

     i_18 = i_23 + 1;

     if (i_18 <= 15) goto <L19>; else goto <L18>;

 

     <L19>:;

     goto <bb 1> (<L0>);

 

     <L18>:;

OUTPUT

     ------

 

     # i_23 = PHI <0(0), i_18(10)>;

     <L0>:;

     j_15 = A[i_23];

 

     <L3>:;

     iftmp.2_4 = j_15 > 41 ? 42 : 0;

     A[i_23] = iftmp.2_4;

     i_18 = i_23 + 1;

     if (i_18 <= 15) goto <L19>; else goto <L18>;

 

     <L19>:;

     goto <bb 1> (<L0>);

 

     <L18>:;

 

3.2 自动并行化(auto parallelization)

在tree-ssa的循环优化中,还有另一类优化:自动并行化。该遍优化将循环迭代转变为多线程,实现代码在“tree-parloops.c”。现代计算机包含多个CPU,多个核,特别是超级计算机可能包含成千上万个CPU,这就要求编写的程序能够充分利用这些计算资源,所以编译器的并行化就变得很重要。GCC通过将不相关的代码实现线程级的并行化,来提高整体的执行效率。GCC支持OpenMP并行编程,以充分利用计算机的计算能力。线程,有时被称为轻量级进程,是程序执行流的最小单元。

自动向量化很长一阶段都是一个难点,可以想见串行指令变为并行指令是极为困难的。然而,目前并行性是提高整体性能的关键因素,GCC支持四种同步模式:

向量化前面已经探讨了,对于并行编程,GCC直接支持OpenMP,而MPI尚没有直接支持。

GCC使用vect_force_simple_reduction()去检测减少模式,下面是代码转换的例子:

parloop

{

  int sum=1;

 

  for (i = 0; i < N; i++)

   {

    x[i] = i + 3;

    sum+=x[i];

   }

}

 

gimple-like code:

header_bb:

 

  # sum_29 = PHI <sum_11(5), 1(3)>

  # i_28 = PHI <i_12(5), 0(3)>

  D.1795_8 = i_28 + 3;

  x[i_28] = D.1795_8;

  sum_11 = D.1795_8 + sum_29;

  i_12 = i_28 + 1;

  if (N_6(D) > i_12)

    goto header_bb;

 

 

exit_bb:

 

  # sum_21 = PHI <sum_11(4)>

  printf (&"%d"[0], sum_21);

 

 

after reduction transformation (only relevant parts):

 

parloop

{

 

....

 

 

  # Storing the initial value given by the user.  #

 

  .paral_data_store.32.sum.27 = 1;

 

  #pragma omp parallel num_threads(4)

 

  #pragma omp for schedule(static)

 

  # The neutral element corresponding to the particular

  reduction's operation, e.g. 0 for PLUS_EXPR,

  1 for MULT_EXPR, etc. replaces the user's initial value.  #

 

  # sum.27_29 = PHI <sum.27_11, 0>

 

  sum.27_11 = D.1827_8 + sum.27_29;

 

  GIMPLE_OMP_CONTINUE

 

  # Adding this reduction phi is done at create_phi_for_local_result() #

  # sum.27_56 = PHI <sum.27_11, 0>

  GIMPLE_OMP_RETURN

 

  # Creating the atomic operation is done at

  create_call_for_reduction_1()  #

 

  #pragma omp atomic_load

  D.1839_59 = *&.paral_data_load.33_51->reduction.23;

  D.1840_60 = sum.27_56 + D.1839_59;

  #pragma omp atomic_store (D.1840_60);

 

  GIMPLE_OMP_RETURN

 

 # collecting the result after the join of the threads is done at

  create_loads_for_reductions().

  The value computed by the threads is loaded from the

  shared struct.  #

 

 

  .paral_data_load.33_52 = &.paral_data_store.32;

  sum_37 =  .paral_data_load.33_52->sum.27;

  sum_43 = D.1795_41 + sum_37;

 

  exit bb:

  # sum_21 = PHI <sum_43, sum_26>

  printf (&"%d"[0], sum_21);

 

...

}

自动并行化涉及到很多分析步骤,每一个优化的编译器都必须要实现相似的过程,具体的实现过程可能不相同,这就决定了编译器的能力。GCC是功能强大的,在自动并行化的优化上主要涉及以下几步:

Ø  别名分析:确定那个存储位置能够被超过一种方式访问。

Ø  迭代变数分析:对于自动并行化,循环迭代必须是可计算的。

Ø  归纳变量分析:自动并行化的工作是将循环迭代分配到线程中去。为了实现这个目的,每个线程必须有自己的私有归纳变量拷贝。

Ø  数据相关性分析:由串行指令变为并行指令,数据相关性分析是极为重要的,数据相关性直接制约着程序的并行潜力。当前GCC包括两种框架实现数据相关性分析:lambda和GRAPHITE。

四、结论

通过这一阶段的GCC学习,充分认识到GCC强大的功能,以前仅仅认为GCC只支持C和C++,然而GCC能够支持很多通用的语言,并且也支持并行化编程(OpenMP)。对于GCC的开发者,深深地折服。

GCC的学习是一个“浩瀚的工程”,仅仅GCC的源码就超过400MB,阅读起来相当的吃力,甚至于上面的优化分析的代码很多也是没有读懂的,猜测是这个意思,也就写上了;GCC的学习也是一个“任重道远的工作”,GCC是如此的优秀,值得我们深入学习:编译器架构、优化算法、代码等等。

收获也是颇丰的,能够进一步了解GCC,知道其中的编译选项的用法,极大地提高了阅读代码的能力。

五、参考文献

本部分罗列一些参考文献和网站,很多都是能够在http://gcc.gnu.org上找到的。

²  GNU Compiler Collection Internals

²  Compilers Principles,Techniques,&Tools(龙书)

²  www.wiki.com

²  Auto-vectorization in GCC

²  Debugging Options-Using the GNU CompilerCollection

                                                                                                         

                                                                                                                                                                                                                                       转自:http://blog.youkuaiyun.com/lingdongjiangzhi/article/details/7576721

<think>我们正在讨论GCC编译器的优化选项及使用方法。根据提供的引用资料,我们可以总结出以下关键点:1.GCC提供了多种优化选项,可以通过命令行参数进行控制。2.常见的优化等级包括-O0(无优化)、-O1、-O2、-O3(最高优化等级)等。3.除了整体优化等级外,GCC还提供许多具体的优化选项,例如-fomit-frame-pointer(省略帧指针)。4.在S32DS开发环境中,可以通过工程属性设置优化等级。根据这些信息,我们可以为用户提供GCC优化选项的详细说明和使用方法。###GCC优化选项及使用方法GCC编译器提供了多层次的优化选项,主要分为**优化等级**和**具体优化选项**两类。####1.优化等级(最常用)-**-O0**:关闭所有优化。这是默认选项,适用于调试阶段,因为生成的代码与源代码结构一致,便于调试。-**-O1**或**-O**:基础优化。编译器会尝试减少代码大小和执行时间,但不会进行耗时较长的优化。-**-O2**:更高级的优化。包括处理器指令调度、循环优化等,通常推荐在发布版本中使用。-**-O3**:最高级优化。在-O2基础上增加更多优化,如函数内联、循环展开等,但可能增加代码大小。-**-Os**:优化代码大小。在-O2的基础上,选择那些不会显著增加代码大小的优化。-**-Ofast**:执行所有-O3优化,并启用一些不严格遵循标准的优化(如浮点运算的激进优化)。使用示例:```bashgcc-O2-omy_programmy_program.c```####2.常用具体优化选项除了整体优化等级,还可以单独启用或禁用某些优化:-**-fomit-frame-pointer**:省略帧指针(framepointer),可以释放一个寄存器(通常为EBP/RBP),可能提升性能[^1]。-**-funroll-loops**:展开循环,减少循环开销,但可能增加代码大小。-**-finline-functions**:对认为简单的函数进行内联扩展。-**-march=native**:生成针对当前主机处理器架构优化的代码(使用主机支持的所有指令集)。使用示例(组合多个选项):```bashgcc-O2-fomit-frame-pointer-march=native-omy_programmy_program.c```####3.如何检测优化效果-**查看汇编代码**:使用`-S`选项生成汇编代码,便于分析编译器优化行为。```bashgcc-O2-S-omy_program.smy_program.c```-**使用性能分析工具**:如`perf`(Linux)或`gprof`,对比优化前后的程序性能。####4.在集成开发环境(IDE)中设置优化选项以S32DS为例,设置优化等级的方法[^3]:1.右键单击工程中的源文件。2.选择“属性”(Properties)。3.导航到:C/C++Build->Settings->StandardS32DSCCompiler->Optimization。4.在“OptimizationLevel”下拉菜单中选择所需优化等级(如-O2)。####注意事项-高优化等级(如-O3)可能增加编译时间,且不一定在所有情况下都能提升性能。-某些优化可能影响调试,因此在开发阶段建议使用-O0或-O1。-激进优化(如-Ofast)可能改变浮点运算行为,在科学计算中需谨慎使用。###总结GCC优化选项的使用需要根据具体场景权衡。通常,开发调试阶段使用-O0,发布版本使用-O2。特殊需求(如最小代码大小)则选择-Os。此外,可以结合具体选项进行微调。建议通过性能测试确定最佳优化方案。§§相关问题§§1.GCC优化选项-O1、-O2、-O3的具体区别是什么?2.如何避免GCC优化带来的副作用(如调试困难)?3.GCC优化选项对嵌入式系统开发有何特殊考虑?4.如何为特定处理器架构(如ARMCortex-M)优化代码?</think>### GCC优化选项及使用方法详解 GCC(GNU Compiler Collection)的优化选项可显著提升代码性能,以下是核心选项和使用方法: #### 一、优化等级(最常用) 1. **-O0**(默认) - 关闭所有优化,保留调试信息 - 编译速度快,适合开发调试 ```bash gcc -O0 -o program source.c ``` 2. **-O1 / -O**(基础优化) - 减少代码体积和执行时间 - 包含跳转优化、常量传播等 ```bash gcc -O1 -o program source.c ``` 3. **-O2**(推荐发布级别)⭐ - 包含循环优化、指令调度等 - 平衡性能与代码大小 ```bash gcc -O2 -o program source.c ``` 4. **-O3**(激进优化) - 增加函数内联、循环展开等 - 可能增加代码体积 ```bash gcc -O3 -o program source.c ``` 5. **-Os**(优化代码大小) - 在-O2基础上优先减小体积 - 适合嵌入式系统 ```bash gcc -Os -o program source.c ``` #### 二、关键子选项 1. **-fomit-frame-pointer** - 省略栈帧指针(释放EBP/RBP寄存器) - 提升性能但影响调试[^1] ```bash gcc -O2 -fomit-frame-pointer ... ``` 2. **-march=native** - 针对当前CPU架构优化 ```bash gcc -O2 -march=native ... ``` 3. **-funroll-loops** - 循环展开(需配合-O3) ```bash gcc -O3 -funroll-loops ... ``` #### 三、IDE配置示例(Code::Blocks/S32DS) 1. **Code::Blocks** ```plaintext Project → Build options → Compiler settings → Other options 添加 -O2 -march=native ``` 2. **S32DS**(NXP开发环境) ```plaintext 右键源文件 → Properties → C/C++ Build → Standard S32DS C Compiler → Optimization → 选择优化等级[^3] ``` #### 四、优化效果验证 1. 查看汇编代码: ```bash gcc -O2 -S -o output.s input.c ``` 2. 性能分析工具: ```bash perf stat ./program # Linux性能计数器 gprof ./program # 函数级性能分析 ``` #### 五、注意事项 1. 调试时建议使用 **-O0 -g** 2. 高优化等级可能暴露未定义行为 3. 嵌入式系统需注意 **-Os** 与栈溢出风险 4. 旧版GCC需验证选项兼容性[^2] > **最佳实践**:发布版本建议组合使用 > ```bash > gcc -O2 -march=native -fomit-frame-pointer -pipe > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值