原子指令
RV32A有两种类型的原子操作:
- 内存原子操作(AMO)
- 加载保留/条件存储(load reserved/store conditional)
图6.1是RV32A扩展指令集的示意图:
图6.2列出了它们的操作码和指令格式
- AMO(atomic memory operation)指令对内存中的操作数执行一个原子操作,并将目标寄存器设置为操作前的内存值。原子表示内存读写之间的过程不会被打断,内存值也不会被其它处理器修改。
- 加载保留和条件存储(load reserved/store conditional)保证了两条指令之间的操作的原子性。加载保留读取一个内存字,存入目标寄存器中,并留下这个字的保留记录。而如果条件存储的目标地址上存在保留记录,它就把这个字存入地址。如果存入成功,它向目标寄存器写入0;否则写入一个非0的错误代码。
编程语言的开发者会假定体系结构提供了原子的比较-交换操作(compare and swap):比较一个寄存器中的值和另一个寄存器中的内存地址指向的值,**如果它们相等,将第三个寄存器中的值和内存中的值进行交换。**这是一条通用的同步原语,其它的同步操作可以以它为基础来完成。
- 仅管这样一条指令假如ISA看起来十分必要,它在一条指令中却需要3个源寄存器和1个目标寄存器,源操作数从两个增加到三个,会使得整数数据通路、控制逻辑和指令格式都变得复杂许多。
- RV32FD的多路加法(multiply-add)指令有三个源操作数,但它影响的都是浮点数据通路,而不是整数数据通路。
- 不过,加载保留和条件存储只需要两个源寄存器,用它们可以实现原子的比较交换。
用lr/sc实现内存字M[a0]的比较-交换操作。
# Compare-and-swap(CAD) memory word M[a0] using lr/sc.
# Expected old value in a1;desired new value in a2.
lr.w a3,(a0) # load old value
bne a3,a1,80 # old value equals a1?
sc.w a3,a2,(a0) # 相等则存入新的值
bnez a3,0 # 如果存入失败重新尝试
... code following successful CAS goes here ...
地址80: 比较-交换不成功
第一个例子使用加载保留lr.w/条件存储sc.w实现比较-交换操作。
第二个例子使用原子交换amoswap.w实现互斥。
- 另外还提供AMO指令的原因是,它们在多处理器系统中拥有比加载保留/条件存储更好的扩展性,例如可以用它们来实现高效的归约。AMO指令在I/O设备通信时也有用,可以实现总线事务的原子读写。
内存一致性模型
RISC-V具有宽松的内存一致性模型(relaxed memory consistency model),因此其它线程看到的内存访问可以是乱序的。图6.2中,所有的RV32A指令都有一个请求位(aq) 和一个释放位(rl)。
- aq被置位的原子指令保证其它线程在随后的内存访问中看到顺序的AMO。
- rl被置位的原子指令保证其它线程在此之前看到顺序的原子操作。
压缩指令
以前的ISA为了缩短代码长度而显著扩展了指令和指令格式的数量,比如添加了一些只有两个操作数的指令,减小立即数域等。但新的ISA为处理器和编译器增加了负担,同时也增加了汇编语言程序员的认知负担。
- RV32C采用了一种新颖的方法:每条短指令必须和一条标准的32位RISC-V指令一一对应。此外,16位指令只对汇编器和链接器可见,并且是否以短指令取代对应的宽指令由它们决定。
- 编译器编写者和汇编语言程序员可以幸福地忽略RV32C指令及其格式,它们能感知的是最后程序的大小小于大多数其它ISA的程序。图7.1是RV32C扩展指令集的图形化表示。
基于以下的三点观察,架构师成功地将指令压缩到了16位:
- 对十个常用寄存器(a0-a5,s0-s1,sp以及ra)访问的频率远超过其它寄存器。
- 许多指令的写入目标是它的源操作数之一。
- 立即数往往很小,而且有些指令比较喜欢某些特定的立即数。
因此,许多RV32C指令只能访问那些常用寄存器;一些指令隐式写入源操作数的位置;几乎所有的立即数都被缩短了,load和store操作只使用操作数整倍尺寸的无符号数偏移量。
图7.3列出了插入排序和DAXPY程序的RV32C代码。
插入排序中地址为4的地方
c.li a4,1 # (可扩展为 addi a4,x0,1) i = 1
RV32C的立即数指令比较短,因为它只能确定一个寄存器和一个小的立即数。
地址为10的地方
c.mv a2,a3 # 可扩展为add a2,x0,a3
在执行之前用一个解码器将所有的16位指令转换为等价的32位指令。
图7.6到7.8列出了解码器可以转换的RV32C指令的格式和操作码。
向量
- 当存在大量数据可供应用程序同时计算时,我们称之为数据级并行性。
- 最著名的数据级并行架构是单指令多数据(SIMD,Signle Instruction Multiple Data)。SIMD最初流行是因为它将64位寄存器的数据分成多个8位、16位或32位的部分,然后并行计算他们。操作码提供了数据宽度和操作类型。数据传输只用单个SIMD寄存器的load和store进行。
- 为了使SIMD更快,架构师随后加宽寄存器以同时计算更多部分。
- 由于SIMD ISA属于增量设计,并且操作码指定了数据宽度,因此扩展SIMD寄存器意味着要同时扩展SIMD指令集。
- 将SIMD寄存器宽度和SIMD指令数量翻倍的后续步骤让ISA更为复杂。
另一个利用数据级并行性的方案是向量架构。RISC-V使用向量。
- 向量计算机从内存中收集数据,并将它们放入长的,顺序的向量寄存器中。在这些向量寄存器中,流水线执行单元可以高效地执行运算。
- 接着,向量架构将结果从向量寄存器中取出,并将其分散地存回主存。
- 向量寄存器的大小有现实决定,而不是像SIMD中那样嵌入操作码。
- 将向量的长度和每个时钟周期进行最大操作数分离,是向量体系结构的关键所在。
- 向量微架构可以灵活地设计数据并行硬件,而不会影响到程序员,程序员可以不用重写代码就享受到长向量带来的好处。
图8.1是RV32V扩展指令集的图形表示。
- 前面章节提到的每一个整数和浮点计算指令基本都有对应的向量版本:图8.1中的指令继承了来自RV32I、RV32M、RV32F、RV32D和RV32A的操作。
- 每个向量指令都有几种类型,具体取决于操作数是否都是向量(.vv后缀),或者源操作数包含一个向量和一个标量(.vs后缀)。
- 一个标量后缀意味着有一个操作数来自x或f寄存器,另一个来自向量寄存器(v)。对于向量-标量操作,rsl域指定了要访问的标量寄存器。
- 对诸如减法和除法之类的非对称运算,他们还会使用向量指令的第三种变体。其中第一个操作数是标量,第二个是向量(.sv后缀)。像Y = a - X这样的操作就会使用这种变体。
向量寄存器和动态类型
- RV32V添加了32个向量寄存器,他们的名称以v开头,但每个向量寄存器的元素个数不同。
- 该数量取决于操作的宽度和专用于向量寄存器的存储大小。如果处理器为向量寄存器分配了4096个字节,则这足以