# 毕昇编译器

#### 毕昇编译器简介

毕昇编译器是基于LLVM开源软件开发的一款用于C/C++等语言的native编译器，能将C/C++代码工程编译链接成可以在设备上运行的二进制。在无需改动用户代码的条件下，相比业界主流的开源LLVM或GCC编译器，毕昇编译器能提供更强大的优化能力，使编译链接出来的二进制的运行时长更短、指令数更少，帮助提升应用在设备上的运行流畅度。  

#### 能力范围

毕昇编译器提供将C/C++代码工程编译链接成可以在设备上运行的二进制的基本能力，主要包括以下三方面：

* 编译能力：将C/C++源码文件编译成汇编文件，汇编文件是指使用汇编语言编写的文件。

* 汇编能力：将汇编文件汇编成可重定向文件，可重定向文件是ELF格式的二进制文件，但不能直接放在设备上运行。

* 链接能力：将一个或多个可重定向文件一起链接成一个可执行的二进制文件。

备注： 毕昇编译器基于开源LLVM开发， LLVM的常见问题，请参考开源官方文档https://llvm.org/docs/index.html。  

#### 亮点特征示例

毕昇编译器相对于LLVM/GCC编译器有以下特点。

* 毕昇编译器Loop Distribution优化增强示例

针对循环相关的编译优化，毕昇编译器在场景识别、结构变换等方面做了改进和增强。例如在社区LLVM已有的Loop Distribution优化上，毕昇编译器相比开源LLVM编译器，能额外识别出循环内不同代码块间数据依赖关系、以及不同代码块运行的迭代次数差别，从而能对更多的循环进行loop distribution优化。

![](https://media:401788444123583006)

* 毕昇编译器矢量化优化增强示例

毕昇编译器在矢量化优化方面，相比开源LLVM编译器，不仅能将更多的循环做矢量化转换，还在矢量化指令选择上更高效。例如下面示例中，开源LLVM编译器虽然做了矢量化，但使用了5条矢量指令；而毕昇编译器只需要使用2条矢量指令，最终产生的二进制效率更优。

![](https://media:401788444123615007)  

#### 毕昇编译器使用指导

在DevEco Studio 中使用毕昇编译器：

* 开发者获取或在线升级到DevEco Studio 5.1.1 release及之后的版本，新建C/C++工程默认使用毕昇编译器，打开C/C++老工程有弹窗提示，点击Try Now可以切换使用毕昇编译器，构建HarmonyOS工程的C/C++代码。

![](https://media:401788444123649008)

* 开发者获取DevEco Studio 5.1.1 beta及之前的版本，默认使用还是开源llvm编译器，需要在HarmonyOS应用的工程级build-profile.json5中简单配置即可使用毕昇编译器：在runtimeOS为HarmonyOS的时候，设置nativeCompiler为BiSheng，即可使用毕昇编译器构建HarmonyOS工程的C/C++代码。

![](https://media:401788444123689009)

此外，当开发者自己单独配置流水线切换毕昇编译器时，可能会遇到"找不到头文件"错误，此时添加sysroot路径选项即可解决（xxx为sdk的路径）：

--sysroot=xxx/sdk/default/openharmony/native/sysroot

在流水线中使用毕昇编译器：

若在流水线中直接使用SDK构建，不通过DevEco构建，可以使用下述方式使用毕昇编译器：

* 找到毕昇编译器所在路径：

在DevEco的安装目录下，会有hms和HarmonyOS两个目录：

![](https://media:401788444123730010)

毕昇编译器所在的路径为：xxx/sdk/default/hms/native/BiSheng。

* 替换毕昇编译器

替换编译器有2种方式，命令替换和目录替换。

（1）命令替换：

如果脚本中有指定使用的编译器路径，如TOOLCHAIN_PATH="xxx/sdk/default/openharmony/native/llvm/bin/clang"，则可以直接将其替换为"xxx/sdk/default/hms/native/BiSheng/bin/clang"。

注意：若编译器路径为拼凑路径，且会影响到sysroot的路径，要确保sysroot路径仍指向HarmonyOS下的sysroot。

（2）目录替换：

该方法最为简单，不需要改动任何脚本或者环境变量，只需要将目录做一次拷贝，步骤如下：

1） 将xxx/sdk/default/openharmony/native下的llvm重命名为 llvm-bak(以便后续恢复，也可直接删除)；

2） 拷贝xxx/sdk/default/hms/native/BiSheng 文件夹到 xxx/sdk/default/openharmony/native目录下；

3） 将BiSheng重命名为llvm。

做完上述步骤，重启流水线即可用毕昇编译器构建。  

#### 主要编译优化特性

* IClang

IClang是一种增量编译优化手段。该特性在增量编译的基础上，进一步实现函数级别的编译复用，利用上一次的编译结果，显著提升编译速度。

1. 启用指导

编译时，需要增加-iclang选项。以CMake构建系统为例，可在 CMakeLists.txt 中配置：add_compile_options(-iclang)

2. IClang维护的数据

IClang 会在目标文件所在目录下自动生成一个 .iclang 子目录，用于存储其运行所需的数据。例如，编译 test.o 时，会创建 test.o.iclang 目录。

3. IClang的编译模式

由于用户通常仅修改部分源文件而非整个项目，故IClang仅会为需要频繁修改的源文件生成可复用的缓存。具体而言，IClang的编译可以分为以下三种模式：

（1）初始模式：在首次编译-iclang时，仅维护一些非常轻量的元数据，从而避免对用户的全量编译性能造成影响。

（2）预热模式：当用户首次对某个文件进行修改并重新编译时，IClang会认定其为高频修改目标，并执行一些必要的分析与缓存构建。该模式可能会略微影响用户的首次增量编译性能，相关产物存储在.iclang中。

（3）增量编译模式：完成预热后的文件，用户后续对其修改之后的增量编译都可以得到加速，非常适用于频繁修改某个源文件的场景。

4. 缓存失效的情况

（1）上次编译生成的二进制产物被删除。此时IClang将无法实现二进制函数复用，因此回退到初始模式。

（2）头文件内容或配置发生变化。此时IClang将无法判断哪些函数可以被安全复用，因此回退到初始模式。

（3）编译命令发生变化。此时构建系统会重新编译整个项目，IClang保持与构建系统的行为一致，回退到初始模式。

当IClang发生内部错误时，会在相关产物下生成.iclangtmp目录，检测到该目录存在后IClang将进入恢复模式，完全退回到Clang，确保用户可以正常编译。同时，应将这一行为报告给IClang开发者进行确认和修复。如有需要，用户也可以尝试自行删除.iclangtmp目录，此时IClang将会重新回到初始模式。

5. 编译选项限制

（1）IClang仅会作用于-c编译，且命令只有一个输入文件和一个输出文件，常见的构建系统（如CMake）都可以满足这个条件。

（2）IClang目前仅支持-O0优化级别选项，用户需要注意仅在debug模式的开发环境下使用。如果检测到其他优化级别选项，IClang将回退到Clang并提示：\[IClang Warning\] Please ensure that IClang is only enabled under -O0。

（3）IClang不支持和-flto选项共同使用。

（4）为了确保二进制复用的正确进行，IClang会添加-ffunction-sections与-fdata-sections两个编译选项，按符号划分二进制文件的代码段和数据段。

6. 编译前端限制

IClang在优化过程中，出于正确性考虑会选择不对包含某些语法的函数进行优化，包括：数组实例化类型、函数指针实例化类型、虚函数、UniqueExternalLinkage。

7. 编译后端限制

IClang生成的二进制产物遵循X86-64 ELF规范，在此基础上额外提出以下限制：仅支持64位小端序；二进制文件中仅包含Rela重定位表，不支持Rel重定位表；链接器采用lld，不支持gnu ld。

* PGO（Profile Guided Optimization）

PGO是一种自适应优化手段。它通过收集代码在实际运行过程中的性能数据，来准确得知例如哪些函数是真正被频繁执行的、哪些分支是真正频繁进入的等信息，从而指导编译器做出相应优化。相较于传统的PGO，毕昇PGO具有更强的准确性和优化性。

毕昇编译器通过信号量触发文件写入操作：Dso在接受到特定信号量时将采样信息写入文件中，并清空相应计数器。 其主要流程如下图所示：![](https://media:401788444123762011)

这种方案能够精准的控制采样范围，解决了传统PGO依赖业务中存在的统一入口和出口，无法对某一段范围进行精准采样的痛点。

毕昇PGO使用和开源LLVM一样，优化流程分为插桩阶段、采集阶段和优化阶段。

1. 插桩阶段

编译时，需要增加选项：-fprofile-generate=/data/storage/el2/base/files，该路径为沙箱路径，会在其映射路径下产生对应的文件，映射路径为：/data/app/el2/100/base/应用名/files。

可以通过检查编译后文件是否有llvm_prf字段或反汇编指令序列来判断是否插桩成功。

```
llvm-objdump -h xx.so  # 检查是否有llvm_prf字段
llvm-objdump -d xx.so  # 看反汇编是否有ldr add store代码序列
```

2. 采集阶段

通过lldb方式进行profile数据采集。

编译完成后，在Deveco中用debug模式推送APP包：

![](https://media:401788444123800012)

启动后会出现debug窗口，确保是Native的debug模式，未出现则需要修改Deveco的debug Type配置为Native。

![](https://media:401788444123832013)

![](https://media:401788444123861014)

启动完成后，找到native debug的lldb位置，需要在lldb中设置参数，保证业务正常运行，不会在debug模式下被一些信号影响数据采集（信号一般是程序本身发送），设置方法为：

![](https://media:401788444123900015)

由于程序没有设置断点，导致程序暂停的原因是信号量，可以屏蔽信号量暂停机制：

查看有哪些信号量：

(lldb) process handle

屏蔽所有信号量暂停效果：

(lldb) process handle SIG\* -s false

SIG\*为信号量name, 若要一次性屏蔽所有信号量，则无需指定信号量名。

做完上述动作，直接发送信号量：

(lldb) process signal SIGUSR2

可以在对应的目录(/data/app/el2/100/base/应用进程名/files)找到default\*_profile文件，每发送一次信号会产生一次文件。第一次发送采集到的数据为：应用启动到发送信号时间点的数据；第二次发送为：第一次发送信号量到第二次发送信号量期间的数据；以此类推。可以控制发送信号量时间，来采集我们需要场景的数据。 生成的文件没有权限删除，一次应用启动采集到的数据，是同名的，多次生成文件会覆盖，也可以保证采集的数据是目标场景的数据。

![](https://media:401788444123932016)

用hdc将该文件从手机拷贝到电脑上，使用与clang同级目录下的llvm-profdata.exe工具，执行 llvm-profdata merge --output=lib.profdata default_\*.profraw 得到lib.profdata。

3. 优化阶段

重新编译，增加编译选项-fprofile-use=lib.profdata, lib.profdata为合并后的配置文件。

* LTO（Link Time Optimization）

LTO是一种在链接阶段跨编译单元进行优化的编译器优化手段，旨在提升程序性能。毕昇LTO使用和开源LLVM一样，但是相比开源LLVM在指令预取、inline算法、thin lto funciton import 算法、plt-inline优化等做了增强优化，进一步提升了生成代码的执行效率。

1. 优化指导

为启用LTO，需在编译阶段添加 -flto 选项。以CMake构建系统为例，可在 CMakeLists.txt 中配置：add_compile_options(-flto)

2. 注意事项

-flto：默认为full模式，会显著增加编译时长，但性能会更优

-flto=thin：可修改为 thin lto 降低编译时长，但性能不及full lto，两者相对平衡

若构建时间还显著增加，指定并行任务数加速构建，-flto-jobs=xxx, xxx根据电脑核数设置

* fp优化

fp优化是一组控制浮点运算语义和行为的编译器选项，它用于控制浮点表达式中的运算顺序是否可以被重排或合并，从而达到更高的性能或更强的可预测性。毕昇编译器默认关闭该优化，如果浮点计算场景较多，可以打开该优化

1. 优化指导

在编译阶段加入 -ffp-contract=on 选项。以CMake构建系统为例，在 CMakeLists.txt 中配置：add_compile_options( -ffp-contract=on)  
