面向国产高性能加速器的LLVM编译器设计及优化

2024-04-23 10:04宋强唐俊龙陈照云时洋谭期轩肖紫阳邹望辉
计算机工程 2024年4期
关键词:编译器寄存器指令

宋强,唐俊龙,陈照云,时洋,谭期轩,肖紫阳,邹望辉

(1.长沙理工大学物理与电子科学学院,湖南 长沙 410114; 2.国防科技大学计算机学院,湖南 长沙 410073)

0 引言

随着人工智能、大数据、云服务等新兴技术的发展,计算已经成为科学研究与工业生产必不可少的工具与手段[1]。高性能计算在材料科学[2]、大数据[3-4]、地震模拟[5]、天文观测、生物医学等领域的大规模数据模拟与仿真中发挥着重要的作用,是推动国家科技发展与进步的有力手段之一。国防科技大学作为国内最早开始高性能计算处理器自主设计的优势单位之一,研发了采用中央处理器(CPU)+通用数字信号处理器(GPDSP)片上异构融合架构的高性能加速器,其中GPDSP加速核采用超长指令集(VLIW)+单指令多数据流(SIMD)的向量化架构,具备高性能、低功耗的特点,是新一代超算系统核心计算芯片的有力竞争者之一。为了充分发挥GPDSP的硬件性能,提升用户编程友好性,从编译器层面实现对GPDSP的良好支持与并行性挖掘是可行的办法之一,也是当下亟待解决的重要问题。

低级虚拟器(LLVM)是当前主流的编译框架之一,统一的中间表达(IR)与高度模块化的设计是LLVM所具备的独特优势。高度模块化的设计降低了开发者工作的难度,统一的IR加强了对高级编程语言的支持,丰富的工具集极大地降低了开发者的工作量[6]。然而现有LLVM编译框架对国产高性能加速器兼容效果较差,无法充分发挥GPDSP加速核的硬件优势与结构优势,主要体现在以下几个方面:1)指令调度模块是面向通用处理器设计的,对VLIW体系结构的支持不够完善,在静态功能单元分配以及完整的指令打包与排布优化上具备可提升的空间;2)密集的数据计算指令排布容易造成更多的寄存器溢出,极大程度上影响性能发挥,尤其是对于静态指令排布的VLIW架构;3)不支持GPDSP特有的向量指令,增加了开发者的开发难度,无法充分发挥GPDSP结构在数据级并行上的优势。

本文基于主流LLVM编译框架,结合高性能加速器GPDSP的体系结构特点,优化功能单元分配冲突检测机制,提出支持静态功能单元分配与寄存器压力感知[7]的指令调度策略,充分挖掘程序指令级并行性能,为基于LLVM编译框架的VLIW指令调度研究与实现提供借鉴;在此基础上,结合自主GPDSP指令集,为用户提供一系列丰富且规整的向量指令接口,从而有效降低用户并行开发的难度,并且为用户程序在指令集层面提供更多数据并行优化的可能;最后,结合高性能加速器当前研究热点与发展趋势,对未来编译器优化方向提出一些后续的思考和讨论。

1 GPDSP体系结构与LLVM编译框架

1.1 GPDSP体系结构

高性能加速器核GPDSP基于自研指令集设计,采用VLIW+SIMD的标向量融合架构,最大可支持11条指令发射以及16宽度的向量单元[8],其内核部分的主要结构如图1所示。该加速器主要由包含标量处理单元(SPU)、标量存储单元(SM)的标量部件与包含向量处理单元(VPU)、向量存储单元(AM)的向量部件以及取指派发单元构成。其中:取指派发单元可以同时为标量部件与向量部件派发指令;标量处理单元主要负责标量数据处理与程序流控制,同时负责控制向量部件的运行;向量处理单元负责高密度数据的并行计算任务。向量处理单元由16个同构的向量处理引擎(VPE)构成,VPE内部集成3个同构的浮点乘加器(VMAC),标向量数据通过标向量协同单元(SVR)进行数据共享,可以通过SVR共享寄存器使用广播操作将一个标量寄存器的值传递到16个向量寄存器中[9]。GPDSP核内包含存储器直接访问部件(DMA),支持包括点对点、广播、二维索引传输等灵活高效的传输方式,使得GPDSP能够适应更多的应用需求。

图1 GPDSP结构Fig.1 GPDSP structure

GPDSP架构具有典型的VLIW特征,与传统处理器相比,为了增加运算性能并且简化加速器设计,GPDSP加速核不具备动态指令分配的特性,而是将包含一条或多条指令的完整的指令包分发给确定的功能单元,指令的功能单元分配、排布优化与打包等任务通过编译器的指令调度模块完成,编译器的指令调度优化性能也直接影响GPDSP的硬件性能释放率。同时为了提升数据并行处理能力,GPDSP融合了SIMD的架构特征,在指令集设计上提供了丰富的向量指令,加速器内部的向量处理单元为向量指令的实现提供了良好的硬件支持,编译器的支持能够让用户更好地使用GPDSP向量部件,有效降低了用户的开发难度。

1.2 LLVM编译器框架

LLVM编译架构由编译前端、代码优化器及编译后端三部分组成[10],如图2所示。其中,编译前端负责将高级语言(如C、C++、Python等)编写的程序转化成LLVM特定的中间表示形式LLVM IR;代码优化器主要对程序进行动态、静态分析及代码优化工作;编译后端负责将优化后的MI形式指令翻译成目标机器(如x86、ARM、RISCV等)的汇编代码。LLVM编译框架中包含许多优化遍[11],包含指令调度、寄存器分配等,优化遍能够极大程度地影响编译器最终的性能。

图2 LLVM编译器架构Fig.2 LLVM compiler architecture

然而,在开源LLVM编译器框架支持的众多处理器中,少有与GPDSP架构类似的,指令调度等关键优化遍也无法满足GPDSP使用需求。由于GPDSP采用VLIW+SIMD的异构架构,拥有大量的向量化资源,且用户程序多为数据密集型程序,执行效率极其依赖编译优化效果,因此有必要对GPDSP提供向量与指令调度支持。本文针对GPDSP芯片向量资源丰富、不包含动态调度硬件的特点,设计并优化支持GPDSP架构的指令调度策略,并提供向量支持。

2 指令调度设计

指令调度是编译器在IR层进行程序优化的一种技术,目的是通过调整指令顺序提高指令层上的并行度,使得程序在处理器上能够高效运行。现有LLVM指令调度策略是面向通用处理器CPU设计的,开源LLVM编译框架中仅有高通针对VLIW架构做出支持与优化,但高通与GPDSP架构相差较大,对GPDSP架构后端支持仅有借鉴意义,因此,本文结合GPDSP架构特点设计专用于GPDSP加速核的指令调度模块。指令调度模块设计包含寄存器压力感知调度与静态功能单元分配2个部分:寄存器压力感知调度基于pre-RA-sched设计,主要通过静态分析程序信息预测寄存器压力值,从而指导指令调度,获取更优的调度效果;静态功能单元分配基于post-RA-sched设计,负责将功能单元在编译阶段静态分配给每一条指令,通过冲突检测机制保证功能单元分配的正确性,从而保证程序正确执行,此部分是指令并行执行的基础。

2.1 寄存器压力感知调度

寄存器压力感知调度主要是在MI-scheduler过程中,通过程序静态分析的方式预测寄存器压力,从而指导指令调度的进行。指令调度与寄存器分配是一对相互矛盾的NP困难问题,主流的优先级调度方法往往会产生一定程度的寄存器溢出,而在有大量高密度数据并行计算的GPDSP高性能加速器上,这一矛盾表现得更为明显。采用峰值寄存器压力感知方法(PERP)[12],结合GPDSP结构特点设定代价函数,并通过蚁群优化(ACO)[13]算法实现寄存器压力感知调度,可以有效减少寄存器溢出数量,提升程序执行性能。寄存器压力感知调度流程如图3所示。

图3 寄存器压力感知调度流程Fig.3 Register pressure-aware scheduling process

寄存器压力感知调度主要由两部分组成:一是基于PERP的寄存器压力感知;二是基于ACO算法的指令调度。

2.1.1 寄存器压力感知算法

在指令调度过程中,通过def-use链获取每条指令的寄存器定义及使用信息;在每一次最优节点选取之后,获取该节点的寄存器使用信息,分别将def和use信息放入DefList、UseList中,通过对DefList、UseList 2个队列的信息进行分析与处理,当CurrCycle值变化时计算当前周期的寄存器压力ERP,CurrCycle值是SchedBoundary类下的一个unsigned类型变量,被用来表示调度周期,使用getCurrCycle()方法获得。PERP则是每个基本块内所有周期中最大的寄存器压力值(ERP)。以Topdown调度方式为例,具体算法描述如算法1所示。

算法1寄存器压力感知算法

输入最优节点SU(SUnit类型),当前CurrCycle

输出当前周期寄存器压力值ERP

1.RPValue ComputeRP(SU,CurrCycle){

/*获取当前SU对应的SDNode*/

2.SDNode MIScheNode = SU->getNode();

/*获取当前SUnit使用和定义的值的列表*/

3.UseList=MIScheNode->getOperand();

4.DefList=MIScheNode->getValue();

/*将DefList插入活跃值列表LiveVRegList中*/

5.LiveVRegList.insert(LiveVRegList.end(),DefList.begin(),DefList.end());

/*将UseList中后续无使用且是在当前基本块定义的值从LiveVRegList中删除*/

6.For each I in UseList

/*检查I是否在当前基本块定义*/

7.If (LiveVRegList.contains(I) )

8.If (CheckForRelease)/*检查I是否仍有使用*/

9.LiveVRegList.remove(I);

10.End If;

11.End If;

12.End For;

13.If (checkCurrCycle ());

/*若CurrCycle增加则计算ERP*/

14.ERP=LiveVRegList.size()-NumberPhyRegs;

15.}

算法1实现了对每个周期的寄存器压力感知,当前基本块的寄存器压力PERP则是每个周期寄存器压力ERP的最大值。

2.1.2 指令调度算法

通过ACO算法,在空间中搜索出大量满足依赖的调度方案,根据结合GPDSP架构特点设定的代价函数,每次迭代选出代价函数最小的调度方案保存,达到最大迭代次数后,输出最优调度方案。该算法由3层循环组成,最外层循环与中层循环是重复的迭代搜索过程,最外层循环每次迭代都会实时更新参数信息,最内层循环是基本块调度循环,一次循环生成一种调度方案。以TopDown调度方式为例,指令调度算法描述如算法2所示。

算法2指令调度算法

输入基本块MBB

输出近似最优调度方案

1.Scheduler.ACOSchedule(NumRegionInstrs){

/*初始化初始信息素浓度、信息素浓度矩阵、信息素挥发速率等参数信息,初始化DAG,构造原始调度队列ACOTopRoots*/

2.For(i=0;i<149;++i)/*设定最大迭代次数为150*/

3.For(j:Ant)/*蚁群大小设定为50*/

4.TopRoots=ACOTopRoots;

/*拷贝原始调度队列*/

5.While(true)/*无未调度SU,退出循环*/

/*若可用队列available中无SU,返回空,退出当前调度;若可用队列available中只有一个SU,返回该SU;否则按照可用调度路径上信息素浓度大小,使用轮盘赌法选出一个SU进行调度*/

6.SUnit*SU = SchedImpl->pickNoedwithACO

/*将返回的SUnit保存*/

7.j->ScheduleList.push_back(SU);

/*更新调度队列,从available中移除SU并释放SU的所有后继,更新available队列与pending队列*/

8.updateQueues(SU,IsTopNode)

/*计算当前周期寄存器压力*/

9.CurrERP=ComputeRP(SU,CurrCycle)

10.j->ERPList.push_back(CurrERP);

11.End While;

12.End For;

/*遍历所有调度方案,计算代价函数,返回最优调度方案BestScheduleList*/

13.BestScheduleList=setbest();

/*根据每个调度方案的代价函数值更新信息素浓度矩阵,代价函数值越低,信息素浓度增加越多;代价函数值越高,信息素浓度增加越少*/

14.updatepheromone()

15.End For;

/*根据最优调度方案进行调度*/

16.For(SU:BestScheduleList)

17.ScheduleMI(SU,IsTopNode);

18.End For;

19.}

PERP感知方法通过对程序的静态分析得到基本块的最大寄存器压力,GPDSP高性能加速器结构特点与应用场景决定了用户程序通常包含高密度数据并行计算,大多数应用程序在调度过程中存在多寄存器压力峰值的情况,调度时仅仅考虑最大寄存器压力在多数情况下不能得到良好的效果。因此,设定式(1)、式(2)所示代价函数,整体考虑寄存器压力分布与峰值寄存器压力对程序带来的影响。

(1)

(2)

其中:S表示调度长度;ω表示寄存器压力权重,该值越大,表示单位寄存器压力对程序性能的影响越大,通过调整ω的值可以使程序偏向最小化调度长度或最小化寄存器压力;Pmax表示峰值寄存器压力;Perp表示单位周期的寄存器压力;φ表示峰值寄存器压力在整体寄存器压力中的占比,φ值越高,表示峰值寄存器压力对程序性能的影响越大,φ值越低,表示整体寄存器压力对程序性能的影响越大。

2.2 静态功能单元分配

静态功能单元分配由后端描述、冲突检测、功能单元分配3个模块组成。其中:冲突检测模块主要负责检测是否有可用功能单元、指令是否可分配功能单元等;功能单元分配模块负责将可用功能单元与指令匹配;后端描述模块保存指令的指令执行进程表与调度约束等调度信息。

2.2.1 后端描述

后端描述文件FTSchedule.td主要保存所有指令的调度信息以及与GPDSP加速器硬件相关的调度约束。与RISCV、ARM架构相比,GPDSP需要明确指令在执行过程中对功能单元与读写端口的占用信息。因此,重构LLVM的调度信息与使用方式,增加对读写占用的区分,具体格式如下:

InstrItinData,InstrStage],[IndexA,Index,Index]>

其中:Instr表示指令名称;FuncUnitA、FuncUnitAw、FuncUnitB、FuncUnitBw表示所需的功能单元,使用w后缀对读写端口的占用进行区分,默认没有w后缀表示占用功能单元读端口;CycleA、CycleC表示占用对应功能单元的周期数;CycleB表示当前周期执行完后输出指令执行结果所需要的周期数;Index表示在输出指令之后读取或写入每个操作数所需要的周期数,默认IndexA为目标寄存器。

2.2.2 冲突检测模块

通过冲突检测保证功能单元分配的正确性,每个Cycle都会对需要发射的每条指令SUnit执行冲突检测模块。输入为指令SUnit,输出为无冲突nohazard或有冲突havehazard,检测流程如图4所示。每次执行首先会从AvailableQueue队列中拿出一条指令输入,通过getInstrDesc方法从后端描述文件FTSchedule.td中获得该指令的指令流水信息,从而获得执行该指令所需要的功能单元、读写端口以及占用的周期数,若该指令是伪指令或没有指令执行进程表,则直接返回nohazard。遍历所有功能单元,通过RequiredScoreboard得分板获得当前未被使用且可被分配的功能单元并存储到FreeUnits中。冲突检测时优先进行读冲突检测,不存在读冲突时再进行写冲突检测。读写冲突须同时满足条件返回nohazard,该功能单元可被分配。

图4 冲突检测流程Fig.4 Conflict detection process

2.2.3 功能单元分配模块

该模块的目标是使所有的指令在满足依赖关系的情况下完成功能单元的分配。所有AvailableQueue队列中满足冲突检测的指令会执行功能单元分配并调度,并将指令从AvailableQueue队列中移除。若指令不满足冲突检测,即对该指令不存在可供分配的功能单元,则将该指令放入PendingQueue队列中,等待下一次调度。当AvailableQueue为空时,Cycle加1,重新更新所有队列,开始下一次调度。

3 向量指令支持

GPDSP高性能加速器为向量指令实现提供了良好的硬件基础,用户可以使用向量指令为程序带来可观的性能加速,这需要从编译器层面实现向量指令支持,为用户提供向量指令接口。向量指令支持主要由向量指令实现、向量指令接口映射与汇编指令生成3个部分组成。其中:向量指令实现主要是面向硬件的向量指令集定义;向量指令接口映射主要负责将用户使用的向量接口通过builtin接口映射为intrinsic接口;汇编指令生成负责将intrinsic接口映射为LLVM IR并匹配对应的向量指令,该部分与标量指令采用相同的指令选择算法,参考LLVM指令选择匹配模板即可,无需特殊处理与设计。因此,向量指令支持重点将放在向量指令实现与向量指令接口映射这2个部分。

3.1 向量指令实现

为实现向量指令支持,新增了如表1所示的硬件支持的8种数据类型,所有数据类型的数据宽度均为1 024 bit。

表1 新增向量数据类型 Table 1 New vector data types

根据GPDSP指令集在后端描述文件中定义向量指令,以下给出定义向量指令的具体示例:

class 1OP_2OP_vrrr funct10,InstrItinClass InsnCycle,string opcodestr,VInstUnit u>:FTInstAVR{let Itinerary=InsnCycle;}

其中:1OP_2OP_vrrr表示向量指令模板,具体向量指令的实现需要继承对应的向量指令模板;funct10表示指令的操作码,对应指令集中指令的机器码;InsnCycle表示指令调度模式;opcodestr表示指令的名称;u表示指令所需的功能单元;FTInstrAVR表示指令类型,包含对应的指令功能信息。

根据GPDSP指令集,定义如表2所示的向量访存、向量运算等9种类型向量指令。

表2 新增向量指令 Table 2 New vector instructions

3.2 向量指令接口映射

考虑到给用户提供一套规整的向量指令使用接口,且与GNU编译器集合(GCC)提供的向量接口保持高度统一,有效降低用户开发难度,提高用户编程友好性,采用如图5所示的多级接口设计方案,基于GPDSP指令集,设计builtin接口与intrinsic接口,并将builtin接口封装提供给用户。

图5 向量指令接口设计方案Fig.5 Design scheme of vector instruction interface

每个builtin接口都具有唯一确定的编号。通过封装的形式,将builtin接口封装为与GCC一致的向量指令接口提供给用户使用。builtin接口与intrin接口使用编写的映射规则进行映射,每一个builtin接口都通过统一的接口编号绑定到对应的intrinsic接口。

映射规则的实现是通过在机器描述文件intrinsic.td中编写每一个intrinsic接口的记录,该记录维护builtin接口到intrinsic接口的索引,由tablegen工具自动生成C++转换程序,实现接口的映射。以向量加法为例,映射规则设计如下:

def int_vec_add_v:GCCBuiltin<"__builtin_vec_add">,Intrinsic<[llvm_lvs_int_ty],[llvm_lvs_int_ty,llvm_lvs_int_ty],[IntrNoMem,Commutative]>

其中:__builtin_vec_add为绑定的builtin接口;int_vec_add_v为对应的intrinsic接口内部名称;llvm_lvs_int_ty表示该加法操作的2个操作数都必须为lvector signed int类型数据。

特别地,部分源操作数包含立即数的指令,该类型的指令在匹配指令接口时,会由于重载导致立即数类型转换为变量,无法匹配对应的指令接口。针对该类型指令,通过添加广播操作,将立即数广播到寄存器中,以匹配不含立即数类型的向量接口,实现相同的指令执行效果。

4 测试与分析

4.1 实验平台与程序

本文提出的编译器设计与优化基于LLVM 12.0的版本进行代码功能实现,采用GPDSP高性能加速器[14]作为硬件测试平台,开展LLVM编译器功能测试与性能测试,重点包含指令调度与向量指令接口2个方面。

选用GCC testsuite与SPEC CPU 2017 2个测试集对改进后的指令调度模块进行功能验证与性能测试;基于GPDSP增加的向量指令接口,额外增加部分测试代码进行向量指令功能验证与性能测试。所有测试代码均由改进后的LLVM/Clang 12.0.0编译生成。

4.2 功能测试与分析

4.2.1 指令调度模块功能测试与分析

为验证设计编译器的正确性,对GCC testsuite中的所有测试程序与SPEC CPU2017标准测试集中编程语言不包含Fortran的30个测试题进行测试。以-O0优化级别下通过率为基准,GCC testsuite测试集在各优化级别下的测试结果如表3所示,可见,编译器在-O1优化级别下通过率为98.31%,在-O2、-O3、-Os优化级别下通过率为98.73%。编译及执行未通过测试用例的主要原因是部分inline以及优化处理与GPDSP架构不兼容,这些测试用例后续不再引入测试集进行结果展示。

表3 GCC testsuite测试结果 Table 3 Test results of GCC testsuite

SPEC CPU2017测试集的测试结果如表4所示,可见,标准测试集中整型与浮点型2类测试例共29道测试题均能正常执行通过。

表4 SPEC CPU 2017测试结果 Table 4 Test results of SPEC CPU 2017

通过GCC testsuite与SPEC CPU 2017这2个测试集测试结果的对比分析可以得出,改进指令调度模块后LLVM编译器能够正常生成正确的汇编程序且程序能够正确执行,指令调度模块功能验证通过。

4.2.2 向量指令接口功能测试与分析

基于GPDSP指令集,编写部分包含向量指令接口的测试代码,通过改进后LLVM编译器编译生成可执行文件,并使用GPDSP高性能加速器运行,统计对应向量指令汇编代码生成情况与程序执行情况,测试结果如表5所示,可见,所有向量指令接口都能生成对应的汇编指令,指令匹配正确,程序执行结果正确,向量指令接口功能验证通过。

表5 向量指令接口功能测试结果 Table 5 Function test results of vector intruction interface

4.3 性能测试与分析

4.3.1 整体性能测试与分析

选用GCC testsuite、SPEC CPU 2017测试集对编译器整体性能进行测试,明确性能边界。不同优化选项下采用的编译优化手段各不相同:-O0优化选项下不执行任何编译优化,程序顺序执行;-O1优化选项下会执行一些不增加大量编译时间的简单优化,可能会导致指令并行化;-O2优化选项下会执行绝大部分优化,最大程度地挖掘指令并行性能。在实现支持静态功能单元分配的指令调度后,为GPDSP执行并行指令提供了软件基础,保证了GPDSP指令并行执行的正确性。

通过对比GCC testsuite测试集在-O0、-O1、-O2优化级别下的执行速度,对编译器的整体性能进行评估,测试结果如图6所示。以-O0优化级别为基准,-O1与-O2优化级别均能得到较少的程序执行时间,其中:-O1优化级别下平均加速比为3.082,最高加速比为38.174;-O2优化级别下平均加速比为4.539,最高加速比为44.644。

图6 GCC testsuite整体性能测试结果Fig.6 Overall performance test results of GCC testsuite

图7所示为SPEC CPU 2017测试集测试结果,通过对比SPEC CPU 2017测试集在-O0、-O2优化级别下的执行速度,评估编译器的整体性能。由图可见:以-O0优化级别为基准,浮点测试平均性能加速比为4.49,最高加速比为14.03;整型测试平均性能加速比为3.24,最高加速比为5.94。

图7 SPEC CPU 2017整体性能测试结果Fig.7 Overall performance test results of SPEC CPU 2017

4.3.2 寄存器压力感知性能测试与分析

基于寄存器压力感知的指令调度方法能够在程序编译时静态分析寄存器压力值,通过代价函数选择最优调度方案,有效减少寄存器溢出数量,提升程序性能。寄存器压力感知调度对数据密集型程序更为敏感,因此,采用SPEC CPU 2017测试工具进行性能测试,对比LLVM指令调度器默认调度方法与寄存器压力感知调度方法的加速性能,编译优化选择-O2-mllvm-enbale-regpressure-mllvm-debug-only=machine-scheduler,ref规模,测试结果如图8所示。可以看出,使用寄存器压力感知调度后程序可以得到明显的性能改善,最高加速比为1.06,浮点测试平均加速比为1.024,整型测试平均加速比为1.016。

图8 寄存器压力感知性能测试结果Fig.8 Performance test results of register pressure-aware tests

4.3.3 向量指令接口性能测试

使用向量指令接口进行编程,通过编译器生成对应向量汇编指令,能够极大地减少代码体积,提升程序执行效率。本文基于GPDSP向量指令集,增加了部分函数级测试代码,对比使用向量指令前后的性能与代码体积差异,结果如表6与图9所示。从表6中可以看出,在使用向量指令后,程序执行包数量平均减少95.41%,vsip_vcmagsq_f指令程序执行包数量减少率最高,减少率为98.27%。从图9中可以看出,使用向量指令后,所有程序都获得了不同程度的性能提升,平均性能提升率为97.1%,最高性能提升率为98.7%。

表6 向量程序体积测试结果 Table 6 Vector program volume test results

图9 向量程序运行时间测试结果Fig.9 Test results of vector program runtime

5 思考与讨论

编译器是用户程序开发最重要的工具之一,能够为用户提供多种优化手段,如何利用编译器为用户降低程序开发难度并且带来更好的优化效果,始终是编译器领域研究者所关注的问题。

在高性能计算领域,用户程序大多具有数据密集型与计算密集型的特点,而用户使用高度抽象的高级编程语言开发程序时无法从细粒度的指令集层面考虑热点代码片段的数据访存开销与硬件资源利用率,因此,本文结合当前高性能计算领域研究热点,从编译器的角度出发,对未来优化方向进行讨论和探索:

1)组合优化[15-16]。现有编译器框架的优化方式大多是分开进行的,问题在于多种优化方式之间可能存在隐形的影响,例如指令调度与寄存器分配。指令调度以最大化指令级并行(ILP)为目标,在寄存器分配阶段时可能导致寄存器溢出,需要将多余的变量值写回内存中,从而产生额外的访存开销,影响程序性能。解决这类问题的主要思路是通过组合优化的方式,将多种优化方式组合在一个优化中,从全局的角度进行优化,但这会导致极大的时间开销。目前组合优化的效果并不理想,需要探寻新的优化方式与优化组合,以达到更好的性能提升与更低的时间开销。

2)自动向量化[17-18]。相比于手动编写SIMD向量程序,自动向量化能让程序员继续采用串行思想进行编程,而不用考虑SIMD扩展部件的功能与特点,减少程序员编程难度与工作量。目前,自动向量化技术还存在许多不足之处,值得进一步研究,例如冯竟舸等[19]提出的引入动态规划、整数线性规划等组合优化方法进行并行度的选择、引入同步多线程(SMT)等理论方法解决向量化分组问题、对SIMD扩展部件进行特异化设计等方向都是未来可继续研究的方向。

3)自动调优[20-22]。GCC和LLVM中都包含有大量的优化方式,通过-O参数设置选择对应的优化级别是当下普遍的选择[23-24],然而固定组合优化方式并不能总是带来更好的程序执行性能,不同参数因子、优化选项、优化选项执行顺序对程序的性能都会带来不同的影响。通过迭代编译或机器学习的方式在大量训练下进行自适应的因子选择、自动优化编译选项、内联决策和分支概率预测等能为编译器带来更多性能提升的空间[25]。

6 结束语

本文结合高性能加速器与其加速核GPDSP的结构特点,提出了支持功能单元静态分配与寄存器压力感知的指令调度策略,为高性能加速器的指令并行执行提供软件支持,减少数据密集型应用普遍存在的寄存器溢出,同时使用向量intrinsic方法对GPDSP向量指令集提供了支持。实验结果表明:优化后指令调度在功能上能够实现对高性能加速器的良好支撑,相对于LLVM -O0优化方法,使用GCC testsuite测试在-O2优化选项下能够实现平均4.539的加速比,使用SPEC CPU 2017浮点测试与整型测试在-O2优化选项下分别实现了平均4.49、3.24的加速比;使用寄存器压力感知的指令调度在浮点程序与整型程序上分别实现了平均1.024、1.016的性能加速比;相对于标量程序,使用向量指令接口实现的向量程序在执行包数量上平均减少95.41%,平均性能加速比为35.2,降低了用户编程难度,提高了用户友好性。未来将继续优化寄存器压力感知调度方法,探索新的寄存器压力感知策略,并进一步支持向量程序指令调度,提升向量部件资源利用率。同时,将持续研究编译器层面理论方法,从软件角度挖掘高性能计算的优化空间。

猜你喜欢
编译器寄存器指令
听我指令:大催眠术
Lite寄存器模型的设计与实现
基于相异编译器的安全计算机平台交叉编译环境设计
ARINC661显控指令快速验证方法
LED照明产品欧盟ErP指令要求解读
分簇结构向量寄存器分配策略研究*
通用NC代码编译器的设计与实现
坐标系旋转指令数控编程应用
高速数模转换器AD9779/AD9788的应用
一种可重构线性反馈移位寄存器设计