对于内存受限的嵌入式芯片(包括 MCU 和成本要求的 AP 类芯片)来说,代码密度非常重要。同样功能的程序,如果代码密度过大,就可能导致因 ROM 空间装载不下而无法使用。所在,在嵌入式领域中,代码密度是最重要的指标之一。那么,代码密度由什么决定?如何提高代码密度呢?RISC-V 的代码密度现状又如何?
代码密度的决定因素
如上面的倒金字塔所示,代码密度主要由指令集、ABI、编译器、Runtime 库、程序代码五个部分决定。处在金字塔的越底端,说明该因素的越底层,更新的频率越小,但辐射和影响的范围却越广。
指令集 指令集是代码密度最根本的决定性因素,它决定了一个操作在最优的情况下需要编译成多少位宽的编码。
很多体系结构比如 ARM、RISC-V、C-SKY 都是 16 位指令、32 位指令混编的,同样的一条指令,如果能够被编译成 16 位指令,那么它显然比编译成 32 位指令占用更小的空间;再比如,一个乘累加的操作,如果指令集中存在乘累加指令,那么它只需要一条指令来实现乘累加操作,如果没有则需要至少两条指令来完成相同的操作,假设指令都是 32 位的,显然一条指令将占用更少的空间。
由于指令集的编码空间是有限的,所以指令集设计的核心是将哪些指令(包括指令操作数的范围)放到编码空间当中,就像一个商场的店面是有限的,当我们把需求最广的商家引进来时,商场的销量就会达到最高。
ABI ABI 的全称是 Application Binary Interface,是二进制级别的协议,它指导着编译器如何生成代码和二进制程序,同样也指导着用户如何写汇编代码。它主要包含函数调用约定(calling convention)、数据的对齐方式等内容。
其中对代码密度影响最大的就是函数调用约定,它规定了堆栈寄存器、链接寄存器、哪些寄存器寄存器需要在函数头尾保存和恢复、哪些寄存器可以作为参数寄存器等,还有一些特殊用途的寄存器。大部分特殊寄存器都是会被高频使用的,配合指令集设计可以降低代码密度;需要保存和恢复的寄存器个数同样也会影响代码密度。
编译器 编译器是开发者最直接接触的工具,也是给开发者体感最强的代码密度影响因素。它对代码密度的影响主要体现在两方面: 编译器本身的优化能力,优化能力的强弱是影响编译器产品竞争力的最主要的因素。 编译器的使用方法,比如 GCC,除了添加-Os 之外,还可以添加-ffunction-sections -fdata-sections -Wl,--gc-sections 来删除没有用到的函数。
Runtime 库 Runtime 库是指程序运行所需的一些基本的函数库,它们一般都是预先编译好,和编译器一起打包发布,是工具链的一部分。由于这些函数的使用频率较高,一般程序都会用到一部分 Runtime 库的函数,对这些函数做针对性地优化会有比较好的收益。
程序代码 开发者书写的代码质量也会影响程序的代码密度,虽然编译器能够优化一部分冗余代码,但是并不能保证百分之百的优化,所以开发者也要注意代码的质量。
RISC-V 架构的代码密度现状 RISC-V 的代码密度表现一直被人诟病,那么,它的现状真的这么不值一提吗?
首先,RISC-V 对代码密度做过一些专门的优化。在指令集方面,它通过量化分析的方法测试了 spec 等 benchmark,找到高频指令并将它们放到 16 位指令的编码当中,这就是目前的 compress 指令集;在 ABI 方面,rv32e 通过限制 16 个寄存器,使代码可以生产更多的 16 位指令;在编译器和 Runtime 中,它支持-msave-restore 功能通过库函数调用的方式弥补了由于没有 push/pop 指令造成的一部分代码密度损失。这里需要特别指出的是,RISC-V 的链接器做了较多的 relax 优化,即某些指令的目标符号距离比较接近的时候,可以优化使用更少的指令。比如函数跳转,比如具体在 4k 之内,可以使用一条 jalr 指令实现,而如果超过 4k 的话,则需要 auipc+jalr 或者 lui+jalr 两条指令实现。一般在未链接的 object 文件中,预留的都是指令条数最多的形式,链接之后很大一部分将被优化。如果测试的 benchmark 如果统计的是 object 文件,比如 CSiBE,那么结果会比实际的要差一些。
那么,在做了这些优化之后,为什么 RISC-V 的代码密度还是被这么多人诟病呢?主要是由于 compress 指令集设计时,基于的 benchmark 是 spec2006,它在应用 PC 端非常具有代表性,但在嵌入式领域却不具有典型性。所以在 RISC-V 64 位核中,它的代码密度表现还不错,但是在真正关心代码密度的嵌入式领域表现却不尽如人意。其次,ABI 也需要重新针对嵌入式领域做量化评估和设计。
平头哥对 RISC-V 的代码密度优化 针对代码密度,RISC-V 社区目前也在不断地优化中,比如 code-size TG 和 EABI TG 的成立。平头哥也参与其中,在优化代码密度的道路上不断前行。目前,平头哥所做的优化有如下两个方面:
Runtime 库 平头哥设计开发了一套针对嵌入式领域的、最大化优化代码密度的 Lower-Level Runtime Library。目前玄铁 E902、玄铁 E906 和玄铁 E907 均已支持该 Runtime 库,它相对于 libgcc 提升 40%;相对于 Arm 的 macrolib,玄铁 E902 与 M0-plus 相当,玄铁 E906、玄铁 E907 与 M4 相当。
ABI 目前,社区正在设计、制定针对嵌入式领域的新的 ABI——EABI(Embedded ABI),它不仅会调整 Calling Convention 以减少中断延迟,也会考虑代码密度,使用量化分析地方法设计出一套对嵌入式领域优化的 ABI。平头哥作为 EABI Task Group 的 Co-Chair,也参与其中推动 EABI 的前进。
作为平头哥玄铁系列科普文章之一,以上是本文关于 RISC-V 代码密度的介绍及平头哥在其中的优化成果的简介,更多关于 RISC-V 指令集优劣势分析等内容可参见后续文章。
|