编译器是连接软件和硬件的桥梁,它生成代码的质量直接影响程序在硬件上的执行效率。本篇首先介绍编译器的原理及基本工作流程,然后介绍了编译的多种选项,最后讲解如何进行编译优化。特别的还补充说明了拓展数学库的相关知识,以及运行时的优化因素。
目录
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后端
后端主要包括目标代码生成、链接。
- 目标代码生成代码生成的目的是为了将普适的中间代码变换成特定硬件平台上的目标代码,并针对该硬件环境的特点(寄存器个数、内存大小、架构方式等)进行针对硬件的汇编代码优化。
- 链接其主要功能是将各个编译单元(如源文件、库文件)编译生成的目标文件进行链接,形成最终的可执行程序或库。编译器链接的功能包括以下几个方面:1. 符号解析(Symbol Resolution):编译器链接会解析各个目标文件中使用的符号(如变量、函数名),找到它们对应的地址或存储空间。2. 符号重定位(Symbol Relocation):编译器链接会根据符号解析的结果,将所有目标文件中引用的符号的地址信息更新到最终的可执行程序或库中的正确位置。3. 库文件链接(Library Linking):编译器链接还会将所需的库文件链接到最终的可执行程序或库中,以满足程序对外部函数和变量的调用需求。4. 符号重复检查(Symbol Duplication Checking):编译器链接也会检查是否有重名的符号存在,避免符号冲突导致链接错误。5. 目标文件排列(Object File Arrangement):编译器链接会合并各个目标文件中的代码和数据段,进行地址重定位和布局排列,生成最终的可执行程序或库文件。6. 生成符号表(Symbol Table Generation):编译器链接会生成符号表,记录各个符号的名称、地址、大小等信息,以供程序在运行时进行符号查找和加载。链接可以在源代码翻译成机器码的过程中完成(静态链接),也可以在程序运行时完成(动态链接),以下是两者的对比特点静态链接动态链接链接时间形成可执行文件前程序执行时方式地址与空间分配和符号解析和重定位装载时重定位和地址无关代码技术库扩展名.a.so优缺点程序的启动、运行速度快,方便移植;浪费内存和磁盘空间增加程序执行时的开销,可移植性差,但是节省了内存和磁盘的空间
2编译选项
本部分主要介绍优化人员和编译器的交互内容。
2.1前端选项
预处理
编译器选项功能描述GCC-E仅进行预处理,生成预处理后的代码而不进行编译-M生成包含文件依赖关系的规则-undef清除预定义的宏定义-nostdinc不搜索标准系统头文件目录-I<dir>添加包含目录Clang-E仅进行预处理,生成预处理后的代码而不进行编译-M生成包含文件依赖关系的规则-undef清除预定义的宏定义-nostdinc不搜索标准系统头文件目录-I<dir>添加包含目录Microsoft Visual Studio/P仅进行预处理,生成预处理后的代码而不进行编译/showIncludes显示包含的头文件信息/I<dir>添加包含目录
语言和模式
编译器选项功能描述GCC-std=<standard>指定要遵循的语言标准,如-std=c11或-std=gnu11-ansi启用 ANSI C 标准模式-Wall显示所有警告信息-Werror将警告视为错误-O<level>指定优化级别,如-O0(无优化)、-O1(基本优化)、-O2(较高优化)等-m32生成 32 位代码Clang-std=<standard>指定要遵循的语言标准,如-std=c11或-std=gnu11-ansi启用 ANSI C 标准模式-Wall显示所有警告信息-Werror将警告视为错误-O<level>指定优化级别,如-O0(无优化)、-O1(基本优化)、-O2(较高优化)等-m32生成 32 位代码Microsoft Visual Studio/std:<standard>指定要遵循的语言标准,如/std:c11或/std:c++17/Wall显示所有警告信息/Werror将警告视为错误
2.2优化选项
内联优化
编译器选项功能描述GCC-finline-functions将函数调用替换为函数体,以减少函数调用的开销-finline-limit=<N>设置内联函数体大小的上限,超过此大小的函数将不会被内联-finline-small-functions优化小函数的内联Clang-finline-functions将函数调用替换为函数体,以减少函数调用的开销-finline-limit=<N>设置内联函数体大小的上限,超过此大小的函数将不会被内联-finline-small-functions优化小函数的内联Microsoft Visual Studio/Ob[<n>]控制函数的内联优化,/Ob0 表示禁用内联,/Ob1 表示只内联 __inline 函数,/Ob2 表示内联所有适合的函数
循环优化
编译器选项功能描述GCC-ftree-loop-optimize开启循环优化-ftree-loop-linear线性循环优化-funroll-loops循环展开-floop-interchange循环交换-floop-block指定循环分块优化的程度Clang-floop-optimize开启循环优化-floop-unroll-and-jam循环展开并插入融合指令-fvectorize启用循环向量化优化Microsoft Visual Studio/GL启用整个程序优化/Oy-停止生成 Frame Pointer
自动向量化
编译器选项功能描述GCC-ftree-vectorize启用自动向量化优化-fopt-info-vec显示有关向量化优化的信息Clang-fvectorize启用自动向量化优化-fvectorize-slp使用交叉循环向量化优化-Rpass=loop-vectorize显示循环向量化优化信息Microsoft Visual Studio/Qvec启用向量化优化/Qvec-report:2显示有关矢量化计算的信息
自动并行化
编译器选项功能描述GCC-fopenmp启用 OpenMP 并行化支持-ftree-parallelize-loops尝试并行化适合的循环Clang-fopenmp启用 OpenMP 并行化支持-fopenmp-simd尝试并行执行 OpenMP SIMD 循环Microsoft Visual Studio/Qpar启用并行性能优化/Qpar-report:2显示并行性能优化的信息
浮点优化
编译器选项功能描述GCC-ffast-math允许忽略 IEEE 标准,以提高数学表达式的性能-funsafe-math-optimizations执行可能非法的数学优化-fassociative-math允许重新关联数学运算-freciprocal-math使用倒数来代替除法操作Clang-ffast-math允许忽略 IEEE 标准,以提高数学表达式的性能-funsafe-math-optimizations执行可能非法的数学优化-fassociative-math允许重新关联数学运算-freciprocal-math使用倒数来代替除法操作Microsoft Visual Studio/fp:fast使用快速浮点模式/fp:precise使用精确浮点模式
优化级别
编译器选项功能描述GCC-O0无优化-O1基本优化-O2较高优化-O3更高优化-Ofast除了 IEEE 或 ISO C 标准外的所有优化-Os优化代码大小Clang-O0无优化-O1基本优化-O2较高优化-O3更高优化-Ofast除了 IEEE 或 ISO C 标准外的所有优化-Os优化代码大小Microsoft Visual Studio/O1生成较快代码,但保留调试能力/O2生成较快代码,可能会减少可读性和调试能力/Ox最大优化
2.3代码生成选项
编译阶段
编译器选项功能描述GCC-c仅编译源文件而不进行链接-E只运行预处理器并输出预处理结果-S将源代码编译成汇编代码而不进行汇编Clang-c仅编译源文件而不进行链接-E只运行预处理器并输出预处理结果-S将源代码编译成汇编代码而不进行汇编Microsoft Visual Studio/c仅编译源文件而不进行链接/E只运行预处理器并输出预处理结果
目标平台
编译器选项功能描述GCC-march=arch生成指定架构的目标代码-mtune=cpu优化生成针对指定 CPU 的代码Clang-target target生成目标平台为指定的目标Microsoft Visual Studio/arch:arch生成指定架构的目标代码/favor:processor优化生成针对指定 CPU 的代码
后端选项
编译器选项功能描述GCC-Olevel指定优化级别,level 可以是 0 到 3,或者 s,g 等-flto启用链接时优化Clang-Olevel指定优化级别,level 可以是 0 到 3,或者 s,g 等-flto启用链接时优化Microsoft Visual Studio/Olevel指定优化级别,level 可以是 1 到 4/GL启用链接时优化
2.4链接选项
编译器选项功能描述GCC-o outfile指定输出文件名-llibrary指定链接时需要的库文件-Ldir指定库文件的搜索路径Clang-o outfile指定输出文件名-llibrary指定链接时需要的库文件-Ldir指定库文件的搜索路径Microsoft Visual Studio/out:outfile指定输出文件名/link启动链接器
2.5其他选项
基础信息
编译器选项功能描述GCC–version显示编译器版本信息–help显示编译器的使用帮助信息Clang–version显示编译器版本信息–help显示编译器的使用帮助信息Microsoft Visual Studio/?显示编译器的使用帮助信息
调试
编译器选项功能描述GCC-g生成调试信息Clang-g生成调试信息Microsoft Visual Studio/Z7生成调试信息
3编译优化
3.1过程间优化
过程间优化的目的是为了消除重复计算、内存的低效使用等方式,常用的一种方式是内联优化。通过将另一个函数内联的方式,减少了函数调用的开销。
3.2循环优化
包含循环交换、循环展开、循环对齐、循环分布、循环倾斜等手段,在五、程序编写时的优化(下):循环优化、语句优化的4循环优化中几乎都已经提到。
3.3自动向量化
向量化是一种并行计算的方式,可以同时对多个数据执行相同的操作。编译器LLVM支持两种自动向量化的方式,第一种是循环级向量化,对循环结构进行向量化优化;第二种是基本块级优化,将多个相同操作的标量合并为向量进行优化运算。
3.4数据预取优化
数据预取的意义,在六、访存时的优化(上):寄存器优化、缓存优化、内存优化的2缓存优化中已经介绍了。编译器也会使用内建的预取函数,插入在原有代码中,对代码进行数据预取优化。
3.5浮点优化
以下给出三种浮点优化方式:
- 浮点数据归约优化(Floating-Point Data Reduction Optimization)
这种优化主要用于对浮点数数组的规约操作,如对数组元素进行求和或累乘操作。编译器会尝试将这些操作优化为一个单一的累加或累乘操作,以减少循环内部的浮点数操作次数,提高运行效率。
- 除法运算优化(Division Operation Optimization)
这种优化主要用于改善浮点数除法运算的性能。除法操作通常比乘法和加法操作消耗更多的计算资源,编译器在这种优化中可能会尝试将除法操作转换为更高效的乘法、移位或其他方式实现,以提高程序的性能。
- 忽略浮点数0的正负号(Ignoring the Sign on Floating-Point Zero)
这种优化可以避免在除以0的情况下出现浮点数正负号的异常情况。在一些处理器架构中,当浮点数除以0时,会产生正无穷大(+INF)或负无穷大(-INF)。启用这个优化选项可以让编译器将除以0的操作优化为生成一个正负号正确的无穷大结果,而不是抛出异常或错误。
3.6反馈优化
这个很有意思,指的是在编译的过程中插入一些检测语句,在可执行文件运行时生成检测文件,以便重新编译的过程中进行优化。
| 实践是检验真理的唯一标准
以code.c文件的编译举个例子
- 生成可执行文件并收集性能数据:
gcc -O2 -o code code.c
./code <test_case1>
./code <test_case2> ...
在这个步骤中,我们使用 GCC 编译器对
code.c
进行编译,添加
-O2
选项进行中级别优化,生成可执行文件
code
。然后我们执行一系列测试用例,例如
test_case1
、
test_case2
等,来收集程序在不同输入下的性能数据。
- 根据性能数据进行优化:
gcc -O2 -o code_optimized code.c
在这个步骤中,我们再次使用 GCC 编译器对
code.c
进行编译,同样添加
-O2
选项进行中级别优化,但这次生成的可执行文件命名为
code_optimized
。编译器在这个过程中会利用之前收集到的性能数据进行反馈优化,以提升程序的性能。
- 执行优化后的程序:
./code_optimized
最后,我们执行优化后的程序
code_optimized
,以验证优化效果是否符合预期。
3.7编译指示
指的是向编译器传达特定的指令或信息的语句。
- #pragma once:确保头文件只被编译一次,可以提高编译效率。
#pragmaonce #include "header.h"
- #pragma comment:用于向链接器传达特定信息,例如链接特定的库文件。
#pragmacomment(lib,"libname.lib")
- #pragma pack:控制结构体的字节对齐方式。
#pragmapack(1)structMyStruct{int a;char b;};
- #pragma message:向编译器输出自定义消息。
#pragmamessage("This is a custom message")
- #pragma startup 和 #pragma exit:在程序开始前和结束后执行特定函数。
#pragmastartup myStartupFunction #pragmaexit myExitFunction voidmyStartupFunction(){// 在程序开始执行前调用 }voidmyExitFunction(){// 在程序结束后调用 }
- #pragma warning:控制编译器警告的输出行为。
#pragmawarning(disable: unused-variable)
- #pragma region 和 #pragma endregion:用于折叠代码段,便于代码阅读。
#pragmaregion Utility Functions intadd(int a,int b){return a + b;}8#pragma endregion
- #pragma omp simd:用于指示编译器对循环进行向量化并行处理。
#pragmaomp simd for(int i =0; i < N; i++){// 循环体 }
- #pragma unroll:用于指示编译器对循环进行展开。
#pragmaunroll for(int i =0; i < N; i++){// 循环体 }
4数学库优化(日更好累,前面的区域下一篇再来探索吧)
5运行时的优化(日更好累,前面的区域下一篇再来探索吧)
专栏安排(已有,或将有)
一、程序性能优化的意义
二、程序性能的度量指标
三、程序性能优化流程
四、程序性能的测量和分析
五、程序编写时的优化(上):算法优化、数据结构优化、函数优化
五、程序编写时的优化(下):循环优化、语句优化
六、访存时的优化(上):寄存器优化、缓存优化、内存优化
六、访存时的优化(下):磁盘优化、数据分布
七、编译与运行时的优化(上):编译器结构、编译选项、编译优化
七、编译与运行时的优化(下):数学库优化、运行时的优化
八、系统配置的优化
九、单核优化
十、OpenMP程序优化
十一、MPI程序优化
十二、…
如有不足之处,敬请批评指正
更欢迎在评论区留下你的见解,你的方法,如果有效我会增加在文章中,并@你。
版权归原作者 准时睡觉的雨繁 所有, 如有侵权,请联系我们删除。