2. 中科南京软件技术研究院 智能软件研究中心, 南京 211100
2. Intelligent Software Research Center, Nanjing Institute of Software Technology, Nanjing 211100, China
SIMD (single instruction multiple data)技术[1,2]是一种基于向量运算的并行计算技术, 可以同时对多个数据进行相同的操作. 它通过将多个数据打包成一个向量, 然后对整个向量进行计算, 从而实现了单个指令周期内并行处理多个数据的能力. 在计算机科学领域, SIMD技术最初用于数字信号处理、图像处理和视频编解码等领域, 以提高运算速度和效率. 现在, 随着计算机硬件的发展, SIMD技术也被广泛应用于其他领域, 如科学计算、机器学习和数据处理等. 除了向量寄存器, 现代CPU还配备了许多SIMD指令, 以便处理不同的数据类型和操作. 例如, ARM架构的NEON指令集、Intel架构的MMX/SSE指令集、RISC-V架构的向量指令集等.
RISC-V指令集[3]是一种新兴的开源指令集, 旨在提供一个可扩展、可定制、高效的架构. 它的发展始于2010年, 起源于加州大学伯克利分校(UC Berkeley)的一个研究项目. 该指令集最初是为了提供一个适合教学和研究的指令集而设计的, 但现在已经成为一个面向商业应用的实用指令集. RISC-V指令集包含基础指令集和扩展指令集两部分, 包括基础指令集I、乘法和除法指令集M、原子指令集A、单精度浮点指令集F、双精度浮点指令集D、位操作指令集B、向量操作指令集V等.
musl libc[4]是一个轻量级的C标准库实现, 旨在提供高性能、低复杂度、可移植性和安全性. 与其他主流的C标准库实现(如glibc和uclibc)相比, musl libc具有更小的代码库和更快的启动时间, 同时也更容易维护和制定. 由于其小巧、高效和安全的特性, musl libc被广泛应用于嵌入式系统、操作系统和各种应用中. musl libc支持多种体系结构, 包括X86、ARM、MIPS、PowerPC、RISC-V等. 目前RISC-V的向量扩展指令集仍处于冻结状态, 虽然有了编译工具链和模拟器的向量支持, 但对于musl libc库的向量扩展优化还不够成熟, 软件生态亟待发展.
字符串与内存操作函数在许多应用程序中都会被频繁调用, 往往会涉及大量的数据处理任务, 且对于性能要求较高. 在glibc、musl libc中X86、ARM等架构均使用汇编语言更加高效地实现了这些操作, 而对于RISC-V架构仍使用的是C语言实现的函数. strlen与memset函数是字符串与内存操作函数中常被向量化优化的函数之一, 因此, 本文选取这两个函数进行向量扩展优化, 并利用内置宏解决硬件不同导致的指令集支持具有差异的兼容性问题, 采用了基础指令集和向量指令集的两种函数汇编实现, 最大程度地提升字符串函数和内存操作函数的执行效率和性能, 并在gem5模拟器上进行了性能测试.
2 RISC-V向量扩展指令集RISC-V是一个新兴的指令集架构, 吸引了广泛的关注和采用[5,6]. RISC-V指令集由基础指令集和扩展指令集组成[7]. 基础指令集中定义了x0–x31共32个通用寄存器. 每个寄存器都有其对应的用途. 如表1所示.
在扩展指令集中, RISC-V的向量指令集(RVV)为其提供了高效的SIMD支持[8,9]. RVV是一个独立的寄存器文件, 支持8位、16位、32位、64位等数据类型的向量运算. 向量扩展增加了32个向量寄存器v0–v31, 每一个向量寄存器有VLEN的位宽, 以及7个非特权状态控制寄存器, 如表2所示. RISC-V向量指令集具有3个特性: 向量长度不可知、寄存器分组、向量指令支持掩码操作.
2.1 向量长度不可知
RISC-V的向量寄存器位宽VLEN是由具体的硬件实现来定义的, VLEN的范围在25–216之间.
由于VLEN的值在编译阶段无法获得, 因此用户可以通过csrr指令读取控制状态寄存器vlenb中的值, 从而获得向量寄存器的位宽. 在SIMD编程中, 往往需要考虑尾部处理, RISC-V中可使用配置指令vsetvli或者vsetvl动态配置运行时向量的宽度, 可通过设置vl修改感兴趣的向量长度, 这给自动向量化以及向量化编程带来了极大的便利.
2.2 寄存器分组支持寄存器分组是RISC-V向量扩展指令集(RVV)的一个重要特性, 它为向量操作提供了更灵活的数据宽度选择. 在传统的向量指令集中, 向量长度和数据宽度通常是固定的, 而在RVV中, 向量长度和数据宽度都可以由软件在运行时进行配置. 这为编程者提供了更大的灵活性和自由度, 使得向量计算可以更好地适应不同的场景和应用需求. RISC-V的向量寄存器组支持不同数量的寄存器, 包括1个、2个、4个和8个, 这取决于LMUL参数的设置[10]. 寄存器组的作用是扩展寄存器的位宽, 但相应地会减少可用的寄存器数量. 举个例子, 当LMUL设置为8时, 只有v0、v8、v16和v24这4个寄存器可用.
如果指令使用了不存在的向量寄存器(如v1), 将会引发非法指令异常. 图1和图2是分别设置LMUL为4和8时寄存器分布情况.
2.3 掩码操作
RISC-V向量指令掩码操作是一种在RISC-V架构中使用掩码向量来控制向量操作的技术. 可以实现对向量数据的选择性处理, 提高数据处理的灵活性和效率. 掩码操作的基本原理是使用一个掩码向量来指示对应位置的操作是否应该执行. 掩码向量的每个元素表示对应位置的操作是否有效, 当掩码向量中的元素为非零值时, 对应位置的操作将被执行; 当掩码向量中的元素为零值时, 对应位置的操作将被跳过. 这种灵活的条件选择机制使得向量操作可以根据实际需求进行动态配置和选择性处理.
3 基于musl libc的RVV优化
字符串与内存处理函数在ARM、X86等架构中均有汇编实现, 是基础C库常优化的函数, 但对于RISC-V, 此方面的优化还很欠缺, 因此本文选取常见的函数strlen、memset进行优化, 详述了优化原理.
3.1 RVV兼容方案由于RISC-V指令集的可扩展性, 这导致了向量指令优化的函数无法运行在不支持向量扩展的硬件上. 目前, 在Linux系统中常用hwcap (hardware capabilities)机制检测和标识硬件的特性和功能. hwcap机制通过定义了一组位标志来表示不同的硬件功能. 这些位标志被编码为一个或多个特定的寄存器或内存位置, 并且由操作系统内核在系统启动时进行设置. 由于目前RISC-V内核hwcap机制支持不够完善, 且运行时动态读取硬件配置会带来额外的开销, 因此, 本文使用gcc预定义宏__riscv_vector在编译阶段检查是否支持向量指令集扩展, 以决定是否使用向量化代码路径来提高性能, 同时实现仅包含基础指令集和RVV优化的两个版本汇编. 具体兼容实现如下所示.
代码清单1. RVV兼容实现
#ifdef __riscv_vector
//RVV优化汇编实现
#else
//基础指令汇编实现
#endif
3.2 strlen函数优化 3.2.1 strlen基础指令集实现strlen函数是C标准库中的一个字符串处理函数, 用于计算一个字符串的长度(即字符的个数), 不包括字符串末尾的空字符(“\0”). 它的函数原型为size_t strlen(const char* str), 参数str是一个指向以空字符结尾的字符串的指针[11]. 函数会从给定的字符串的开头开始遍历, 直到遇到空字符为止, 然后返回计算得到的字符串长度作为无符号整数(size_t类型).
实现一个strlen的函数功能, 最简单的就是逐字节进行判断是否为终止符“\0”, 这种做法实现逻辑相对简单且容易理解, 但当字符串较长时会增加循环次数导致性能较低, 逐字节判断无法充分利用现代处理器的并行能力, 无法同时处理多个字符. 因此, 本文同时处理8个字节, 利用魔法数0xfefefefefefefeff和0x8080808080808080更加快速高效地找到终止符并返回字符串的长度, 算法流程图如图3所示.
利用基础指令实现的strlen函数算法如算法1.
算法1. 基础指令实现strlen函数
(1) 首先判断字符串首地址是否为8字节对齐;
(2) 若首地址不对齐, 则先逐一取单个字节数据进行判断是否为终止符, 若是终止符则直接返回长度; 否则直至取到地址8字节对齐处, 然后跳转步骤(3);
(3) 若首地址对齐, 直接取长度为8字节的数据data, 然后利用魔法数对data进行处理(data+0xfefefefefefefeff)&~data&0x8080808080808080, 若处理后的8字节仍为空, 则继续循环查找下一个8字节数据; 若不为空, 则8字节中存在终止符, 在这个8字节中逐一查找终止符, 得到字符串长度返回.
3.2.2 strlen RVV指令集实现利用魔法数虽能够一次处理8个字节的数据, 但随着RISC-V RVV指令集的发布, 这对字符串类函数优化有了新的方向. RVV扩展提供了丰富的向量操作指令, 包括向量加载和存储指令、向量算数指令、向量掩码操作等, 这些指令能够更加有效地利用数据并行性, 减少指令的数量和执行时间. 结合RVV扩展指令的丰富性, strlen向量指令集实现算法流程图如图4所示.
利用RVV指令实现的strlen函数算法如算法2.
算法2. RVV指令实现strlen函数
(1) 首先读取vlenb寄存器得到向量寄存器位宽, 判断字符串首地址是否按向量寄存器位宽对齐;
(2) 若首地址不对齐, 则先逐一取单个字节数据进行判断是否为终止符, 若是终止符则直接返回长度; 否则直至取到向量寄存器位宽对齐处, 跳转至步骤 (3);
(3) 若首地址对齐, 设置vl寄存器为最大向量长度(参数SEW=8、LMUL=8), 加载LMUL×VLEN长数据并和0比较, 相同则置1、不同置0, 查找比较后的结果是否存在1, 有则表明取出的数据包含终止符, 计算字符串长度返回, 没有则继续循环.
3.3 memset函数优化 3.3.1 memset基础指令集实现memset函数是C标准库中的一个常用的内存操作函数. 它的作用是将内存区域的每个字节都赋值为相同的值, 常用于初始化内存、清零内存或填充内存区域. memset函数的函数原型为void *memset(void *s, int c, size_t n), 其中s是要设置值的内存起始地址, c是要设置的值, n是要设置的字节数.
由于memset函数设置的字节数不固定, 往往函数实现时会根据不同的数据量采用不同位宽的字节操作指令, 比如musl中memset.c中其根据数据量大小分别采用了单字节存储、4字节存储、8字节存储. 本文利用RISC-V指令特性以及编译优化技术, 采用基础指令实现更加高效的memset函数.
循环展开: 在memset函数中, 循环展开的目的是将字节填充操作展开成多个重复的指令序列, 以提高执行效率. 通过将循环体内的代码重复多次, 减少了循环的迭代次数, 降低了循环控制开销, 同时利用处理器的指令并行和高速缓存, 改善了内存访问模式, 减少对内存的访问延迟. 因此, 本文针对核心循环段展开32次, 核心循环代码段如下所示.
代码清单2. memset循环展开
loop:
sd a1, 0(t0)
sd a1, 8(t0)
sd a1, 2*8(t0)
sd a1, 3*8(t0)
… //循环展开
sd a1, 29*8(t0)
sd a1, 30*8(t0)
sd a1, 31*8(t0)
sd t0, t0, 32*8
bltu t0, a3, loop
地址跳转: 由于循环展开了32次, 则要求数据量至少是256字节, 因此本文针对数据量小于256字节时, 计算其需要展开的次数以及与loop循环段的地址偏移, 直接跳转到loop内执行, 这样数据量小于256字节时仍能够循环展开, 部分代码如下所示.
代码清单3. memset地址跳转
/* 判断能否做32次循环展开 */
andi a4, a4, 31*8
beqz a4, loop
/*计算与loop段的偏移*/
neg a4, a4
addi a4, a4, 32*8
sub t0, t0, a4
/* 加载loop循环段地址, 并跳转至loop内*/
la a5, loop
srli a4, a4, 1
add a5, a5, a4
jr a5
尾部处理: 当剩余字节无法循环展开存储, 往往采用的方式是逐字节存储, 本文利用双指针思想, 从剩余字节的头和尾部进行存储, 这种做法虽然会产生重复的存储, 但指令并行与减少跳转次数会带来更大的收益, 部分代码如下所示.
代码清单4. memset尾部处理
sb a1, 0(t0) //头部存储
sb a1, –1(a3) //尾部存储
li a4, 2 //数据量判断
bgeu a4, a2, 6f
…
sb a1, 5(t0)
sb a1, 6(t0)
sb a1, –6(a3)
sb a1, –7(a3)
li a4, 14
bgeu a4, a2, 6f
sb a1, 7(t0)
3.3.2 memset RVV指令集实现RVV指令集特性以及传输指令使得memset向量化编程实现更为简洁化. 其中vmv指令能够高效地在向量寄存器之间以及向量与标量之间传输和复制数据, 同时配合vsetvli指令控制复制的个数, 就不需要像memset基础指令集实现那样考虑数据量不同的处理和尾部处理. 核心循环代码如下.
代码清单5. memset RVV指令集实现
loop:
vsetvli t1, a2, e8, m8, ta, ma
vmv.v.x v0, a1
sub a2, a2, t1
vse8.v v0, (t0)
add t0, t0, t1
bnez a2, loop
4 测试结果及分析由于目前RISC-V硬件支持RVV不够完善, gem5上游中科院软件所PLCT实验室提交的RVV补丁正在review, 还未完全合入, 因此本文使用了下游中科院软件所PLCT实验室gem5的代码仓[12], 并在gem5模拟器上测试, 具有一定的参考价值. 测试集采用了ARM官方提供的测试用例[13], 由于该测试集中包含了对ARM接口函数的调用, 本文将其注释以便在模拟器上运行.
4.1 strlen函数性能测试性能测试结果如表3–表5所示, 其中small aligned测试了首地址对齐且数据量较小时的性能、small unaligned测试了首地址不对齐且数据量较小时的性能、medium测试了数据量中等时的性能, 值越大代表着性能越好.
从测试结果来看, 基础指令实现的strlen与musl C语言实现性能相当, 这是因为两者的算法相同导致, 这对于只支持基础指令集的RISC-V硬件也可以获得与C实现相当的性能; RVV实现的strlen相比于C实现, small aligned测试性能平均提升83%, small unaligned测试性能平均提升98%, medium测试性能平均提升703%.
4.2 memset函数性能测试
性能测试结果如表6–表8所示, 其中random测试了在对应数据量附近时的性能、medium测试了数据量中等时的性能、large测试了数据量较大时的性能, 值越大代表着性能越好.
从测试结果来看, 无论是基础指令还是RVV实现的memset, 其性能均好于musl库中C语言实现的memset, 一方面基础指令实现的性能提升得益于巧妙地利用循环展开、地址跳转、尾部处理等编程优化, 而这些对于编译器则是无法生成的, 在random测试中, 基础指令集实现相比于C实现性能提升了20%, 在medium测试中性能提升了69%, 在large测试中性能提升了88%; 另一方面RVV实现的memset性能最优, 得益于RVV指令集的丰富性, 能够最大粒度地并行处理数据, 在random测试中, RVV实现相比于C实现性能提升了85%, 在medium测试中性能提升了222%, 在large测试中性能提升了334%.
5 结语
针对目前musl libc库RVV扩展优化还不完善, 提出了基础指令集与RVV扩展实现并存的解决方案, 详细介绍了strlen与memset函数的基础指令集与RVV扩展实现的算法流程, 从实验结果看出, RVV优化的函数具有很大的性能提升. 由于基础C库的函数还有很多, 在未来工作中将结合RVV扩展优化其他函数, 丰富RISC-V基础软件生态.
[1] |
冯竞舸, 贺也平, 陶秋铭. 自动向量化: 近期进展与展望. 通信学报, 2022, 43(3): 180-195. DOI:10.11959/j.issn.1000-436x.2022051 |
[2] |
高伟. 面向SIMD的自动向量化优化技术研究[硕士学位论文]. 郑州: 解放军信息工程大学, 2013.
|
[3] |
Waterman A, Lee Y, Avizienis R, et al. The RISC-V instruction set. Proceedings of the 2013 IEEE Hot Chips 25 Symposium. Stanford: IEEE, 2013. 1.
|
[4] |
musl libc官网. http://www.musl-libc.org/. (2023-05-01)[2023-05-05].
|
[5] |
Bakthavatsalam G, Mehata KM. A case for hybrid instruction encoding for reducing code size in embedded system-on-chips based on RISC processor cores. Journal of Computer Science, 2014, 10(3): 411-422. DOI:10.3844/jcssp.2014.411.422 |
[6] |
王诲喆, 唐丹, 余子濠, 等. 开源芯片、RISC-V与敏捷开发. 大数据, 2019, 7(4): 50-66. DOI:10.11959/j.issn.2096-0271.2019032 |
[7] |
刘畅, 武延军, 吴敬征, 等. RISC-V指令集架构研究综述. 软件学报, 2021, 32(12): 3992-4024. DOI:10.13328/j.cnki.jos.006490 |
[8] |
Asanović K. Vector microprocessors [Ph.D. Thesis]. Berkeley: University of California, 1998.
|
[9] |
RISC-V 向量扩展规范. https://github.com/riscv/riscv-v-spec. (2021-09-18)[2023-05-05].
|
[10] |
叶锡聪, 庄灿锋, 王宇木, 等. RISC-V向量指令集的Compute Library函数库移植. 单片机与嵌入式系统应用, 2021, 21(1): 8-13. |
[11] |
李恺, 翁玉萍. 基于龙芯2F的Glibc库优化. 电子技术, 2010, 37(10): 27-29. |
[12] |
gem5模拟器. https://github.com/plctlab/plct-gem5. (2023-04-25) [2023-05-05].
|
[13] |
ARM官方测试集. https://github.com/ARM-software/optimized-routines/tree/master/string/bench. (2022-02-10)[2023-05-05].
|