以RISC-V 为目标的动态二进制翻译代码质量优化方法

2023-10-27 02:50余子濠孙凝晖包云岗
计算机研究与发展 2023年10期
关键词:二进制字节寄存器

余子濠 陈 璐 孙凝晖 包云岗

(处理器芯片全国重点实验室(中国科学院计算技术研究所) 北京 100190)

(中国科学院大学 北京 100049)

生态系统是一款指令集架构(instruction set architecture,ISA)以及相应芯片的生命力所在.目前主流的两大生态系统是以Windows 和Intel 为联盟的Wintel 生态系统,以及以Android 和ARM 为联盟的AA 生态系统.对于其他ISA(尤其是新兴ISA,如RISC-V),其生态系统建设面临生态系统壁垒问题:生态系统构成庞大,而且与ISA 的耦合度非常高,要将一个生态系统从一款ISA 完美地移植到另一款是一项非常困难的工作.因此,生态系统建设需要投入大量人员和精力.以RISC-V 为例,自2011 年5 月第1 版发布起,Linux 内核和Binutils 工具链分别经过78 个月和71 个月才进入主线分支[1];而Debian 发行版自宣布开始往RISC-V移植至正式发布,则花费41个月[1].

解决生态系统壁垒问题的主流技术是动态二进制翻译技术.动态二进制翻译技术的思想与编译技术十分类似,编译技术负责将程序从源代码静态地翻译成行为等价的二进制指令;而动态二进制翻译技术则通过二进制翻译程序在运行时刻将二进制程序从一种源指令翻译成另一种目标指令.但在生态系统复用的场景中,大部分缺失源代码的二进制程序无法通过编译技术重新生成目标ISA 的二进制文件,故编译技术不能解决此问题.相对地,动态二进制翻译技术在提供二进制程序的场景下即可使用,同时可通过收集运行时刻的信息辅助翻译,因此成为解决生态系统壁垒问题的主流技术.

动态二进制翻译技术的效率受3 个因素影响:翻译程序的翻译效率(是否翻译得快);翻译的目标代码质量(是否翻译得好);动态二进制翻译机制的维护开销(是否维护得好).对于大部分具有明显热点的程序,如SPEC CPU2006 基准程序[2],其执行时间主要集中在若干热点代码片段中,能否翻译出质量更高的目标代码,对这些程序的执行效率影响很大.因此本文将重点讨论动态二进制翻译的目标代码质量优化方法.

为分析出潜在的优化点,本文选定了一种目标ISA,然后按照语义差异由小到大分析从其他ISA 翻译到该目标ISA 时,影响动态二进制翻译技术目标代码质量的因素.具体地,本文以当前流行的RISCV64 为目标ISA,分别将RISC-V64,RISC-V32,MIPS32,x86 作为源ISA 进行分析,并将影响目标代码质量的因素总结成3 点:1)目标寄存器的使用.一方面,动态二进制翻译机制需要占用部分目标寄存器;另一方面,目标代码与运行时环境进行交互时也需要对目标寄存器进行保存和恢复.2)源程序和动态二进制翻译器的地址空间存在差异.为保证目标代码正确访问源程序的数据,需要对所有访存进行段式地址转换.3)源ISA 与目标ISA 存在语义差异.语义差异过大会导致翻译程序需要将1 条源指令翻译成若干条目标指令,从而降低目标代码的执行效率.

为解决这3 个问题,本文针对动态二进制翻译的翻译质量提出了若干优化方法:

1)为优化目标寄存器的使用,本文提出一套兼容应用程序二进制接口(application binary interface,ABI)的目标寄存器分配方案,将常用的源寄存器分配在静态的目标寄存器中,减少由运行时环境交互引入的状态保存和恢复开销;

2)为解决因源程序和动态二进制翻译器的地址空间差异引入的开销,本文提出一种基于位置无关可执行(position-independent executable,PIE)文件的虚拟地址空间复用方法,借助PIE 可加载到任意地址的特性,消除段式地址转换引入的开销;

3)为减少源ISA 与目标ISA 间的语义差异,本文运用B 扩展(位操作扩展)[3]和P 扩展(DSP 定点运算扩展)[4]中的指令来实现若干条简单目标指令的功能,提升目标代码的质量.

本文在一个新的动态二进制翻译程序DBT-FEMU中实现这些优化方法,在FPGA(field programmable gate array)中通过DBT-FEMU 和QEMU-i386 分別将x86 源程序翻译成RISC-V64 目标程序并运行.评估数据显示,在运行SPEC CPU2006的整数基准程序时,与QEMU-i386 相比,DBT-FEMU 翻译出的目标程序所执行的动态指令数平均减少57%,平均性能达到QEMU-i386 的4.12 倍.

1 背景

1.1 动态二进制翻译介绍

动态二进制翻译技术主要涉及3 个程序:源程序(source program)是翻译过程的输入;目标程序(target program)是翻译过程的输出,其ISA 通常与本地环境的ISA 相同,可直接在本地环境运行;翻译程序(translation program)是执行翻译操作的主体,负责读入源程序并输出目标程序.源程序的指令称为源指令(source instruction),目标程序的指令称为目标指令(target instruction).

图1 展示了动态二进制翻译的基本工作流程.在翻译过程中,源程序的指令以基本块(basic block)为单位划分.对于即将执行的基本块,翻译程序首先检查该基本块是否已被翻译.若否,翻译程序首先将该基本块中的源指令翻译成行为等价的目标指令;若是,翻译程序将直接取出该基本块的翻译结果.接下来翻译程序将把执行控制权交给翻译后的目标指令.执行该基本块之后,翻译程序将分析源程序的下一个基本块.此过程将循环进行.

Fig.1 Basic workflow of dynamic binary translation图1 动态二进制翻译的基本工作流程

与将源代码编译成目标指令并直接在目标机器上运行相比,通过动态二进制翻译技术运行源程序的运行效率一般有所下降.影响运行效率的原因主要有3 点:1)翻译程序翻译的速度.由于动态二进制翻译技术需要在运行时刻进行翻译,故翻译的时间开销会算入程序运行时间,若翻译耗时过长,将降低程序运行的效率.2)目标指令的质量.翻译程序翻译出目标指令之后,将执行这些目标指令,若目标指令质量较低,则需执行更多指令,从而降低程序运行的效率.3)维护二进制翻译机制所需开销.动态二进制翻译技术需要在翻译程序和目标程序间来回切换执行,同时需要正确处理翻译程序和目标程序的地址空间,还要正确维护源基本块和目标基本块的映射关系.在这一系列开销中,有的体现为翻译过程的开销,有的体现为额外数量的目标指令,有的则体现为支持目标程序运行时环境的开销.

1.2 RISC-V B 扩展和RISC-V P 扩展介绍

RISC-V B 扩展由若干扩展组件组成,旨在缩减代码大小、提升性能和节约功耗.B 扩展中的大部分指令具有通用性,但小部分指令在某些特定领域中更有用.因此B 扩展以若干个较小的子扩展组成,根据功能和使用场景对子扩展进行分组.每个子扩展包含一些作用相近的指令,故通常共享相同的硬件逻辑.这些子扩展覆盖的功能包括地址生成(Zba 子扩展)、基础位操作(Zbb 子扩展)、无进位乘法(Zbc子扩展)、单比特操作(Zbs 子扩展)、交叉排列(Zbkx子扩展)和面向密码算法的位操作(Zbkb 子扩展).

RISC-V P 扩展旨在提升数字信号处理(digital signal processing,DSP)算法在RISC-V 处理器上的处理能力,这些算法覆盖了传感器融合、伺服电机控制、音频编解码、语音合成和编码、视频解码、医学成像、计算机视觉、嵌入式控制、机器人、人机接口等领域.通过添加RISC-V P 扩展,RISC-V 处理器运行上述DSP 应用程序时可降低功耗并提升性能.具体地,RISC-V P 扩展通过引入面向整数以及8b,16b,32b 定点数据类型的单指令多数据(single instruction multiple data,SIMD)指令来实现上述优化.与其他 SIMD 指令不同,RISC-V P 扩展使用通用寄存器而不是专用寄存器来执行SIMD 操作,对嵌入式应用具有更好的效果.此外,RISC-V P 扩展还包括一些用于定点运算的非SIMD 指令.

2 相关工作

2.1 传统的动态二进制翻译器

动态二进制翻译技术在2000 年前后是一个研究热点.一个经典的工作是Transmeta 公司通过动态二进制翻译技术运行x86 的Windows 生态系统.Transmeta公司的处理器是顺序的超长指令字(very long instruction word,VLIW)架构,其上运行专门开发的代码变换软件(code morphing software,CMS),通过软硬件协同实现x86 架构的兼容性[5].CMS 由解释器、运行时系统和动态二进制翻译器组成.CMS 的解释器通过记录x86 指令的执行频率和一些启发性指标,为后续的翻译优化过程提供指导.这种做法与Shade 软件[6]类似,但CMS 的翻译开销比Shade 大很多,生成的代码质量也高很多.在软硬件协同设计方面,CMS 和硬件处理器协同实现一套“推测执行-检查恢复-自适应重翻译”机制[7].具体地,CMS 首先进行激进的推测,包括假设不发生异常,假设没有内存映射输入输出(memorymapped I/O,MMIO)的访存,假设访存操作没有重名,假设不存在自修改代码.在这些假设的前提下,CMS可生成高度优化的推测执行代码.然后处理器执行代码时检查这些假设是否成立,若不成立,则回滚到最近一次提交的状态并进行解释,来保证程序正确执行.CMS 也会统计回滚频率,若回滚操作频繁发生,将在激进程度较低的假设下重新翻译代码,以减少回滚操作的开销.CMS 中包含的解释器可模拟用户模式和系统模式的操作,同时CMS 支持从初始版本到最新多媒体指令的所有x86 指令,故对x86 指令的兼容性高于同期的动态二进制翻译器.

同期还有不少著名的动态二进制翻译器.DEC公司的FX!32 是一个程序分析导向的动态二进制翻译器[8].其他的动态二进制翻译系统还有IBM 公司的Daisy 和BOA[9-10].一些系统结合了解释与动态二进制翻译.例如,Java HotSpot 客户端编译器包含Java虚拟机(Java virtual machine,JVM)解释器和即时编译器,以实现机器代码性能与编译速度之间的平衡[11].HP 公司的Aries 模拟器结合了代码的快速解释与动态翻译,在IA-64 系统上通过HP-UX 透明并正确地执行PA-RISC 应用程序[12].此外,也有一些机器自适应的动态二进制翻译器可根据机器特性规范及其ISA 来支持不同的源和目标机器的二进制翻译,例如UQDBT[13]和Walkabout/Yirr-Ma 框架[14].

2.2 QEMU

QEMU[15]是一个开源的ISA 模拟器,可通过动态二进制翻译模拟处理器行为,并为机器提供不同的硬件和设备模型,使其运行于各种客户操作系统.QEMU还支持运行Linux 用户级程序,甚至允许将编译成一种ISA 的应用程序运行在另一种ISA 的处理器上.QEMU 支持多种ISA 的模拟,包括x86、MIPS64(支持到第6 版)、SPARC、ARM、SuperH、PowerPC、ETRAX CRIS、MicroBlaze 和RISC-V.

QEMU 支持多种源ISA 和多种目标ISA,这是通过QEMU 中的目标代码生成器TCG 实现的.具体地,TCG 将QEMU 的动态二进制翻译过程划分成2 部分:1)将源程序以基本块为单位翻译成TCG 指令,其中TCG 指令是一种机器无关的中间表示;2)通过目标代码生成器TCG 将这些TCG 指令编译成目标代码.通过对过程的划分,QEMU 可对TCG 指令进行优化,如活跃变量分析、可达性分析、消除冗余数据移动等.QEMU 从0.10.0 版本引入TCG,消除了早期版本中dyngen 模块依赖特定版本GCC 编译器的缺陷[16],方便QEMU 在运行时刻与代码生成和其他任务交互(如支持插件等).

QEMU 支持将x86 源程序翻译成RISC-V64 目标程序,但翻译质量较低.据测试,运行SPEC CPU2006整数基准程序的test 输入时,QEMU 将源程序从x86翻译到RISC-V64 后,目标程序的平均运行效率只有本地的8.04%.

一些面向QEMU 的优化工作包括通过信息静态预处理方法优化源程序的库函数调用[17];通过对目标处理器的指令缓存和数据缓存的访问进行动态负载均衡调度,缓解QEMU 产生的目标代码对缓存的压力,从而提升目标程序执行的效率[18].

2.3 面向RISC-V 的动态二进制翻译器

2015 年后,学术界开始围绕RISC-V 开展动态二进制翻译的研究工作.Rv8[19]利用ISA、ABI 和当前的x86-64 微结构特征,将31 个RISC-V 源寄存器更合理地映射到16 个x86-64 目标寄存器.同时Rv8 还利用行为复杂的指令提升目标代码密度,并将微操作融合成复杂指令,生成最少数量的目标微结构微操作.Ilbeyi 等人[20]为ISA 模拟器生成器Pydgin 添加RISC-V支持,方便用户生成一个易于个性化的动态二进制翻译引擎.R2VM[21]利用二进制翻译来实现周期精确的全系统快速模拟,支持运行时刻在功能模式和时序模式之间按需切换.但这些动态二进制翻译程序都是将RISC-V64 源程序翻译成x86-64 目标程序,而本文探究的是将x86 源程序翻译成RISC-V64 目标程序的翻译质量.

2.4 RISC-V B 扩展和RISC-V P 扩展的相关工作

目前RISC-V B 扩展标准已经冻结,近年来学术界也有一些研究工作围绕RISC-V B 扩展开展.Marshall 等人[22-23]受到RISC-V B 扩展的启发,介绍了第1 个完整的32b RISC-V加密扩展的开源实现.Babu 等人[24]实现了RISC-V B 扩展,并从代码密度和速度提升2 个方面对位操作指令进行了量化分析,结果显示,某些程序采用RISC-V B 扩展后,运行效率提升28%,同时代码长度降低20%.而对于RISC-V P扩展,虽然其标准目前尚未完全冻结,但也有一些研究工作围绕RISC-V P 扩展开展.例如Chen 等人[25]提出了一个端到端系统栈,通过在TVM 和LLVM 上支持RISC-V P 扩展指令,使RISC-V 架构可高效运行机器学习模型.与上述研究工作不同,本工作期望通过RISC-V B 扩展和RISC-V P 扩展提升动态二进制翻译的目标代码质量.

3 动态二进制翻译的目标代码质量分析

为保证正确运行目标代码,在不同ISA 间进行动态二进制翻译,通常需要翻译出多条目标指令.本文以RISC-V64 作为目标ISA,按照语义差异由小到大的顺序,讨论从其他ISA 翻译到RISC-V64 时影响目标代码质量的因素,为动态二进制翻译的代码质量优化提供依据.

3.1 RISC-V64 作为源ISA

为简单起见,本文首先讨论RISC-V64 作为源ISA的情况,即将RISC-V64 源程序通过二进制翻译技术翻译到相同的目标指令并执行.在这种情况下,由于源ISA 和目标ISA 完全相同,故运行目标代码的额外开销主要源于动态二进制翻译机制的固有开销.具体地,本文对常见整数指令进行分类,并分别讨论动态二进制翻译在翻译相应类型指令时引入的开销.

整数计算指令只会在寄存器间进行计算,包括算术运算指令、逻辑运算指令、移位指令、立即数加载指令.但在动态二进制翻译机制中,为保证运行时环境的正确性,并非所有目标寄存器都可直接分配给目标代码使用.如gp 寄存器用于存放指向全局偏移量表(global offset table,GOT)的指针,供运行时环境的动态链接机制使用,覆盖它将导致相关功能出错.故即使源ISA 与目标ISA 完全相同,动态二进制翻译的机制仍需占用部分目标寄存器,使部分源寄存器只能分配到目标内存中.若源程序读取这部分源寄存器,则需额外翻译一条目标访存指令,将源寄存器的值从目标内存读入临时寄存器;若源程序写入这部分源寄存器,也需额外翻译一条目标访存指令,将访存数据从临时寄存器写回源寄存器对应的目标内存中.

对于访存指令,因源程序的地址空间与动态二进制翻译器的地址空间不同,动态二进制翻译器会在其地址空间中分配一段连续的内存区间供源程序访问,如图2 所示.为了让翻译出的目标访存指令访问到正确的内存位置,还需额外翻译出1 条加法指令进行段式地址转换,对源程序的访存地址加上该内存区间的基地址,使其结果落在该内存区间内.

Fig.2 Difference of address space between source program and translation program图2 源程序和翻译程序的地址空间的不同

对于控制转移指令,由于源指令长度与目标指令长度不存在明确对应关系,故翻译程序须正确维护每个基本块的源地址和目标地址间的映射关系.源程序执行控制转移指令时,需通过源地址查询该映射关系,得到目标地址并跳转.对于直接跳转指令(如jal 和beq 等),可以通过块链接技术(block chaining)[15]节省查询开销.但因目标指令中跳转范围有限(如RISC-V64 中beq 指令跳转范围是±4KB),若跳转距离较远,则需通过额外1 或2 条目标指令将跳转目标地址装载到临时寄存器中,再通过间接跳转指令进行跳转.

除上述指令的翻译外,调用运行时环境的辅助函数也会引入开销.具体地,函数调用需遵循ABI 规范的调用约定(calling convention),其中规定了每个寄存器由何者保存(调用者或被调用者).因此,若目标代码调用运行环境的辅助函数时,希望某由调用者保存的目标寄存器不被破坏,则需在调用前把该目标寄存器的值保存到栈上,并从辅助函数返回后恢复该寄存器.

3.2 RISC-V32 作为源ISA

RISC-V32 是RISC-V64 的子集,大部分RISC-V32指令均可翻译成一条RISC-V64 指令.但因ABI 规范中定义的数据模型(data model)的差异,部分情况下需翻译出额外的目标指令.具体地,RISC-V32 采用ILP32 数据模型,即整数、长整数、指针类型变量的长度均为32 b;而RISC-V64 采用LP64 数据模型,即整数类型变量的长度为32 b,长整数、指针类型变量的长度均为64 b,同时其ABI 规范的调用约定规定,32 b 数据以符号扩展的形式在64 b 寄存器中存储[26-27].故目标访存指令访存时,可能因访存地址的高32 b存放了符号扩展的结果,使其落在0~4 GB 外,触发非法访问异常,如图3 所示.为解决此问题,翻译程序需在目标访存指令前生成额外的目标指令,对访存有效地址合规化,即对访存地址的高32 b 清零,保证访存有效地址位于0~4 GB 内.

Fig.3 Legalization for effective address图3 对有效地址合规化

3.3 MIPS32 作为源ISA

与将RISC-V32 作为源ISA 的情况相比,翻译程序还需为另外2 种情况翻译出额外的目标指令:1)对于行为稍复杂的源指令,需多条目标指令组合实现其功能,如MIPS32 的nor 指令需翻译成or 和xori这2 条RISC-V64 指令,而MIPS32 的条件传输指令movz 则需翻译成8 条RISC-V64 指令;2)MIPS32 的I型指令中的立即数长度为16b,但因RISC-V64 指令中的立即数为12b 有符号数,其表示范围为-2 048~+2 047,故若源指令的立即数大于2 047,则翻译程序需翻译出额外的目标指令用于装载立即数.

3.4 x86 作为源ISA

因x86 更复杂,故有更多因素影响动态二进制翻译的目标代码质量:1)与MIPS32 的复杂指令类似,翻译程序需将x86 的复杂指令翻译出多条目标指令来完成其功能,如bswap 指令用于对源寄存器按字节反向排序.特别地,x86 支持多种寻址方式,其中最复杂的是相对基址变址寻址,如mov $0x1,-0x2000(%ecx,%ebx,4),其有效地址需通过计算ECX+EBX×4-0x2000得到,故翻译程序需为该源指令额外翻译出3 条目标指令用于计算有效地址.2)x86 通过标志寄存器EFLAGS 存储某些指令执行的结果(如算术比较指令cmp),并在后续指令中读出标志来决定如何执行该指令(如条件跳转指令je).RISC-V64 不支持标志寄存器,故需通过较多目标指令维护EFLAGS 寄存器的状态.一般通过条件码惰性求值(lazy condition code evaluation)技术[15]优化冗余的条件码计算,以减少目标指令.3)x86 通用寄存器的组织方式类似C 语言的联合体,可通过字节和半字方式读写通用寄存器的低位,如图4 所示.但RISC-V64 的通用寄存器仅支持整体读写,要支持x86 通用寄存器的特性,需通过多条目标指令读出目标寄存器的一部分,对读出结果与待写入数据进行拼接,最后将拼接结果整体写入目标寄存器.

Fig.4 Accessing x86 source register by byte granularity图4 通过字节粒度访问 x86 源寄存器

3.5 翻译质量分析案例

如图5 所示,在RISC-V64 平台上运行的QEMU-i386将1 条采用基址变址寻址方式的x86 源指令翻译成8 条RISC-V64 目标指令.由于QEMU 采用简单的基本块内数据流分析技术,可对多条x86 源指令翻译出的RISC-V64 目标指令进行优化,故本案例中,在RISC-V64 目标指令①前,EDX 源寄存器的值已被读入s3 目标寄存器.基于该前提,QEMU-i386 翻译出的8 条RISC-V64 目标指令说明为:①从目标内存中读取变址寄存器EBX 的值到临时寄存器s5 中;②通过逻辑左移操作将EBX 的值左移1 b,计算EBX×2;③从目标内存中读取基址寄存器EBP 的值到临时寄存器s7 中;④将基址与变址相加,计算访存的有效地址,即EBP+EBX×2;⑤将④计算出的有效地址逻辑左移32 b;⑥将⑤的结果逻辑右移32 b,⑤⑥这2 步用于对访存地址合规化,得到合法的32 b 有效地址;⑦对该合法的有效地址进行段式地址转换,加上源程序的内存在动态二进制翻译器中的基地址(提前存放在目标寄存器s1 中),计算出目标程序需访问的内存地址;⑧把DX 寄存器的值写入该内存地址.

Fig.5 An example of translated target code of QEMU-i386图5 QEMU-i386 翻译出的目标代码示例

从图5 案例可见,翻译出的8 条目标指令中只有目标指令⑧反映出该x86 源指令本质的写入内存操作,其余目标指令均因动态二进制翻译而被额外引入.具体地:1)指令①和指令③的引入是由于QEMU-i386把部分源寄存器(包括EBP 和EBX)分配到目标内存,需通过额外的目标访存指令访问.2)指令②和指令④的引入是由于x86 的编程模型与RISC-V64 存在较大差异,此处体现在寻址模式的差异,RISC-V64 需通过指令②和指令④实现x86 基址变址寻址方式.3)指令⑤和指令⑥的引入是由于ABI 规范中数据模型的差异,具体地,x86 是32 b 的ISA,采用ILP32 数据模型,其指针长度为32 b,只能访问0~4GB 的地址空间;而RISC-V64 是64 b 的ISA,采用LP64 数据模型,其指针长度为64 b,可访问4 GB 以上的地址空间.故通过RISC-V64 目标指令计算出访存地址后,还需通过指令⑤和指令⑥对访存地址合规化,即将地址的高32 位清零,保证访存地址位于0~4 GB 内.4)指令⑦的引入是由于源程序的地址空间与QEMU-i386 不同,需加上提前存放在目标寄存器s1 中的基地址进行段式地址转换.

4 动态二进制翻译代码质量优化方法

针对上述动态二进制翻译代码质量分析结果,本文提出若干方法提升动态二进制翻译的代码质量,如表1 所示.其中块链接技术和条件码惰性求值技术已被广泛应用于动态二进制翻译领域[7].

Table 1 Summary of Code Quality Optimization Methods for Dynamic Binary Translation表1 动态二进制翻译代码质量优化方法总览

4.1 兼容RISC-V64 ABI 的目标寄存器分配方案

QEMU-i386 默认不将任何x86 源寄存器分配到RISC-V64 目标寄存器中,而是将其分配在目标内存中,需通过访存指令访问,并通过数据流分析技术对目标寄存器使用情况进行动态分析,优化冗余的访存指令.因RISC-V64 有32 个通用寄存器,远多于x86 的8 个通用寄存器,故可将这8 个源通用寄存器均静态地分配到RISC-V64 的目标通用寄存器中,以消除因访问源通用寄存器而引入的目标访存指令.

因翻译后的目标程序会与翻译程序提供的运行时环境交互,如调用辅助函数、处理系统调用,故可根据RISC-V64 的调用约定规范更合理地分配x86 源寄存器,实现按需保存寄存器.本文采用的寄存器分配方案如表2 所示.

Table 2 Target Register Allocation Scheme Compatible with RISC-V64 ABI表2 兼容RISC-V64 ABI 的目标寄存器分配方案

此分配方案的说明为:1)x86 的8 个源寄存器被分配到RISC-V64 的sp,s0 和s2~s7 中,在调用约定规范中,这些目标寄存器均为被调用者保存,若被调用者无需使用这些寄存器,则无需保存,达到按需保存寄存器的效果.2)因条件码惰性求值所用寄存器的生存期可能跨越不同基本块,故将其分配到s8,s9,s10,亦可借助调用约定规范实现按需保存寄存器的效果.3)s11 存放用于和运行时环境交互的全局指针,其作用类似于指向GOT 的指针.具体地,该全局指针指向内存中预先分配的一张表,该表用于分配不常用的变量(如EFLAGS 中的DF 标志)以及运行时环境提供的辅助函数的地址,通过全局指针,翻译后的目标程序可方便地访问表中内容.4)因RISC-V64的寄存器数量较多,故可将翻译过程所用临时寄存器静态分配到固定的目标寄存器中,从而无需在翻译程序中实现目标寄存器的动态分配,既简化了翻译程序的实现,又提升了翻译速度.此外,本方案将t0~t4 以及a3~a7 用作翻译过程中的临时变量,根据调用约定规范,虽然这些目标寄存器在函数调用过程中可能被覆盖,但若临时变量的生存期不会跨越1 条源指令,则目标程序无需保存相应寄存器.5)翻译程序可在目标寄存器中静态分配若干常用的常数提升翻译质量,如长度分别为8 b,16 b,32 b 的掩码,借助这些掩码可方便实现x86 通用寄存器的字节和半字访问,具体方法将在4.6 节介绍.6)目标寄存器zero,gp,tp 不向目标程序分配,其中zero 寄存器恒为0,不适合分配;gp 和tp 分别用于存放翻译程序的全局指针和线程指针,破坏它们将导致翻译程序运行错误.7)a0~a2 及ra 为空闲,翻译程序未使用.

4.2 基于PIE 的虚拟地址空间复用方法

为优化因源程序和动态二进制翻译器的地址空间差异引入的开销,本文利用了PIE 和虚拟地址空间的特性.具体地,动态二进制翻译器可将源程序的代码和数据加载到与源程序自身虚拟地址相同的内存位置,以消除源程序和动态二进制翻译地址空间的差异.此后,段式地址转换所需加上的偏移量为0,故无需翻译出加法指令,使目标程序直接通过源程序虚拟地址空间中的地址进行访存时,亦可正确访问加载的内存位置.

但若加载源程序时,其内存位置已被动态二进制翻译器所使用,加载过程将破坏动态二进制翻译器.为解决此问题,本文将动态二进制翻译器自身编译成PIE,此后操作系统可将PIE 加载到任意内存位置正确运行.目前本地环境通常为64 b,其虚拟地址空间远大于4 GB(如RISC-V64 支持 Sv39 分页机制,默认的虚拟地址空间至少为512 GB),操作系统通常将PIE 加载到位于4 GB 以上的动态段(dynamic segment);而32 b 的源ISA 只支持32 b 的虚拟地址空间,故按照上述加载方案,源程序均会被加载到0~4 GB 的内存范围;对于64 b 的源ISA,虽然其虚拟地址空间多于4 GB,但根据ABI 规范,源程序的起始地址同样位于0~4 GB.综上,源程序的加载过程均不会破坏动态二进制翻译器,从而保证优化方法正确工作.

4.3 基于RISC-V B 扩展指令的地址合规化方法

为优化访存地址合规化操作,需考虑如何高效进行“对64 b 寄存器的高32 位清0”的操作,以保证访存地址位于0~4 GB 范围.QEMU-i386 对该寄存器左移32 b,将原高32 b 移出,再将结果逻辑右移32 b,以实现地址的合规化.此方法需花费2 条目标指令.

为优化访存地址合规化操作,可预先在某目标寄存器中存放低32 b 的掩码,即0x00000000ffffffff,再将此掩码与需合规化的目标寄存器进行与操作.因可在翻译程序初始化阶段设置该掩码,故翻译程序只需额外翻译出1 条and 目标指令,与QEMU-i386采用的方法相比可节省1 条目标指令.但该方法需占用1 个目标寄存器存放掩码.为进一步节省该目标寄存器,可利用RISC-V B 扩展中的add.uw 指令.具体地,目标指令add.uw r,r,x0 可对目标寄存器r 的0~31 b 进行0 扩展.

可进一步利用编译优化技术中的数据流分析技术节省冗余的合规化操作.具体地,采用数据流分析技术可分析每条指令执行后其目标寄存器是否已满足“高32 b 为0”的条件,得知执行访存指令前地址是否已合规化.若是,则无需生成用于合规化的目标指令.

4.4 基于RISC-V B 扩展指令的复杂指令优化方法

一些源指令的行为较复杂,如x86 中的bswap 指令需对源寄存器按字节反向排序.若仅翻译到RISC-V64的基础ISA,将花费约10 条目标指令;若使用RISC-V B扩展中的rev8 指令实现该功能,则可大幅度提升翻译质量.类似情况还有置位计数指令(如x86 中的popcnt指令)、前导0 和尾随0 计数指令、循环移位指令等,皆可采用RISC-V B 扩展中行为相同的指令作为目标指令.

4.5 基于RISC-V B 扩展指令的有效地址计算方法

为优化有效地址计算,需考虑如何高效计算“基址+变址×比例因子”.QEMU-i386 先通过移位指令计算“变址×比例因子”,再与基址相加得到有效地址.此方法需花费2 条目标指令.

为优化上述操作,可采用RISC-V B 扩展中的sh1add,sh2add,sh3add 指令直接计算“基址+变址×比例因子”.上述指令在加法前分別对变址寄存器的值左移1 b,2 b,3 b,即分别乘以2,4,8,故可节省1 条用于移位的目标指令.

4.6 基于RISC-V B 扩展指令的x86 寄存器访问方法

读取x86 寄存器的字节和半字需抽取该寄存器部分数据到另一寄存器的低位.对于半字读取,QEMU-i386 对该寄存器左移16 b,移出高16 b,再对结果的低32 b 逻辑右移16 b,从而把原来的16~63 b清0,实现抽取0~15 b 的效果.此方法需花费2 条目标指令.为优化上述操作,可预先在某目标寄存器中存放低16 b 的掩码,即0x000000000000ffff,再将此掩码与代抽取寄存器进行与操作得到其低16 b,此方法只需花费1 条目标指令.为进一步节省该目标寄存器,可利用RISC-V B 扩展中的zext.h 指令.具体地,目标指令zext.h rd,rs 可对寄存器rs 的低16 b 进行0 扩展并写入寄存器rd.

对于字节读取,又分从0~7 b 读取(如AL 寄存器)和从8~15 b 读取(如AH 寄存器)2 种情况.从0~7 b 读取的情况较简单,其掩码为0x00000000000000ff,可通过12 b 有符号数表示,故可通过目标指令andi rd,rs,0xff 实现,无需进一步优化.要从8~15 b 读取,则需先对寄存器右移8 b,再借助上述目标指令抽取结果的低8 b,共计2 条目标指令.为进一步优化,可利用RISC-V B 扩展中的xperm.b 指令.如图6(a)所示,指令xperm.b rd,rs1,rs2 根据寄存器rs2 中的索引向量对寄存器rs1 中的值以字节为单位查找并重组,若索引向量中的元素大于7,则结果向量中相应位置为0.故可通过目标指令序列li r,0xffffffffffffff01;xperm.b rd,rs,r 实现“从寄存器rs 读取8~15 b 到寄存器rd”的功能.具体地,0xffffffffffffff01 为xperm.b 所用索引向量,其功能为“将寄存器rs 中索引为0x01 的字节写入寄存器rd 的第0 字节,并将rd 其余字节清0”.将该索引向量预先存放到某目标寄存器,可节省用于加载立即数的指令li,从而通过一条xperm.b 指令读取x86 寄存器8~15 b 的字节.

Fig.6 Behaviors of xperm.b instruction and the pack instruction in RISC-V B extension图6 RISC-V B 扩展中 xperm.b 指令和 pack 指令的行为

写入操作更复杂,因为写入x86 寄存器的字节和半字要求其他字节保持不变.以半字写入操作为例,需先将写入目标的0~15 b 清0,再将待写入数据的16~63 b 清0,最后对二者进行或操作,QEMU-i386 为该过程翻译出5 条目标指令.为优化此过程,可利用RISC-V B 扩展中pack 和xperm.b 指令,其中pack 指令的行为如图6(b)所示.首先通过pack 指令把写入目标的0~31 b 和待写入数据的0~31 b 拼接起来,再借助合适的索引向量让xperm.b 指令从拼接结果中选择正确的字节并重组,得到写入操作的结果.通过pack 指令拼接后,写入目标的数据位于拼接结果的第2 字节(从0 开始计数,下同)和第3 字节,待写入数据位于拼接结果的第4 字节和第5 字节,故令索引向量为0xffffffff03020504.同理可实现0~7 b 的写入和8~15 b 的写入,索引向量分别为0xffffffff03020104和0xffffffff03020400.将索引向量预先存放到某目标寄存器,可通过2 条目标指令实现x86 寄存器的字节和半字写入操作.

若采用RISC-V P 扩展,则可通过1 条目标指令实现x86 寄存器的字节和半字写入操作.对于字节写入操作,可通过insb 指令把待写入字节插入到写入目标中的正确位置;而对于半字写入操作,可通过pktb16 rd,rs1,rs2 指令将rs1 的16~31 b 与rs2 的0~15 b 进行拼接.但RISC-V P 扩展的标准规范仍未完全冻结,其指令语义还可能变化.

5 实验评估

5.1 目标指令数量评估

本文首先在模拟器上评估翻译出的RISC-V64指令数量.本文选择Spike 模拟器[28]作为RISC-V64模拟器,它被RISC-V 基金会指定为RISC-V 指令行为的标准参考实现.本文在Spike 模拟器的基础上新增了一个动态二进制翻译模块,得到DBT-Spike.DBTSpike 可在运行时刻从内存中读出x86 指令并译码,然后生成RISC-V64 目标指令并执行.DBT-Spike 还为用户态模式提供运行时环境的支持,包括加载x86程序以及系统调用的捕获和转发.

本文选择SPEC CPU2006 的整数基准程序,使用编译目标为i686-linux-gnu 的GCC 10.2.1 编译基准程序,编译选项为-O2 和-static,其中使用-static 是因为DBT-Spike 暂不支持加载动态链接的程序.该编译目标将编译出静态链接的32 b x86 程序,这些程序将会以用户态模式在DBT-Spike 上运行,DBT-Spike 将统计执行的RISC-V64 指令数量,从而体现动态二进制翻译技术的翻译质量.本文采用test 输入规模来运行SPEC CPU2006 的整数基准程序.实验评估不采用浮点基准程序,一方面是因为QEMU 通过纯软件模拟方法翻译浮点指令,难以准确统计其动态目标指令数量;另一方面,本文的优化技术主要针对整数指令的翻译.

图7 展示依次添加各项优化技术后,DBT-Spike通过动态二进制翻译方式执行基准程序所花费的动态目标指令数量,其中以QEMU-i386 的动态目标指令数量作为归一化的基准,数值越小表示优化效果越好.具体地,在这一基准上,“兼容RISC-V64 ABI的目标寄存器分配方案”平均节省41%的动态目标指令数量,对于483.xalancbmk 甚至可节省48%的动态目标指令数量.在此基础上采用“基于PIE 的虚拟地址空间复用方法”,可平均节省总计48%的动态目标指令数量.进一步优化地址合规化,则可平均节省总计56%的动态目标指令数量.继续对复杂指令、有效地址计算方法和x86 寄存器访问方式进行优化后,最终可平均节省总计57%的动态目标指令数量,说明这3 种优化技术在选定的基准程序上的优化效果不明显.分析后发现,这是因为在基准程序的热点基本块中,复杂指令、复杂寻址模式以及x86 字节和半字访问频率较低.以复杂指令为例,占比最多的是400.perlbench 的pack 输入集,其复杂指令占源程序动态指令数的比例达到0.13%;对于大部分基准程序,该比例均小于0.000 1%.

Fig.7 Number of dynamic target instructions after applying optimization for binary translation图7 使用二进制翻译优化后的动态目标指令数量

5.2 性能评估

本文在现场可编程门阵列(field programmable gate array,FPGA)平台上评估动态二进制翻译优化技术的性能.为此,本文采用开源乱序超标量RISC-V处理器BOOM[29]作为目标处理器,并使用LargeBOOM的配置,其流水线的具体配置如表3 所示.LargeBOOM还分别配备32 KB 一级指令缓存和32 KB 一级数据缓存,以及1 MB 的二级联合缓存.此外,本文在其流水线中实现了扩展指令.目标处理器被综合并下载到Xilinx zu19eg FPGA[30]中,主频为80 MHz.为缓解FPGA 中内存主频比CPU 主频高的问题,本文在访存通路上额外添加90 周期延迟,以得到更接近真实芯片场景的性能数据.

Table 3 Pipeline Configuration of LargeBOOM表3 LargeBOOM 流水线配置

为在目标处理器上评估本文提出的动态二进制翻译优化技术,本文将DBT-Spike 修改成翻译程序DBT-FEMU.DBT-FEMU 在翻译出目标指令后,将切换到执行模式,并跳转到目标指令执行;而在遇到未翻译的基本块时,将切换到翻译模式对基本块进行翻译.同时,DBT-FEMU 实现了块链接技术和条件码惰性求值技术.

本节用于性能评估的基准程序及其编译方法与5.1 节相同.同样的x86 基准程序将运行在DBTFEMU 和QEMU-i386 6.2.0 中,二者均可将x86 指令翻译为RISC-V64 指令,并在相同的目标处理器上运行.此外,目标处理器上将启动RISC-V64 版本的Linux 5.6.0 操作系统内核,并运行Debian 12 发行版,DBT-FEMU 和QEMU-i386 将在该发行版上运行,系统层次如图8 所示:

Fig.8 System stack of FPGA platform图8 FPGA 平台的系统栈

图9 展示了QEMU-i386 和DBT-FEMU 运行基准程序的性能.考虑总体性能,QEMU-i386 的平均运行效率是本地的8.04%;而DBT-FEMU 的平均运行效率达到本地的33.54%,是QEMU-i386 的4.17 倍.其中,执行429.mcf 时,DBT-FEMU 的效率甚至达到本地运行效率的1.12 倍.这是因为429.mcf 的核心数据中包含指针,在RISC-V64 的本地程序中,429.mcf 大小是x86 程序的2 倍,因此x86 程序的工作集比RISC-V64本地程序小,对目标处理器数据缓存的压力也更小.此外,执行456.hmmer 时,DBT-FEMU 的效率是QEMU-i386的6.90 倍,这是因为虽然456.hmmer 属于整数基准程序,但其运行过程中需执行不少浮点指令.据统计,执行x86 的456.hmmer,其动态浮点指令的比例达到所有动态指令的3.30%,而对其他整数基准程序来说,该比例不足1%.故456.hmmer 放大了QEMU-i386 执行浮点指令较慢的效果,使执行456.hmmer 时DBT-FEMU相对QEMU-i386 的效率高于执行其他基准程序.

Fig.9 Performance ratio of DBT-FEMU and QEMU-i386图9 DBT-FEMU 与QEMU-i386 的性能比例

考虑优化技术点,以QEMU-i386 作为基线,“兼容RISC-V64 ABI 的目标寄存器分配方案”平均贡献了2.64 倍的性能提升,因为该技术直接将源寄存器分配到目标寄存器,优化了冗余的目标访存指令.在此基础上,优化段式地址转换可额外提升5.98%的性能;继续优化地址合规化可再次提升5.39%的性能.但优化复杂指令几乎未带来性能提升(小于 0.01%),这是因为复杂指令的占比很低.在此基础上,优化有效地址的计算可额外提升2.44%的性能;若继续对x86 寄存器的字节和半字访问进行优化,则性能只提升0.01%,这是因为在热点基本块中x86 寄存器的字节和半字访问频率较低.

总体而言,与寄存器分配方案的优化相比,其他优化技术只优化了整数计算指令.而与访存指令相比,目标处理器执行整数计算指令的开销较小,因此带来的性能提升不如寄存器分配方案的优化明显,但对于翻译质量的提升仍然有重要意义.

6 总结

本文以RISC-V64 作为目标ISA,分析了当RISCV64,RISC-V32,MIPS32,x86 分别作为源ISA 时,影响动态二进制翻译技术翻译质量的因素.针对该因素,本文分别提出相应的优化方法,并借助RISC-V B 扩展和RISC-V P 扩展中的部分指令提升翻译质量.评估数据显示,在运行SPEC CPU2006 的整数基准程序时,本文优化方法可使目标程序所执行动态指令数平均减少57%,平均性能达到QEMU-i386 的4.12 倍.

作者贡献声明:余子濠提出了文章总体思路,实现部分优化技术,分析数据,撰写和修订论文;陈璐负责实现扩展指令、运行实验、统计数据和修订论文;孙凝晖和包云岗负责指导论文撰写和论文审阅.

猜你喜欢
二进制字节寄存器
用二进制解一道高中数学联赛数论题
No.8 字节跳动将推出独立出口电商APP
Lite寄存器模型的设计与实现
有趣的进度
二进制在竞赛题中的应用
No.10 “字节跳动手机”要来了?
简谈MC7字节码
分簇结构向量寄存器分配策略研究*
高速数模转换器AD9779/AD9788的应用
一个生成组合的新算法