编译过程与编译优化基础概念:以C语言为例

编译流程

编译是指将某一种语言(源语言)写的程序(源程序)翻译成一个等价的、用另一种语言(目标语言)写的程序(目标程序)的过程。通常,目标程序是一个可执行的机器语言程序,在编译后可以被用户 调用、处理输入并产生输出。常见编译器的流程(目标语言为机器语言)如下图所示:

暂时无法在飞书文档外展示此内容

  • 词法分析负责解析关键字、常量等,将源代码程序中的最基本元素提取出来,如int a = 1可以提取出来int, a, =, 1,并记录成一个词表。
  • 语法分析负责基于源语言的语法,将词表进一步翻译成抽象语法树(见第三章)。语法错误也是在这一过程中被编译器提示出来的。

常见C语言编译工具链

Clang/LLVM

LLVM是一个通用的编译优化框架,提供了一个现代化、精心设计的语言无关的中间语言(LLVM IR)。LLVM可以为任意编程语言设计编写前端、为任意机器指令集架构编写后端(x86/64,Arm,Power PC

,MIPS,RISC-V等),且由于其优秀的软件工程流程、严格的代码审查和活跃的社区,大量的编程语言基于LLVM实现了自己的编译器,如Rust、Swift等。

Clang([k-laeng], [k-lang])是基于LLVM的C/C++编译器,目前被用作macOS的自带C/C++编译器。

GCC

GCC是GNU Compiler Collection的缩写,是历史最悠久、开发最活跃的C/C++编译器之一。由于出色的优化和长时间的维护,拥有卓越的性能,并支持绝大多数指令平台。然而,尽管GCC是开源的,但文档很难阅读,源代码也很难使用。因此,GCC开发仅限于一小群精英GCC开发人员,他们高度关注特定领域的知识。GCC目前是大部分Linux和Unix发行版的默认C/C++编译器。

ICC/ICX

Intel提供的商用付费C/C++编译器。其实现和IR是完全闭源的,但是IR部分正在逐步过渡到LLVM IR上。现代化的ICC现在被称为ICX。ICC仅支持Intel架构(x86、x64、安腾),可在Linux、Windows和MacOS平台下使用。

MSVC

  1. 微软为Windows平台(x86 /arm-Intel/MIPS/ALPHA/PowerPC)开发的闭源C/C++编译器,由于年代久远,MSVC的代码库陈旧且难以维护。曾经有人试图将MSVC迁移到使用LLVM IR,但该尝试主要由于无法控制的复杂性而失败。MVSC仍然是Windows平台上的主要工具链。

其它

其它优秀的C语言编译器包括IBM XLC、Cray、SunCC、Open64、Watcom C/C++、Borland/turbo等。部分已经停止维护。

抽象语法树 Abstract Syntax Tree

抽象语法树是使用树的形式对源代码语法结构的一种抽象表示,树上的每一个节点都表示源代码中的一种结构。

以下面这个简单的程序为例,

其AST为:

在Clang中,该AST的数据结构被表示为:

从图中可以看出来,Clang抽象的AST的节点包括节点类型(如类型声明、函数声明、语句、表达式等)、唯一标识符、对应源代码位置、数据类型、备注信息、源代码中的字符串等。

LLVM IR

LLVM IR是一种基于SSA(Static Single Assignment)的中间代码表示。其整体的文档参见:llvm.org/docs/Passes...

SSA(静态单赋值)

在基于SSA的IR中,每个变量仅被赋值一次,相较于原始的IR或源代码程序,原来的变量会被分割成不同的版本。例如:

ini 复制代码
y = 1;
y = 2;
...
x = y;

在SSA表示中,会被翻译成:

ini 复制代码
y_1 = 1;
y_2 = 2;
...
x_1 = y_2;

从这个例子中我们可以看出,使用SSA的优点包括:

  1. y_1没有被引用,可以直接删除。当y_1包含比较复杂、耗时的计算任务时,消除y_1相关的运算可以提升产物的运算速度。
  2. x_1只被y_2影响,当y_2已知时,针对x_1的计算可以被优化(见下面的constant folding和constant propagation)。

LLVM IR Example

在上面的例子中,我们可以使用LLVM提供的工具链获得其LLVM IR。

r 复制代码
clang -S -emit-llvm branch.cpp -c

基于LLVM工具链提供的命令行程序或API,我们可以将源程序转换为LLVM IR,分析/操作IR,并将IR编译为机器语言(可执行程序)。参见:llvm.org/docs/Gettin...

编译优化基本概念与LLVM的优化pass

LLVM的优化主要是源语言无关的LLVM IR层面的优化,此时,源语言程序已经被对应的前端翻译成LLVM IR,优化的流程如下图:

暂时无法在飞书文档外展示此内容

如图所示,LLVM的优化指令按照功能和目的,被分为了一个个相互独立的优化pass,IR在经过若干轮pass的优化后,最终可以由后端输出为目标程序。

根据pass的特点,pass可以被分为:

  1. 分析 - Analysis pass:并不对IR进行变动性的操作,通常用于过一遍IR来收集debug、优化、可视化信息。
  2. 变换 - Transformation pass:对IR进行修改,以达到优化、插桩或其他目的。
  3. 工具 - Utility pass:其他类型的pass,包括从模块中提取所有basic block的信息、查看函数的控制流图等。

在这里我们介绍一些常用的pass,并通过部分pass介绍其他一些编译优化、静态分析常用的基本概念。为了方便表达,我们使用C源代码作为例子,而非LLVM IR。

分析类

调用图(Call Graph)与dot-callgraph pass

Call graph即函数调用关系图,如Code Graph产品。一个例子为:

LLVM中提供了不同的生成call graph的analysis pass,dot-callgraph,可将文件的call grpah生成到dot文件中,并进一步可视化。

Call graph生成的原理也较为简单,只需要遍历声明函数和调用函数的IR。将所有声明的函数作为全集,在遍历调用函数的IR时,函数caller为IR所属的函数、callee为被调用的函数,在全集中为他们之间建立连线即可。

控制流图(CFG)与dot-cfg pass

控制流图研究函数内各个basic block(即不包含控制流的最基本的代码块)之间的控制关系。例子(搬运自:www.geeksforgeeks.org/software-en...

如果对计算并生成CFG感兴趣,可以参考我之前为区块链Solidity语言编写的CFG生成程序:github.com/chao-peng/S...

变换类

Dead Code与dce(Dead Code Elimination)pass

Dead code即确定代码执行过程中不会被覆盖的代码,一个极端的例子为:

arduino 复制代码
#def X 10
if (X != 10) {
    // dead code here
}

通过数据流和控制流分析,我们知道X永远不会不等于10,因此这一块代码可以被直接优化掉。

类似的还有dead global elimination(优化掉没有被引用的全局变量)。

函数内联 inlining

Inlining / inline expansion即将函数体直接展开到调用处。可以消除调用函数的开销(压栈、保护/恢复现场),但可能造成产物体积膨胀,影响指令缓存的命中率。有研究表明函数内联展开在缓存小的时候能提升性能,缓存较大的时候性能有可能下降。

举个例子:

scss 复制代码
int pred(int x) {
    if (x == 0)        
        return 0;
    else        
        return x - 1;
}

int func(int y) {
    return pred(y) + pred(0) + pred(y+1);
}

在inlining后:

ini 复制代码
int func(int y) {
    int tmp;
    if (y == 0) tmp  = 0; else tmp  = y - 1;
    if (0 == 0) tmp += 0; else tmp += 0 - 1;
    if (y + 1 == 0) tmp += 0; else tmp += (y + 1) - 1; 
    return tmp;
}

合并冗余指令、constant folding(常量折叠)、constant propagation(常量传播)

这一类优化指令基于简单的算术运算,对编译时已知的数值进行相应的优化。

Constant folding指的是当算数运算为已知数(常数)时,可以在编译时直接计算,并将结果直接赋值到变量中。constant propagation指的是通过数据流分析,确定前面已知的常量可以传播到后面的表达式中,并进行进一步的常量折叠。

一些例子:

ini 复制代码
// 例子1:常量传播 + 折叠 + 逻辑运算优化
bool a = TRUE; // 或 bool a = FALSE;
bool x = a && b;
// 若a为已知值,b取决于输入,当a为TRUE时优化为:
bool x = b;
// 若a为已知False
bool x = FALSE;

// 例子2:使用左移右移优化乘除法
int a = b + b;
// 首先,a = b + b 等价于 a = b * 2,✖️2操作又可以被进一步优化为
int a = b << 2;

Loop unrolling(循环展开)

循环展开是一种展开循环体、减少循环次数的优化方法。这种方法牺牲了程序大小来提升执行速度。如:

css 复制代码
for (i = 1; i <= 60; i++) {
    a[i] = a[i] * b + c;
}

如果对本循环展开两次,会被优化为:

css 复制代码
for (i = 1; i <= 58; i+=3) {
    a[i] = a[i] * b + c;
    a[i+1] = a[i+1] * b + c;
    a[i+2] = a[i+2] * b + c;
}

优化后,程序的循环次数是原来的三分之一。

针对code idioms的特殊定制化优化

Code idioms是指在编程开发中经常出现的一些代码结构,针对这些代码结构,LLVM社区的开发者们提供了数以千计的定制化优化pass。一个简单的例子为高斯求和。当识别到如下的代码结构时:

ini 复制代码
int sum = N;
for (int i = a; i <= b; ++i) {
    sum += i;
}

这段代码会被优化为

css 复制代码
int sum = X + (a + b) * (b - a + 1) / 2;
相关推荐
可均可可29 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_011 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj1 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-1 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
我是谁??2 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
发霉的闲鱼2 小时前
MFC 重写了listControl类(类名为A),并把双击事件的处理函数定义在A中,主窗口如何接收表格是否被双击
c++·mfc
小c君tt2 小时前
MFC中Excel的导入以及使用步骤
c++·excel·mfc
xiaoxiao涛2 小时前
协程6 --- HOOK
c++·协程