编译器是连接软件和硬件的桥梁,它生成代码的质量直接影响程序在硬件上的执行效率。本篇首先介绍编译器的原理及基本工作流程,然后介绍了编译的多种选项,最后讲解如何进行编译优化。特别的还补充说明了拓展数学库的相关知识,以及运行时的优化因素。
目录
1编译器结构
编译器按照工作流程可以分为:预编译(生成.i文件) → \rightarrow →编译(生成.s文件) → \rightarrow →汇编(生成.o文件) → \rightarrow →链接(生成可执行文件)。以下细分为前中后三个阶段进行讲解。
1.1前端
前端包括预编译、词法分析、语法分析、语义分析、中间代码生成。
-
预编译
预编译过程主要是对源文件进行文件包含(#include),宏展开(#define),条件编译(#if #ifdef),删除注释(删除"/*xxx*/" 和 “//xxx”)等操作,得到一个完整的、仅有执行语句的源文件。
-
词法分析
识别出每一个语句中的一个个单词,其中单词包含:
关键字:int , sizeof , for , if
标识符:变量名,函数名
常数:整数、字符串
运算符:+ , - , &&等
分界符:, ; :
-
语法分析
将上述单词按照规则组合起来,形成语句。
以
c = a+b*3;
为例首先根据以下5个规则判断出这是一个赋值语句:
<赋值语句> = <标识符> “=” <表达式>
<表达式> = <表达式> “+” <表达式>
<表达式> = <表达式> “*” <表达式>
<表达式> = 标识
<表达式> = 数值
而后将语句变为语法树
-
语义分析
审查上述语句是否有错误或者需要添加的操作:
静态语义审查处理
上下文相关性审查(比如,使用了未赋值的变量)
类型匹配审查(比如,函数调用时参数类型不匹配)
类型转换(这个就是要另外添加的操作)
比如,上述语句中的变量都是浮点型,那么语法树就变为了
-
中间代码生成
以LLVM编译器为例,它可以生成3种格式的中间代码。在内存中编译的中间语言,在硬盘上二进制存储的中间语言(.bc),可读的中间代码格式(.ll)。
1.2中端
中端包括常规编译优化、过程间优化、循环优化、反馈优化等其他方式,对中间代码优化。详细的内容,见3编译优化部分
比如,对冗余代码进行删除:
优化前
c = a+b*3;
b = a;
return c;
优化后
c = a+b*3;
return c;
1.3后端
后端主要包括目标代码生成、链接。
-
目标代码生成
代码生成的目的是为了将普适的中间代码变换成特定硬件平台上的目标代码,并针对该硬件环境的特点(寄存器个数、内存大小、架构方式等)进行针对硬件的汇编代码优化。
-
链接
其主要功能是将各个编译单元(如源文件、库文件)编译生成的目标文件进行链接,形成最终的可执行程序或库。编译器链接的功能包括以下几个方面:
-
符号解析(Symbol Resolution):编译器链接会解析各个目标文件中使用的符号(如变量、函数名),找到它们对应的地址或存储空间。
-
符号重定位(Symbol Relocation):编译器链接会根据符号解析的结果,将所有目标文件中引用的符号的地址信息更新到最终的可执行程序或库中的正确位置。
-
库文件链接(Library Linking):编译器链接还会将所需的库文件链接到最终的可执行程序或库中,以满足程序对外部函数和变量的调用需求。
-
符号重复检查(Symbol Duplication Checking):编译器链接也会检查是否有重名的符号存在,避免符号冲突导致链接错误。
-
目标文件排列(Object File Arrangement):编译器链接会合并各个目标文件中的代码和数据段,进行地址重定位和布局排列,生成最终的可执行程序或库文件。
-
生成符号表(Symbol Table Generation):编译器链接会生成符号表,记录各个符号的名称、地址、大小等信息,以供程序在运行时进行符号查找和加载。
链接可以在源代码翻译成机器码的过程中完成(静态链接),也可以在程序运行时完成(动态链接),以下是两者的对比
特点 静态链接 动态链接 链接时间 形成可执行文件前 程序执行时 方式 地址与空间分配和符号解析和重定位 装载时重定位和地址无关代码技术 库扩展名 .a .so 优缺点 程序的启动、运行速度快,方便移植;浪费内存和磁盘空间 增加程序执行时的开销,可移植性差,但是节省了内存和磁盘的空间 -
2编译选项
本部分主要介绍优化人员和编译器的交互内容。