3、RISC-V扩展指令集:M扩展(乘除法)、F/D扩展(单双精度浮点)、A扩展(原子操作)、C扩展(压缩指令)

各位同学,今天我们来聊聊RISC-V的扩展指令集。这部分内容,说白了就是让处理器“开挂”的关键。基础指令集(RV32I/RV64I)只能做加减、逻辑、跳转这些基本操作,但实际项目里,你肯定需要乘法、除法、浮点运算,或者多核之间的原子操作。嗯,这些功能就靠扩展指令集来提供。

我个人习惯把扩展指令集分成两类:一类是“算力增强型”,比如M扩展和F/D扩展;另一类是“系统优化型”,比如A扩展和C扩展。咱们一个一个来看。

3.1 M扩展:整数乘除法

M扩展是RISC-V里最常用的扩展之一。它提供了有符号和无符号的乘法、除法、取余指令。为什么需要它?因为基础指令集里只有加法,乘法要靠软件模拟,那效率简直没法看。

我在项目中遇到过这样一个坑:有个同事为了省面积,没实现M扩展,结果跑一个简单的矩阵乘法,软件模拟乘法占了90%的CPU时间。后来加上M扩展,性能直接提升了8倍。所以,除非你的芯片只做控制逻辑,否则M扩展几乎是必选的。

M扩展的指令不多,但每个都很关键:

指令 功能 说明
MUL 乘法(低32/64位) 取乘积的低半部分
MULH 有符号乘法(高32/64位) 取乘积的高半部分
MULHU 无符号乘法(高32/64位) 无符号版本
MULHSU 有符号×无符号乘法(高32/64位) 混合符号场景
DIV 有符号除法
DIVU 无符号除法
REM 有符号取余 余数
REMU 无符号取余 余数
注意:除法指令的延迟通常比乘法大得多。乘法一般1-3个周期就能出结果,但除法可能需要几十个周期。我曾经在项目中因为除法器没做流水线,导致整个CPU的CPI(每指令周期数)飙升。建议在微架构设计时,给除法器单独做一个“忙等待”机制,或者用迭代算法来分摊延迟。

3.2 F/D扩展:单双精度浮点

浮点运算,说白了就是处理带小数点的数。F扩展处理单精度(32位),D扩展处理双精度(64位)。这两个扩展的指令集结构非常相似,只是数据宽度不同。

你想想看,如果没有浮点扩展,你要用整数指令来模拟浮点加法,那代码量得有多大?而且精度还不好控制。我记得有一次做音频处理芯片,客户要求支持IEEE 754标准的浮点运算。如果不用F扩展,光一个浮点加法就要写几十行汇编,还容易出错。

F/D扩展的指令可以分为几类:

  • 算术运算:FADD、FSUB、FMUL、FDIV、FSQRT
  • 比较运算:FEQ、FLT、FLE(返回整数结果)
  • 类型转换:FCVT(整数↔浮点、单精度↔双精度)
  • 符号操作:FSGNJ(符号注入)、FMIN/FMAX
  • 乘加融合:FMADD、FMSUB、FNMADD、FNMSUB

关键点:乘加融合指令(FMA)是浮点扩展的亮点。它执行 a × b + c 的操作,而且只做一次舍入。这比先乘后加(两次舍入)精度更高,速度也更快。我在做科学计算芯片时,大量使用了FMA指令,性能提升了约30%。

浮点单元(FPU)的硬件实现比较复杂。我个人建议,如果芯片面积允许,尽量实现D扩展,因为很多软件库(比如OpenCV、TensorFlow Lite)都依赖双精度。如果面积受限,只做F扩展也可以,但要注意精度损失。

3.3 A扩展:原子操作

A扩展是为多核系统准备的。它提供了一组“读-改-写”的原子指令,确保在多核并发访问共享内存时,数据不会乱掉。

为什么会需要原子操作?假设两个核同时要对一个变量加1。如果没有原子操作,可能两个核都读到同一个值,然后各自加1写回去,结果只加了1次。这在实际系统中是灾难性的。

A扩展的核心指令是LR(Load Reserved)和SC(Store Conditional),它们配合使用可以实现各种原子操作:

// 原子加1的示例
loop:
    lr.w  t0, (a0)      // 保留加载
    addi  t0, t0, 1     // 加1
    sc.w  t1, t0, (a0)  // 条件存储
    bnez  t1, loop      // 如果存储失败,重试

除了LR/SC,A扩展还提供了AMO(Atomic Memory Operation)指令,比如AMOADD、AMOSWAP、AMOAND等。这些指令直接在内存地址上执行原子操作,不需要软件循环。我在做网络处理器时,大量使用了AMOADD来实现多核之间的计数器同步,效率非常高。

避坑指南:我曾经在实现LR/SC时,忘记处理“虚假失败”的情况。LR/SC可能会因为中断、异常或者缓存行冲突而失败。如果你的软件没有重试机制,原子操作就会“静默失败”,导致数据不一致。所以,一定要在软件层面加上重试循环。

3.4 C扩展:压缩指令

C扩展,全称是“压缩指令集扩展”。它的目标很明确:减小代码体积。RISC-V的基础指令是32位定长的,但很多指令其实用16位就能表达。C扩展就是把这些常用指令压缩成16位。

你想想看,在嵌入式系统里,Flash或者ROM的成本是很高的。如果代码体积能缩小30%-40%,那芯片成本就能降不少。我做过一个IoT芯片,Flash只有64KB,用了C扩展后,代码刚好塞进去。如果不用C扩展,就得换128KB的Flash,成本直接翻倍。

C扩展的压缩规则很有意思:

  • 寄存器限制:压缩指令只能使用x8-x15(s0-s1和a0-a5)这8个寄存器。因为编码空间有限,只能覆盖部分寄存器。
  • 立即数范围:压缩指令的立即数范围比32位指令小。比如加法指令,32位版本可以加任意32位数,但压缩版本只能加0-31的小立即数。
  • 指令类型:包括压缩的加载/存储(C.LW、C.SW)、压缩的算术运算(C.ADDI、C.SUB)、压缩的跳转(C.J、C.BEQZ)等。
压缩指令 对应32位指令 节省的比特数
C.LW LW 16位
C.ADDI ADDI 16位
C.J JAL 16位
C.BEQZ BEQ 16位

重要提示:C扩展并不是简单的“指令压缩”,它还会影响指令对齐。32位指令必须4字节对齐,但16位压缩指令可以2字节对齐。这意味着,你的取指单元(IFU)需要支持非对齐访问。我在移植一个开源核时,就因为这个对齐问题,不得不重写了取指逻辑。

嗯,到这里,四个扩展指令集就讲完了。M扩展让处理器能算乘法除法,F/D扩展让处理器能算浮点,A扩展让多核系统能安全共享数据,C扩展让代码体积更小。在实际项目中,这些扩展往往是组合使用的。比如一个AIoT芯片,可能同时需要M扩展(做矩阵乘法)、F扩展(做浮点推理)、C扩展(节省Flash空间)。

最后,我建议大家在设计自己的RISC-V核时,先把M扩展和C扩展加上,这两个性价比最高。F/D扩展和A扩展可以根据应用场景按需添加。记住,扩展指令集不是越多越好,而是越合适越好。