GCC优化浅析
来源:互联网 发布:a站b站有什么区别知乎 编辑:程序博客网 时间:2024/06/06 15:48
一、前言
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.csdn.net/lingdongjiangzhi/article/details/7576721
- GCC优化浅析
- GCC优化浅析
- GCC优化浅析
- gcc/arm-linux-gcc 浅析
- gcc优化
- GCC编译过程浅析
- 浅析gcc内嵌汇编
- 浅析gcc内嵌汇编
- [转]GCC、ARM-LINUX-GCC、ARM-ELF-GCC浅析
- GCC、ARM-LINUX-GCC、ARM-ELF-GCC浅析【转】
- GCC、ARM-LINUX-GCC、ARM-ELF-GCC浅析
- GCC、ARM-LINUX-GCC、ARM-ELF-GCC浅析【原】
- GCC、ARM-LINUX-GCC、ARM-ELF-GCC浅析
- 浅析gcc、arm-linux-gcc和arm-elf-gcc关系
- GCC编译优化指南
- gcc常用优化选项
- gcc 编译优化
- GCC编译优化指南
- Ubuntu14.04 Unity桌面快捷方式
- Auto-vectorization in GCC
- java-流-输出流
- A Mountaineer
- 正式开始写博客了
- GCC优化浅析
- spring batch之二 一个简单的spring batch的例子.
- GCC剖析
- MYSQL基准测试工具
- eclipse中adb没有运行的错误解决
- 【HrbustOJ】Escaping(网络流,二部图)
- 透过源码领悟GCC到底在干些什么
- RakNet学习 (5) -- 详细实现
- 冒泡