GCC优化浅析

来源:互联网 发布:unity3d 绑定脚本 编辑:程序博客网 时间:2024/06/05 09:37

一、前言

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

                                                                                                         

                                                                                                                                                                                                                                                                                        于长沙

原创粉丝点击