一、背景知识——编译优化与软硬协同
编译器:将源程序(高级语言)转换为等价的机器语言
- 源代码(source code)→预处理器(preprocessor)→编译器(compiler)→目标代码(object code)→链接器(Linker)→可执行程序(executables)
编译优化:保留程序语义(正确性)的前提下,对程序进行等价变化,从而较少程序运行时间
软硬协同:在编译优化中适配/使能诸多硬件特性
HPC( High Performance Computing,高性能计算)领域主要是解决计算密集型、海量数据处理等业务的计算需求,如科学研究、气象预报、计算模拟等。如何提高计算能力、极致化应用性能成为当前 HPC 领域各大平台最关键的课题之一,编译器在其中发挥着至关重要的作用。
二、认识毕昇编译器
毕昇编译器是基于开源LLVM 10.0.1版本开发,并进行了优化和改进,同时将flang作为默认的Fortran语言前端编译器,是一种Linux下针对鲲鹏920的高性能编译器,其针对鲲鹏平台进行了深度优化的高性能编译器。除支持 LLVM 通用功能和优化外之外,对中端及后端的关键技术点进行了深度优化,对以下三个方面进行了增强,使得鲲鹏平台的强劲算力能够最大限度地得到释放。
- 高性能编译算法:编译深度优化,内存优化增强,自动矢量化等,大幅提升指令和数据呑吐量。
- 加速指令集:结合 NEON/SVE 等内嵌指令技术,深度优化指令编译和运行时库,发挥鲲鹏架构极致算力。
- AI 迭代调优:内置 AI 自学模型,自动优化编译配置,迭代提升程序性能,完成最优编译。
关键特性
- 支持鲲鹏微架构芯片及指令优化
- 通过软硬协同提供相较开源LLVM更高的性能
- 集成Auto-tuner特性支持编译器自动调优
运行平台:鲲鹏920硬件平台
操作系统:openEuler21.03、openEuler 20.03 (LTS)、CentOS 7.6、Ubuntu 18.04、Ubuntu 20、麒麟V10、UOS 20
注:详细的运行平台和操作系统对应关系请参见 兼容性查询工具
部分通用信息请参考LLVM的用户指导https://llvm.org/docs/UserGuides.html
相比GCC和icc,LLVM前端Clang对语法的检查更严谨,严格匹配语言标准,Clang的常见兼容性和可移植性问题,请参考开源官方文档https://clang.llvm.org/compatibility.html
支持的编程语言
LLVM是一种涵盖多种编程语言和目标处理器的编译器,毕昇编译器聚焦于对C、C++、Fortran语言的支持,利用LLVM的Clang作为C和C++的编译和驱动程序,Flang作为Fortran语言的编译和驱动程序。
1、C,C++程序
Clang不仅仅是可以将C, C++程序编译为LLVM中间表示的IR,它也是一个驱动程序,会调用所有以代码生成为目标的LLVM优化遍,直到生成最终的二进制文件。毕昇编译器提供了端到端编译程序所需的所有工具和库。
2、Fortran程序
Flang是专为LLVM集成而设计的Fortran前端,由两个组件flang1和flang2组成。它也是一个驱动程序,将源代码转换为LLVM IR,前端驱动程序将IR传输下去进行优化和目标代码生成。
毕昇编译器优化效果:Benchmark跑分、HPC典型应用性能提升
当前毕昇编译器已广泛应用于多种 HPC 典型场景,如气象、安防、流体力学等,性能优势已初步体现。毕昇编译器与鲲鹏芯片协同,充分发挥芯片的性能,提升鲲鹏硬件平台上业务的性能体验。
基于鲲鹏上编译器优化,SPEC CPU 2017 benchmark 跑分平均优于 GCC 20%以上
HPC 典型气象应用 WRF 优于 GCC 10%~20%
三、毕昇编译器典型优化场景及其优化原理
1、结构体内存布局优化——大幅提升缓存命中率,突破访存瓶颈
SPEC CPU 2017 benchmark 中的 mcf 子项是对内存要求极高的应用,它是一款叫做MCF的大规模交通规划软件的核心代码。其瓶颈代码如下图左边所示。
将结构体数组转换为数组结构体
结构体可以是显式的,也可以通过检查循环中的数组使用情况来推断它们
可见在 struct 中,data1 的使用率极高,而 data2 是不使用的。然而由于源代码中,数据的排布是以结构体数组的形式排布。按照一般编译器的编译方式,拿数据时每次都会将整个结构体放到 cache 里面,导致大量不参与计算的 data2 也被加载到了 cache 中,造成高速内存空间的浪费和性能的损耗。毕昇编译器会通过用户标记的结构体声明,或者通过自动检查循环中适合优化的内存场景,确认优化点。然后通过将结构体数组变为数组结构体的方式(如上图右),将有效数据紧凑排布,从而提高 cache 命中率和应用性能。经测试,此优化可以对 mcf 子项带来50%的性能提升。
2、结构体指针压缩优化——大幅降低内存使用,提升缓存命中率
- 使编译过程更加灵活和可控
- 细粒度编译控制,提供更多优化机会
将指针成员由8字节压缩至4字节,减小每个结构体node的内存体积。被压缩的域成员指针外提为全局结构体指针ps_head,ps变换为相对基址的偏移,将有效数据紧凑排布,从而大幅降低内存使用,提升 cache 命中率和应用性能。
3、软件预取——大幅提高程序性能,提升缓存命中率
(1)软件预取:通过插入预取指令提前从内存中读取所需数据
(2)预取的效果取决于“提前量”
- 数据太早到达→浪费宝贵的cache空间
- 数据太晚到达→仍需要等待访存过程
(3)如何计算“提前量"
- 循环大小
- Cache line大小
- 访存延迟
(4)针对鲲鹏微架构特征调整软件预取参数
4、自动矢量化—计算效率提升的秘诀
使用矢量寄存器、矢量指令提高并行度
- 理论基础:single instruction multiple data,一条指令可以处理多个数据
- 硬件基础:ARM NEON指令集扩展,32个128位的矢量寄存器,指令可以同时操作4*32或2*64的数据
- 软件基础:编译器针对NEON指令的分析和优化
鲲鹏平台支持 Armv8 NEON 矢量化指令集。当前支持32个128位的矢量寄存器,指令可以同时操作4*32或2*64的数据。毕昇编译器依托这种硬件优势做了大量优化,包括 SLP(superword-level parallelism) 矢量化和循环自动矢量化。
例如在 SPEC CPU 2017 benchmark 中处理视频流格式转换的x264子项中,毕昇编译器会自动识别并使用 uabd 和 udot 这类高效向量指令完成计算来替换标量指令,增大单时钟周期的数据处理量, 从而大幅提升计算效率。对于 x264 子项,这项优化可有效提升其30%的计算效率。
5、循环优化——帮助发现更多的优化机会
Loop unroll:将循环体复制多遍
- 减少分支跳转次数
- 帮助发现更多的优化机会
Loop (partial) unswitch:外提(减少)循化内条件判断
Loop fusion/distribution:将循环体合成一个/拆成多个
6、Autotuner—基于机器学习快速获取最优编译配置
如何获取性能最优编译选项是编译器使用中常见的问题,往往需要长时间的手动选项调优。为了减少这其中的工作量,使得用户能快速找到最优的优化选项,毕昇编译器自研了基于 ML 的自动搜索技术(ML-based Search) 的 Autotuner 工具。
一种自动化的迭代过程, 通过操作编译设置来优化给定程序,以实现最佳性能。它由两个组件配合完成,毕异编译器和Autotuner命令行工具。此功能不需要在源代码中注入pragma,而是允许用户在简单的yaml文件中指定优化配置,该文件包含优化信息及其相应的代码区域信息,包括名称和行号。此外,它还可以记录优化结果,以及可调优的代码区间并以yaml的形式导出。
- 与毕昇编译器进行交互:
- 根据编译器产生的可调优代码结构创建搜索空间(search space)
- 生成编译配置并调用编译器来编译源代码
- 操作调优参数以及应用搜索算法
- 自带的遗传算法
- 获取性能数据
引入基于ML的自动搜索技术(ML-basedSearch)
关键技术点:
1、知识库(Autotuner Database):根据静动态分析信息,建立知识库,支持决策系统进行优化
2、优化决策系统(Optimal configuration):根据热点和性能评估信息、知识库信息,综合考虑确定优化措施
3、热点标记和性能评价:热点标记,瓶颈检测,性能评估
4、查找驱动(Feedback):将优化决策系统,反馈的优化建议
Autotuner 的调优流程由两个阶段组成:初始编译阶段(initial compilation)和调优阶段(tuning process),如下图所示:
初始编译阶段
初始编译阶段发生在调优开始之前,Autotuner首先会让编译器对目标程序代码做一次编译,在编译的过程中,毕昇编译器会生成一些包含所有可调优结构的YAML文件, 告诉我们在这个目标程序中哪些结构可以用来调优,比如文件(module), 函数(function), 循环(loop)。 例如,循环展开是编译器中最常见的优化方法之一,它通过多次复制循环体代码,达到增大指令调度的空间,减少循环分支指令的开销等优化效果。若以循环展开次数(unroll factor)为对象进行调优,编译器会在YAML文件中生成所有可被循环展开的循环作为可调优结构。
调优阶段
当可调优结构顺利生成之后,调优阶段便会开始:
- Autotuner首先读取生成好的可调优结构的YAML 文件,从而产生对应的搜索空间,也就是生成针对每个可调优代码结构的具体的参数和范围;
- 调优阶段会根据设定的搜索算法尝试一组参数的值,生成一个YAML格式的编译配置文件(compilation config),从而让编译器编译目标程序代码产生二进制文件;
- 最后Autotuner将编译好的文件以用户定义的方式运行并取得性能信息作为反馈;
- 经过一定数量的迭代之后,Autotuner将找出最终的最优配置,生成最优编译配置文件,以YAML的形式储存。
简单来说,在初始编译阶段,编译器会通过用户指定的调优方向,对可调优的代码区间进行标记。在随后的调优阶段,Autotuner 会根据搜索算法对不同的优化区间生成不同的编译配置。然后使用此配置编译运行,并根据运行性能的反馈来迭代优化配置参数。最后经过给定迭代次数后找出最优配置供用户使用。在实践过程中,通过 Autotuner 对 Coremark Benchmark 进行调优可以获取5%以上的收益。
7、毕昇编译器Fortran前端:特性、架构与优化增强
(1)毕昇编译器Fortran语言前端基于Classic Flang构建,通过增强Classic Flang,支持如下特性:
- 支持F2003、F2008 (Coarray特性除外)语言标准
- 最高支持四倍浮点精度(real16)数据类型,15维数组数据类型,支持OpenMP4.0、OpenMP4.5并行化编程模型
- 支持DWARF4标准
- 多种Pragma引导语支持,如软件prefetch,omp simd,unroll,vector等
(2)多个Fortran内建函数的优化
- maxloc/minloc/nint的内联
- trim/lentrim采用更优算法
(3)内存分配优化,栈内存替挽堆内存
(4)为中后端提供更准确详细的信息来辅助优化
- 别名分析增强
- 过程间优化增强
四、总结
- 针对不同的场景,不同的应用特点,使用不同的编译优化手段
- 编译优化的代价与收益权衡,需要综合考虑性能收益,代码体积,编译时间,可调试性等多方面因素
- 软硬件结合,通过软件及硬件的协同优化,最大化的发挥硬件算力
- 语言生态构建的持续性,语言标准的不断演进,及新特性支持
【参考】
【1】鲲鹏开发套件:鲲鹏社区官网-凝心聚力 共创行业新价值
【2】鲲鹏首页:鲲鹏社区官网-凝心聚力 共创行业新价值