自编教材分享:第五章—编译与运行优化(三)


目标代码生成
该过程是把中间代码变换成特定机器上的目标代码,形式上包括:绝对指令代码、可重定位的指令代码、汇编指令代码。这是编译的最后阶段,它的工作与硬件系统结构和指令含义有关,涉及到硬件系统功能部件的运用、机器指令的选择、各种数据类型变量的存储空间分配以及寄存器分配等。在LLVM中使用clang -S file.c命令进行编译,生成特定平台的汇编代码.s文件。
以汇编文件中的加法指令为例,该汇编指令由一个操作码和两个操作数组成,操作码addl为加法操作,操作数%x和操作数%y执行加法运算,并将结果放置在%x中。

file.c文件:
file.ll文件:
file.s文件:
目标文件格式
目标文件是源代码编译后但未链接的中间文件,它跟可执行文件的内容与结构很相似,从广义上看目标文件和可执行文件的格式几乎是一样的,在Linux操作系统下都是按照可执行可链接文件格式(Executable Linkable Format,ELF)进行存储。 ELF目标文件格式的最前部是ELF头,包含描述整个文件的基本属性,包括ELF文件版本、目标机器型号、程序入口地址等。节头表描述目标文件中各个节的信息,在ELF头和节头表之间是节本身,其中包含十个段。这十段内容组成了可重定位目标文件,有了这些目标文件之后,通过静态链接就可以将它们组合起来,形成一个可以运行的程序。

总体来说,程序源代码被编译以后,按照代码和数据分别存放到相应的段中,代码段.text段属于程序指令,.data段和.bss段属于程序数据,编译器或汇编器还会将一些辅助性信息诸如符号、重定位信息等也按照表的方式存放在目标文件中,通常情况下,一个表往往是一个段。
相关选项
源于LLVM的设计是高度模块化的,所以针对后端会有特定的优化,比如X86、ARM后端等,并通过优化选项控制。例如针对X86结构的常用后端选项-mavx,功能是支持MMX、SSE和AVX等内置函数和代码的生成。在代码生成阶段可以通过数据选项选择对数据的处理方式,通过目标平台选项生成指定平台的代码,通过后端选项打开不同后端支持的优化功能。
数据选项用于控制数据相关操作,包括数据对齐的方式、强制double输入类型的位数、内存模型的设定等,比如常用选项-mdouble=<value>的功能是指定double类型数据的位数,其中参数可设置为32和64。
通过目标平台选项可以指定生成代码的目标运行机器,包括X86、AMDGPU、ARM等,比如-march=<cpu>选项,指定LLVM为特定的处理器生成代码。

汇编与链接
汇编器
汇编器是将汇编代码转变为机器可以执行的指令,每一个汇编语句都对应一条机器指令,由.s汇编文件经汇编器生成.o目标代码文件。
若有变量定义在其它目标代码文件中,则只有运行链接的时候才能确定绝对地址,所以现代的编译器可以将一个源代码文件编译成一个可重定位的目标文件,最终由链接器将这些目标文件链接起来形成可执行文件。
链接的功能是将一个或多个目标文件以及库文件合并为一个可执行文件。链接可以在源代码翻译成机器代码即编译的时候完成,也可以在程序装入内存时完成,甚至可以在程序运行时完成,根据不同的完成时期可将链接分为静态链接和动态链接。
静态链接与动态链接
静态链接
静态链接指链接器将外部函数所在的静态链接库直接拷贝到目标可执行程序中,这样在执行该程序时这些代码会被装入到该进程的虚拟地址空间中。
关于静态链接,首先使用clang将a.c和b.c分别编译成目标文件a.o和b.o。从代码中可以看到,b.c总共定义了两个全局符号,一个是变量shared,另外一个是函数add。 程序a.c里面定义了一个全局符号是main,且引用b.c里面的shared和add,接下来就是把a.o和b.o这两个目标文件链接在一起并最终形成一个可执行文件ab.out。
动态链接
动态链接是把程序拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序。
动态链接不是像静态链接一样把所有的源代码文件都链接成一个单独的可执行文件。它首先将hello.c编译生成hello.o文件,由hello.o文件生成动态库libhello.so文件,该文件是包含hello.c中hello()函数的共享对象文件,在编译main.c时通过链接动态库libhello.so文件生成可执行文件。
动态链接与静态链接的对比
静态链接对目标文件的更新很不友好,假如一个.o目标文件依赖20个.o目标文件,当20个目标文件中有一个需要更新时,需要将所有目标文件的源代码重新编译出一个可执行程序才可以更新成功,为了解决静态链接的这一缺点,所以引入了动态链接。动态链接可以压缩可执行文件的大小,缺点是可移植性太差,如果两台机器运行环境不同,动态库存放的位置不一致,可能会导致程序运行失败。总结来说,静态链接和动态链接各有优点和缺点。优化人员可根据实际需要在两种方式之间选择,挑选出最适合的链接方式,以达到最优的效果。

链接时优化
链接时优化是链接期间的程序优化,多个中间文件通过链接器合并在一起组合为一个程序,缩减代码体积,并通过对整个程序的分析以实现更好的运行时性能。优化人员通过选项-flto指示LLVM编译器生成含有LLVM比特码的.o文件,将代码生成延迟到链接阶段,并在链接阶段对代码实现进一步地优化。

当链接器检测到.o文件为LLVM比特码时,会将所有的比特码文件读入内存并链接起来,然后再进行跨文件地内联、常量传播和更激进地死代码消除等优化。
选项-flto后可跟参数。默认情况下-flto为full模式。-flto=full指链接时优化将分散的目标文件的LLVM IR组合到一个大的LLVM目标文件中,然后对其整体分析、优化并生成机器码。-flto=thin是把目标文件分开,根据需要才从其它目标文件中导入功能,使用选项-flto=thin链接的速度要快于使用选项-flto=full。
在程序的编译阶段,LLVM编译器通过建立全局函数调用图从而发现并删除没有被调用的死函数。在程序的链接阶段,链接器对所有的输入文件进行解析后,可以建立符号以及函数的相互引用关系,从而发现并删除没有被引用的符号以及对应的函数。所以LLVM编译器和链接器之间的紧密集成实现了更多地优化。
示例:

LLVM将输入的源文件a.c编译成LLVM比特码文件,将输入的源文件main.c编译成本机目标代码。#a.o 是LLVM比特码文件;#main.o是本机目标代码文件
最后通过命令将两个文件链接生成可执行文件main,在该过程中,链接器首先识别出a.c中的foo2()是LLVM 比特码文件中定义的外部可见符号,完成通常的符号解析传递,发现foo2()并没有被使用后,LLVM编译器删除foo2()。一旦删除了foo2(),编译器会识别出条件i=0,这表明foo3()从来没被使用过,因此删除foo3(),之后删除foo4(),最后生成的文件如右边所示。这个例子说明了编译器与链接器紧密集成的优点,编译器不能在没有链接器输入的情况下删除foo3()。
数学库
程序在编译过程中经常链接数学库,数学库是开展科学计算、工程计算等必备的核心基础软件,数学函数的性能、可靠性和精度对上层应用程序尤其是科学计算程序的解算至关重要。使用数学库中提供的各类数学函数,能够缩短应用程序的开发周期,并获取库函数所带来的性能收益。
除了前文提到的基础数学函数库之外,还有很多扩展数学库,如BLAS库、LAPACK库、MKL库等,这些数学库广泛应用于人工智能、数据分析等科学计算领域,其中BLAS库已经成为初等线性代数运算的业界标准,被广泛应用于科学及工程计算,也是许多数学软件的基本核心,本节以BLAS库为例介绍数学库的使用。
BLAS(Basic Linear Algebra Subprograms)基本线性代数库是一组高质量的基本向量、矩阵运算子程序。由于BLAS涉及最基本的向量、矩阵运算应用程序的开发者只需要运用适当的技术将计算过程抽象为矩阵、向量的基本运算,就可以调用相应的BLAS库函数而不必考虑与计算机体系结构相关的性能优化问题。
BLAS从结构上分为三部分包括:
Level 1 BLAS:向量和向量,向量和标量之间的运算;
Level 2 BLAS:向量和矩阵间的运算;
Level 3 BLAS:矩阵和矩阵之间的运算。
BLAS库函数的命名由三部分组成:数据类型、矩阵类型、操作类型。
BLAS库支持的数据类型

BLAS库支持的矩阵类型

BLAS库支持的常用函数操作

英特尔数学内核库(Intel Math Kernel Library,MKL)为英特尔包含有BLAS计算功能的数学核心函数库。当在C语言中使用该数学库编程时需引用头文件mkl_cblas.h。
使用Intel 的ICC编译器对代码进行编译,需要添加选项-mkl=<arg>选项,其中不同的参数表示的含义不同,包括:
-mkl或-mkl=parallel:并行链接Intel(R) MKL库,这也是使用-mkl选项时的默认值;
-mkl=sequential:采用串行Intel(R) MKL库链接;
-mkl=cluster:使用Intel(R) MKL Cluster库和Intel(R) MKL序列库链接。
数学库优化
在性能要求苛刻的情况下,函数库提供的性能有时并不能满足优化人员的需要,因此需要优化人员提升库的性能。优化人员可以根据一些平台无关的算法,在考虑平台的相关特性后自行对库函数进行优化,以达到对于性能的要求。
通常代码中的abs数学函数都是标量运算,本示例在没有数据依赖的情况下,将标量abs运算改为向量abs运算,这样可以同时处理4个abs值求解。
未向量化示例abs.c:
手工向量化示例abs-vec.c:
相关选项
汇编与链接相关选项如下表,其中选项-flto=full指链接时优化将分散的目标文件的LLVM IR组合到一个大的LLVM目标文件中,然后对其整体分析、优化并生成机器码。-flto=thin是把目标文件分开,根据需要才从其它目标文件中导入功能,使用选项-flto=thin链接的速度要快于使用选项-flto=full。
