---
title: 编译器调优手段
description: "编译器优化选项能够在编译阶段完成代码的优化，下面列举了常用的优化编译选项及其优化原理，在一些场景下能够大幅提高程序的运行性能。"
url: https://www.hikunpeng.com/document/detail/zh/perftuning/progtuneg/kunpengprogramming_05_0033.html
sourcePath: /source/zh/perftuning/progtuneg/kunpengprogramming_05_0033.html
indexId: d32fa8625c9eb54f163dcf53dc812add7c0d92c5af6d851098d44836f2adde7663
---
# 编译器调优手段

编译器优化选项能够在编译阶段完成代码的优化，下面列举了常用的优化编译选项及其优化原理，在一些场景下能够大幅提高程序的运行性能。

#### 指令集和流水线

C/C++代码在编译时，编译器将源码翻译成CPU可识别的指令序列，写入可执行程序的二进制文件中。CPU在执行指令时，通常采用流水线的方式并行执行指令，以提高性能，因此指令执行顺序的编排将对流水线执行效率有很大影响。通常在指令流水线中要考虑：执行指令计算的硬件资源数量、不同指令的执行周期、指令间的数据依赖等因素。我们可以通过通知编译器，程序所运行的目标平台（CPU）指令集、流水线，来获取更好的指令序列编排。

在GCC 9.1.0版本，支持了鲲鹏处理器所兼容的Armv8指令集、流水线。

使用方法：

在GCC for openEuler编译器、毕昇编译器、GCC编译器高于9.1.0版本上，并在CFLAGS、CPPFLAGS里增加编译选项：

```
-mtune=tsv110 -march=armv8-a
```


#### 优化等级

编译器使用-O优化等级来控制程序的优化程度，具体如表1所示。


**表1 优化等级说明**

| 优化等级 | 说明 |
| --- | --- |
| \-O | 缺省为\-O0，不添加任何优化选项，具有最快的编译速度，且程序更有利于调试。 |
| \-O1 | 添加常用的优化选项。 |
| \-O2 | 相比于\-O1，添加更多优化选项。 |
| \-O3 | 最高级别的优化层次，编译时间较长，生成执行速度更快的程序。 |
| \-Ofast | 添加跟\-O3一样的优化选项，还增加一些非标准的优化选项。 |
| \-Os | 添加跟\-O2一样的优化选项，还增加一些选项去减小程序代码。 |
| \-Og | 增加调试信息。 |


GCC使用gcc -Q --help=optimizers -O2可以查看各个优化等级选择的优化选项。


编译器有着众多的优化选项，下面列举一些例子，编译器能够优化的场景。

- 函数内联：
  可以通过编译选项-finline-functions控制开启，在-O2、-O3和-Os优化等级下默认开启，函数内联能够减少函数的调用和返回开销，提高指令Cache命中率。还能节省代码空间，给其他优化措施提供机会。

  优化前：

```
int funA (int a) {
return a * a;
}
int funB (int b) {
return funA(b) + 1;
}
```


  优化后：

```
int funB (int b) {
return b * b + 1;
}
```


- 常量传播和常量折叠：
  通过-fdevirtualize、-fipa-cp、-fipa-cp-clone、-fipa-bit-cp、-fipa-vrp、-ftree-bit-ccp、-ftree-ccp、-ftree-dominator-opts、-ftree-vrp这些编译选项控制开启，在-O2、-O3或者-Os下默认开启。在编译阶段，能够直接计算出结果的变量或者多个变量的计算结果能够用常量来替代。节省运行时的计算时间。

  优化前：

```
int funA (int a) {
return a * a;
}
int funB () {
int a = funA(2);
int b = a + 1;
return b;
}
```


  优化后：

```
int funB () {
return 5;
}
```


- 消除公共表达式：
  通过-fgcse、-fgcse-lm、-fgcse-sm、-fgcse-las、-fgcse-after-reload编译选项控制，在-O2、-O3或者-Os下默认开启。同样是在编译时期优化计算过程，减少运行时的计算量。

  优化前：

```
int a, b, c;
b = (a + 1) * (a + 1);
c = (a + 1) / 2;
```


  优化后：

```
int a, b, c, tmp;
tmp = a + 1;
b = tmp * tmp;
c = tmp / 2;
```


- 循环展开：
  可通过-funroll-loops、-funroll-all-loops编译选项控制开启。在循环次数较少的场景，编译器会将循环体的代码多次复制来消除循环控制的开销，同时更有利于数据预取，提高Cache命中率。

  优化前：

```
int a[4];
for (int i = 0; i < 4; ++i) {
a[i] = i;
}
```


  优化后：

```
int a[4];
a[0] = 0;
a[1] = 1;
a[2] = 2;
a[3] = 3;
```


- 循环中不变代码外提：
  通过-fmove-loop-invariants编译选项控制，在高于-O1优化等级下默认开启，优化后减少循环体重复计算量。

  优化前：

```
int a[100];
void funA(int b) {
for (int i = 0; i < 100; ++i) {
a[i] = b * b + i;
}
}
```


  优化后：

```
int a[100];
void funA(int b) {
int tmp = b * b;
for (int i = 0; i < 100; ++i) {
a[i] = tmp + i;
}
}
```


- 归纳变量：
  通过-fivopts编译选项控制开启，默认开启。循环中的一个变量其值在每一次循环迭代过程中增加（或减少）固定的值，可以实现用加法替换乘法，提高计算速度。

  优化前：

```
int a[100];
for (int i = 0; i < 100; ++i) {
a[i] = i * 9 + 3;
}
```


  优化后：

```
int a[100];
int tmp = 3;
for (int i = 0; i < 100; ++i) {
a[i] = tmp;
tmp += 9;
}
```


- C++中虚函数的调用是在查询虚表后，通过虚表中的函数指针进行函数调用：
  如果编译器能够确定实际调用哪个虚函数，即可采用直接调用方法进行调用，减少调用开销。通过-fdevirtualize编译选项控制开启，在-O2、-O3或者-Os下默认开启。示例代码：

```
class C0 {
public:
virtual void funA ();
};
class C1 : public C0 {
public:
virtual void funA ();
};
int main () {
C1 c1;
C0* c = &c1;
c->funA();
return 0;
}
```


  优化前，通过虚表调用：

  优化后，直接调用：

- 向量化：
  编译时将代码向量化，会自动利用NEON属性，NEON是一种基于SIMD思想的技术，能够基于单条指令对多个数据同时进行操作。GCC编译器使用-O3会自动使能-ftree-vectorize选项，在-O1和-O2下需要添加-ftree-vectorize选项才能进行向量化。在-O0模式下，即使添加-ftree-vectorize也无法进行向量化。

  实例代码：

```
int a[64 * 4];
int b[64 * 4];
int c[64 * 4];
int main ()
{
for (int i = 0; i < 64 * 4; i++) {
c[i] = a[i] + b[i];
}
return 0;
}
```


#### PGO

PGO（Profile-Guided Optimizations）是通过收集程序运行时的信息（Profile）进行优化决策。PGO需要两次编译运行，第一遍编译过程中，编译器会在程序中插入一些获取程序运行特征的函数或者指令，然后使用第一遍编译的程序运行，程序运行期间会将特征信息保存在文件中。在第二次编译过程中，首先读入第一次程序运行保存的程序特征文件，编译器根据这些运行特点指导各种编译优化技术进行优化决策，生成目标程序，然后用于性能测试。

PGO支持两种基于反馈信息的优化技术。一种方式采用编译器插桩、运行、反馈编译的流程；另外一种方式和系统的perf工具一起使用，不需要编译器插桩，通过perf工具运行程序、收集信息、反馈编译。下面介绍下编译器插桩的PGO使用方法，使用perf分析工具的PGO方法可以参考官网链接：

- GCC：  https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options(https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options)

- Clang：  https://clang.llvm.org/docs/UsersManual.html#profile-guided-optimization(https://clang.llvm.org/docs/UsersManual.html#profile-guided-optimization)


GCC使用方法

1. 添加-fprofile-generate编译选项，打开插桩应用，生成profile信息的开关。
  1 gcc -O2 -fprofile-generate vec.cpp

2. 运行程序，生成profile信息，即gcda文件。
  1 ./a.out

3. 添加-fprofile-use编译选项，使用profile信息重新编译程序。
  1 gcc -O2 -fprofile-use vec.cpp


Clang使用方法

1. 添加-fprofile-instr-generate编译选项，打开插桩应用，生成profile信息的开关。
  1 clang -O2 -fprofile-instr-generate vec.cpp

2. 运行程序，生成profile信息，文件名默认为default.profraw。
  1 ./a.out

3. 使用llvm-profdata工具将default.profraw转换成Clang能够识别的profile文件格式。
  1 llvm-profdata merge -output=code.profdata default.profraw

4. 添加“-fprofile-instr-use”编译选项指定profile信息重新编译程序。
  1 clang -O2 -fprofile-instr-use=code.profdata vec.cpp


PGO具体优化内容：

- 寄存器分配：非反馈编译一般采用某种静态启发式寄存器分配算法，会尽量让变量的值或计算结果保留在寄存器中。而PGO会合理利用寄存器，使用优先级驱动的寄存器分配方法，根据基本块的执行频率来确认优先级，保证经常使用的变量优先分配到寄存器。
- 冷热分区：编译器在不使用PGO时候，也会根据程序结构静态地进行冷热分区，但是不够准确。而通过profile信息准确记录BB（Basic Block）块的调用频率，可以更加精准地划分冷热块。然后进行BB块的优化，包括循环展开、函数内联等。还用于重排BB块，将冷区BB块放到远区，将热区BB块集中在一起，有利于提高指令Cache利用率。
- 函数重排：源码的函数定义顺序决定代码段里的函数顺序，而代码段的函数顺序决定加载到内存里函数顺序，这样冷热函数是混合在一起的。编译器会根据profile信息，获取函数调用关系，在代码段里根据调用栈顺序重排函数顺序，将冷函数剥离到代码段尾部，热函数按照函数调用栈排布，减少跳转指令开销和提高Cache命中率。
- 分支重排：if/else、switch/case等条件跳转语句预测失败会导致Cache miss，PGO通过插桩采集各分支概率，来调整分支调用顺序，从而降低Cache miss
- 其他：在  优化等级
提到的函数内联、常量传播和常量折叠、循环展开等优化手段。


#### LTO

LTO（Link Time Optimization）是链接期间的程序优化，将多个中间文件合并在一起，形成一个全局调用图，从而进行全程序的优化，链接时优化是对整个程序的分析和跨模块的优化。

添加-flto编译选项即可打开LTO优化。因为LTO是在编译后的优化，因此可以解决多个.o文件互不感知的优化问题，可以在全局上对整个程序进行优化，优化内容参考优化等级。例如：全局的函数内联优化，比单个.o文件的内联优化更加全面；无用代码消除，由于跨文件原因，无法判断代码是否被调用，而LTO则可以确定是否存在无用代码，减小代码体积。

需要注意的是，LTO在改善程序性能的同时也带来了编译时间过长，编译时内存占用变高的问题。为了减少开启LTO带来的编译时间太长的问题，LLVM提出了ThinLTO技术，可以大幅降低编译时间，在LLVM编译器下增加-flto=thin即使用的是ThinLTO优化。


#### AutoTuner

AutoTuner是一种自动化的迭代过程，通过操作编译选项来优化给定程序，以实现最佳性能。它由两个组件配合完成，毕昇编译器和AutoTuner命令行工具。

AutoTuner调优流程分两个阶段：初始化编译阶段和调优阶段。


在初始化阶段，毕昇编译器增加-fautotune-generate编译选项进行一次编译，在编译的过程中，毕昇编译器会生成一些包含所有可调优结构的YAML文件，告诉我们在这个目标程序中哪些结构可以用来调优。

在调优阶段，AutoTuner首先读取生成好的可调优结构的YAML文件，从而产生对应的搜索空间，然后根据设定的搜索算法尝试一组参数的值，生成一个YAML格式的编译配置文件，编译出目标程序二进制，最后AutoTuner将编译好的文件以用户定义的方式运行并取得性能信息作为反馈。经过一定数量的迭代之后，AutoTuner将找出最终的最优配置，生成最优编译配置文件，以YAML的形式储存。

AutoTuner目前有两种使用方式，并对应两种不同的命令行工具llvm-autotune和auto-tuner。具体使用方法参考《毕昇编译器 Autotuner 特性指南(https://www.hikunpeng.com/document/detail/zh/kunpengdevps/compilation/ug-bisheng/kunpengbisheng_20_0002.html)》。
