riscv-v-spec-1.0(⽮量指令)学习理解(1-518gment)
1.Introduction
引⽤计算机体系结构中的⼀句话:执⾏可向量化应⽤程序最⾼效⽅法就是向量处理器。向量化的⽬的主要是为了去除程序中的loop,以减少不必要的指令开销。并且向量化可以将加载和存储的过程做到流⽔化,⽐较好的掩藏存储器延时,下⾯举个例⼦说明向量化的好处:
for (i = 0 ; i <64 ; i++) {
Y[i] = a * X[i];
}
//将该程序转换成riscv汇编:
li a0,0 //⽤于loop中的条件
li a1,64 //⽤于loop中的条件⽐较值
li t0,COEF_A //COEF_A为常量a
li s0,BASE_ADDR1 //BASE_ADDR1为LW基地址
li s1,BASE_ADDR2 //BASE_ADDR2为SW基地址
loop:
lw a2,0(s0)
mul a3,a2,t0
sw a3,0(s1)
addi s0,s0,4
addi s1,s1,4
addi a0,a0,1
bne a0,a1,loop
//将该程序转换成riscv-v汇编:
/
/VLEN=512
li t0,COEF_A
li s0,BASE_ADDR1 //BASE_ADDR1为LW基地址
li s1,BASE_ADDR2 //BASE_ADDR2为SW基地址
vtvli t1,t2,e32,m4
vle32 v1,(s0)
vmul.vx v2,v1,t0
vsw32 v2,(s1)
由上⾯的例⼦可以看出:
1.纯标量汇编指令⽐向量化的riscv汇编指令,多出了(5 + 7*64 - 7)= 446条需要执⾏的指令。
2.对于纯标量汇编指令,每次执⾏mul前后,都需要执⾏lw,sw。这个加载和存储的过程是不能流⽔
起来的,⽽向量化的vl,vs指令,是可以做到流⽔化的,将整个取向量的时间分散在单个元素上,可以很好的掩藏存储器延迟。
2.Implementation-defined Constant Parameters
向量扩展spec定义了2个parameters(以下参数均需要满⾜:位宽为2的幂):
1.ELEN:单个向量元素的最⼤位宽,ELEN>=8。(我的理解是:该参数应该根据硬件中ALU能处理的最⼤位宽决定)。//2030 4bit
2.VLEN:通⽤向量寄存器的位宽。
riscv中其他通⽤架构寄存器位宽命名:
XLEN:INT类型通⽤寄存器位宽。
FLEN:FLOAT类型通⽤寄存器位宽。
基向量扩展spec要求VLEN>=128。这是权衡后的⼀个⽐较折中的值,尽管设置较⼤的VLEN可以减少短向量指令的条带挖掘代码,但是设置较⼤的VLEN意味着较⼤的硬件register的开销。并且riscv-v-sp
ec提供了LMUL寄存器,该寄存器的值可以对向量寄存器进⾏分组。⽐如通⽤向量寄存器⼀共32个,设置LMUL为8,就可以将全部的通⽤寄存器分成4组,每组包含了8个连续的向量寄存器,这种分组的⽅式增加了向量寄存器组中元素的个数,同样减少了条带挖掘(strip minin g)的代码。
这⾥先解释⼀下条带挖掘的含义:条带挖掘技术是为了解决实际应⽤程序的向量长度长于硬件单次处理⽀持的最⼤长度的问题,条带挖掘代码是指⽣成⼀段代码,使得每个向量运算处理元素的长度都是⼩于或等于最⼤向量长度的。下⾯以计算机体系结构中的⼀段C程序来进⼀步解释条带挖掘代码:
//假设⼀共要处理n个元素,vlmax为循环内每次最⼤处理的长度
st = 0 ;//元素起始索引
vl = (n % vlmax) ; //得到不规则部分的元素数量
for (j =0 ; j <= (n/vlmax) ; j++) {
for (i = st ; i <(st + vl) ; i++) {
Y[i] = a * X[i] + Y[i] ;
}
st = st +vl ; //计算下⼀次开始的元素索引
vl = vlmax ; //将每次处理的元素长度恢复成最⼤的向量长度
}
3.Vector Extension Programmer’s Model
向量扩展向基标量RISC-V ISA中增加了32个向量寄存器(v0-v31)以及7个⽆特权的CSRS(控制和状态寄存器),分别是vstart、vxsat、vxrm、vcsr、vl、vtyp e、vlenb。向量寄存器的位宽为固定的VLEN宽度。
riscv 架构规定了⼀些只在机器模式下⽀持的寄存器,机器模式是riscv中硬件线程执⾏时的最⾼权限模式。机器模式对内存,I/O和⼀些对于启动和配置系统来说必要的底层功能有着完全的使⽤权。通常⽤户写的程序都在⽤户模式执⾏,⽤户态具有最低级别的权限。当⽤户程序需要使⽤⼀些底层硬件设备或者执⾏出现了异常⼜或者外围设备对正在执⾏的处理器发起了中断请求,那么cpu就会由⽤户态切换⾄内核态,也就是切换到机器模式下,将cpu的控制权,转交给内核程序。
riscv把程序执⾏时出现的异常,或者外部中断统称为陷阱(陷阱其实很好理解,因为不管是异常还是中断,cpu都会由⽤户态陷⼊内核态,这个陷⼊内核的过程就可以理解成踩⼊了陷阱⾥,要经过⼀些
其他的操作,才能爬出陷阱,恢复正常⾏⾛)。当遇到陷阱时,⾸先要将当前的pc保存下来,⽅便之后进⾏恢复。然后清空异常指令之前的流⽔线。接下来根据对应的陷阱类型,切换到对应的程序⼊⼝,开始执⾏内核程序(内核程序也是由⼀条条指令组成的,同样需要在流⽔线上执⾏)。等内核程序执⾏完成后,在重新把cpu的控制权转交给⽤户程序,从之前保存的pc指针开始重新取指,执⾏。
在重新回到riscv规定了⼀些只⽀持机器模式相关的寄存器的话题。mstatus寄存器是机器模式下的状态寄存器,其中mstatus[10:9]是向量上下⽂状态域(⼀个进程存储在处理器各寄存器中的中间数据叫做进程的上下⽂,所以进程的切换实质上就是被中⽌运⾏进程与待运⾏进程上下⽂的切换)。
mstatus.vs域同mstatus.fs类似。当mstatus.vs域被写成0时,试图执⾏向量指令或者访问向量寄存器均会引发⾮法指令异常。当mstatus.vs域被设置为初始状态或者⼲净的状态,只要执⾏了相关的指令改变了vpu(vector processor unit)状态,该域就会被改写成dirty的状态。
misa寄存器⽤于指⽰当前处理器的架构特性,该寄存器⾼两位⽤于表⽰当前处理器所⽀持的架构位数;该寄存器低26位⽤于指⽰当前处理器所⽀持的不同模块化指令集。riscv架构⽂档规定misa寄存器可读可写,从⽽允许处理器可以动态的配置某些特性。misa.v表⽰vector扩展指令集域,即使misa.v域被清0,mstatus .vs域也存在。这样设计可以简化mstatus.vs的处理。
3.1.vstart
vstart为可读可写寄存器,该寄存器规定了⼀条向量指令中第⼀个被执⾏的元素的索引。
vstart寄存器通常在向量指令执⾏的过程中产⽣了陷阱被写⼊。该寄存器记录了进⼊陷阱时向量指令操作元素的索引,以便跳出陷阱之后能够继续执⾏剩下的元素。所有向量指令都根据vstart中指定的元素索引开始执⾏。执⾏的过程保证该索引之前的⽬的寄存器的元素不被⼲扰,在执⾏的末尾,将vstart寄存器的值复位到0。当向量指令产⽣⾮法指令异常时,vstart寄存器将不会被改写。所以vstart寄存器被改写,只能是程序执⾏时出现了可恢复的同步异常,或者外部产⽣中断的情况。
思考假如不能通过vstart存储异常时的元素索引,那么在执⾏向量指令过程中发⽣的可恢复异常,必须要等到这条向量指令执⾏完,才能进⼊异常处理程序。这就要求向量指令必须是原⼦的,增加了控制复杂度,并且对于⼀些长延时的指令,⽐如load,将会导致响应中断的时间特别的长。
若vstart索引值⼤于vl的值,说明vstart指向的元素索引已经超过了当前所有元素的范围,该指令不会执⾏,并且同时会把vstart寄存器复位到0。
vstart可写的bit位,根据VLMAX确定,在vl部分已经描述过。
3.2.vxsat
vxsat为可读可写寄存器,该寄存器不仅有独⽴的寄存器地址,并且在vcsr寄存器中也有对应的域。该
寄存器有效表⽰输出结果做了饱和截位以适应⽬的寄存器格式。⽐如当运算发⽣正溢出时,保留结果为能取到的最⼤正值;当运算发⽣负溢出时,保留结果为负数最⼩值。
3.3.vxrm[1:0]
vxrm[1:0]为可读可写寄存器,该寄存器不仅有独⽴的寄存器地址,并且在vcsr寄存器中也有对应的域。该寄存器控制定点舍⼊模式,⼀共四种模式,分别是rou nd-to-nearest-up(rnu)、round-to-nearest-even(rne)、round-down(rdn)、round-to-odd(rod)。(问题:可否解释⼀下定点数在内存中的存放格式)
vxrm[1:0]寄存器通过单条csrwi指令写⼊值。
假如源操作数是v,有低d bit数据要被截掉,那么做完rounding-mode之后的最终结果应该是(v>>d)+r,r就是根据不同的rounding mode得到的增量值。
rnu:向距离近的⽅向进⾏舍⼊,当距离与两边都相等时,向上舍⼊。
rne:向距离近的⽅向进⾏舍⼊,当距离与两边都相等时,向偶数⽅向舍⼊。
rdn:向下舍⼊,直接取移位后的值。
rod:舍⼊到奇数值⽅向。
其中,v[d-1]表⽰权重位。当v[d-1]=0,表⽰距离舍的⽅向更近;当v[d-1]=1且v[d-2:0]=0时,距离舍⼊两个⽅向距离均相等;当v[d-1]=1,且v[d-2:0] != 0时,表⽰距离⼊的⽅向更近。
3.4.vcsr
vcsr[2:0]寄存器为可读可写寄存器,该寄存器由vxrm[1:0],以及vxsat组成。
思考为什么vxsat和vxrm既有单独的寄存器地址,也在vcsr中占有对应的域:
riscv spec中fflags和frm寄存器也采⽤的这样的设计。这类状态寄存器,希望⽅便快速的读写,⽐如有时候希望⼀条指令能把这两个寄存器的值同时读出来。⼜或者写这两个寄存器,如果要同时写,就得先做移位拼接在写,那有的时候只想改变其中⼀个寄存器的值,也做移位在拼接然后写的操作就⽐较慢,因此单独写对应的地址就显得尤为⽅便快速。考虑到适应不同的需求,这类状态寄存器就设计为既有各⾃单独的寄存器存储空间,⼜集中在⼀个寄存集中划分各⾃的域。
3.5.vl
vl寄存器为只读寄存器,该寄存器存储着⼀个⽆符号整数,⽤来规定⼀条向量指令需要更新多少个元素。
该寄存器只能被vtvli、vtvl指令以及fault-only-first⽮量读取指令的变体进⾏更新。
⽬的寄存器中元素索引⼤于等于vl的元素,将不会被修改。如果vstart⼤于等于vl,那么⽬的寄存器的任何元素都不会被修改。
vl的位宽由最⼩元素组成的最⼤向量长度决定。最⼩的元素位宽⾄少是8bit,最⼤的分组设置为LMUL等于8,那么VLMAX=LMUL * (VLEN / SEW) = VLEN。也就是说vl的位宽,直接由VLEN的⼤⼩决定。
3.6.vtype
vtype为只读寄存器,位宽同通⽤整型架构寄存器位宽(XLEN)。该寄存器提供了默认值⽤于解析向量寄存器中的内容,并且只能通过vtvl{i}指令进⾏更新(这样做的⽬的是使得维护vtype寄存器的状态简单化)。
vtype寄存器⽤于解析向量寄存器⽂件中的内容、决定单个向量寄存器中元素的组成以及决定多个向量寄存器是如分组的。
vtype寄存器有五个域,分别是vill,vma,vta,vw[2:0],vlmul[2:0]。
3.6.1.vill
当先前的vtvl{i}指令试图写⼊⼀个不被⽀持的值到vtype寄存器,vtype的vill域置1,同时vtype的其他域均清0。后⾯其他依赖于vtype寄存器执⾏的指令,将会引发⾮法指令异常。
3.6.2.vma &vta
这两个域修改⽬标尾部元素和⽬标被屏蔽元素在向量指令执⾏过程中的⾏为。在向量指令执⾏过程中,尾部元素和被屏蔽元素,不会接收新的运算结果。vma:vector mask agnostic,⽤于控制⽬的寄存器被屏蔽元素的⾏为。0:undisturbed 1:agnostic
vta:vector tail agnostic,⽤于控制⽬的寄存器尾部元素的⾏为。0:undisturbed 1:agnostic
采⽤undisturbed策略,那么⽬的寄存器相应元素集合,将保持原来的值。
采⽤agnostic策略,那么⽬的寄存器相应元素集合,既可以保持原值,也可以全部写1(spec上说全部写1⽽不是全部写0,是为了避免软件开发者依赖于这部分值,虽然我现在也没明⽩为啥写1就可以避免了。。。)。
那么采⽤agnostic策略的好处是什么呢?举个例⼦:对于超标量的流⽔线,会采⽤寄存器重命名的⽅式,来避免WAW以及WAR这两类hazard。那程序的逻辑寄存器会映射到物理寄存器,映射后的对应关系会更新到重命名映射表中。那对于undisturbed策略,需要⽬的寄存器相应的元素保持原来的值。
那么在⽤新的物理寄存器重命名时,还需要根据重命名映射表,查到原有的映射关系,再把这部分元素的值先读出来,写到重命名后的对应元素位置。这种⽅式对于压根⼉不关⼼尾部元素集合或者被屏蔽元素集合的值的后续操作,就既降低了性能,⼜增加了不必要的功耗。对于普通的in-order流⽔线,可以采⽤这种undisturbed的策略。对于超标量的流⽔线,使⽤agnostic策略就显得更加明智。
3.6.3.vw[2:0]
vw[2:0]:vector lected element width,⽤于动态的设置元素位宽。
⼀个向量寄存器被分成(VLEN/SEW)个元素。SEW=8*(2^vw)。
eg:假设VLEN=128,那么当vw等于0时(SEW=8),表⽰⼀个向量寄存器含有16(128/8)个元素。
SEW的值应该跟随LMUL的变化⽽变化(如果现在你不理解这句话,先带着问题往后看)。
3.6.
4.vlmul[2:0](有符号数)
LMUL:for vector register grouping,LMUL=2^(vlmul[2:0])。
多个向量寄存器可以组在⼀起,这样单条向量指令可以操作多个向量寄存器(这⾥多个向量寄存器组成⼀组,成为单条向量指令的⼀组操作数)。之前讲到过条带挖掘,其实就是⼀组多维的嵌套loop,切割任务,将任务划分为规则的部分和不规则的部分。对长向量的处理,如果硬件⽀持的单次处理的向量长度⽐较短,就需要loop多次,但是循环本⾝就增加了指令个数,还增加了⼀些划分的运算,因此对处理器性能的提⾼不是很理想。这⾥就解释了通过LMUL配置,增加组内寄存器个数,也就是增加了⼀条向量指令可操作的元素个数,避免了条带挖掘代码的出现,或者减少了条带挖掘的次数。
if(LMUL>=1):表⽰组合在⼀起的向量寄存器个数(LMUL=1、2、4、8)。
if(LMUL 是⼀个⼩数):⽤于减少⼀个向量寄存器中被使⽤的位宽。(LMUL=1/2、1/4、1/8)。设置LMUL为⼩数通常是⽤于混合位宽的向量指令操作。举个例⼦:⽐如源操作数向量⽤到的LMUL设置为1,计算后的元素位宽拓宽了2倍,保持元素个数不变的情况下,LMUL也需要增⼤两倍,也就是需要两个向量寄存器才能够存储下所有的向量运算结果。那假设源操作数本⾝只需要占据⼀个向量寄存器的⼀半,但是LMUL不能为⼩数,也就是占据⼩于等于1个向量寄存器宽度的都使⽤LMUL=1,那么这种情况再遇到位宽扩展指令,还是以刚才的例⼦就需要两个向量寄存器。但是很有可能扩展了的运算结果刚好只需要占据⼀个向量寄存器。这样设计就造成了向量寄存器的浪费。
spec要求必须⽀持LMUL>=SEW(lmul1min)/SEW(lmul1max)的配置。其中SEW(lmul1min)表⽰在LMU
L为1的条件下必须⽀持的最⼩元素位宽;SEW(lmul1max )表⽰在LMUL为1的条件下必须⽀持的最⼤元素位宽。因为SEW(lmul1max)是可以设置为与VLEN相等的,假如LMUL<SEW(lmul1min)/SEW(lmul1max),那么将不能保证⼀个寄存器剩余部分的位宽⾄少包含⼀个完整的元素。
VLMAX = LMUL * (VLEN/SEW),表⽰单条向量指令能够操作的最⼤元素数量。
掩码寄存器只包含在⼀个单独的向量寄存器中,与LMUL设置值⽆关。
3.7.vlenb
该寄存器为只读寄存器,表⽰以字节为单位的向量寄存器长度,vlenb=VLEN/8。vlenb是⼀个设计时常量,增加这个寄存器是为了减少⼀些需要直接⽤到vlenb 的程序的额外计算指令的开销。
4.Mapping of Vector Elements to Vector Register State
这⾥spec上⾯举例描述的⾮常详细了,总结⼀下:
1.元素以⼩端模式进⾏存放,也就是元素的数据低位放在向量寄存器的低位(低字节放在低地址)。
2.当LMUL=1时,元素按照顺序,从寄存器最低有效位开始摆放,元素摆放填满整个向量寄存器。
3.当LMUL<1时,元素按照顺序,从寄存器最低有效位开始摆放,只填LMUL * (VLEN / SEW)个元素,剩余的⾼位未被填充的位置,按照tail部分处理。
4.当LMUL>1时,元素依然按照顺序,从寄存器最低有效位开始摆放,当索引最⼩的向量寄存器被填满,就按照顺序开始填下⼀个向量寄存器。
5.当LMUL>1时。允许SEW>VLEN,也就是⼀个元素可以占多个向量寄存器,元素填充⽅式依然和前述⼀样。
针对混合位宽操作解释⼀下:混合位宽操作⽐如扩位宽操作(源操作数元素位宽⼩于⽬的操作数元素位宽),或者缩位宽操作(源操作数元素位宽⼤于⽬的操作数元素位宽)。但是⽆论位宽怎么变,源操作数元素的个数和⽬的操作数元素的个数肯定是相等的。那么这就需要动态配置LMUL随着元素位宽(SEW)的变化⽽变化也就是说需要保持SEW/LMUL为常数。LMUL设置的值⼤,可以减少向量指令取指和分发的开销。
向量掩码只占据⼀个向量寄存器。它的存储与SEW和LMUL的配置⽆关。每个向量元素具有对应的1bit向量掩码。
5.Vector Instruction Formats
向量扩展指令在已有的三类指令LOAD-FP、STORE-FP、AMO编码的基础上,重新定义了⼀些域。并且新增了⼀类指令OP-V。
向量扩展指令可以有标量或者⽮量源操作数,也能产⽣标量或者⽮量的结果。
5.1.标量操作数
标量操作数可以是⽴即数、整数通⽤寄存器、浮点通⽤寄存器,或者从⽮量寄存器元素0获得。
标量运算结果可以写到整数通⽤寄存器、浮点通⽤寄存器,也可以写到⽮量寄存器元素0的位置。
任意⽮量寄存器都可以⽤于存储标量,与LMUL的设置⽆关。
5.2.⽮量操作数
⽮量操作数通过有效元素位宽(EEW)和有效分组模式(EMUL)来决定⼀个⽮量寄存器组中元素的⼤⼩和位置。
默认情况下,⼤多数指令的⼤多数操作数EEW=SEW,EMUL=LMUL。
有⼀些指令的源操作数和⽬的操作数有相同数量的元素,但是却不是同样的位宽。所以EEW可能与S
EW不同,EMUL也可能和LMUL不同。但是EEW/EMUL=S EW/LMUL。如果源操作向量寄存器与⽬的向量寄存器有重叠的部分,那么必须满⾜以下三个约束之⼀:
1.⽬的向量寄存器的EEW与源向量寄存器的EEW相等(这句话我的理解是⼀⼀对应,⽬的寄存器与源寄存器完全相同)。
2.⽬的向量寄存器EEW⽐源向量寄存器的EEW⼩,但是交叠部分只在源寄存器组最低索引部分。
eg:vnsrl.wi v0,v0,3 #SEW = (2*SEW) >> SEW,当前LMUL=1
该指令为narrow逻辑右移指令,源操作数EEW=2*SEW,⽬的操作数EEW=SEW。当前源寄存器组为v0,v1组成,⽬的寄存器为v0。当前⽬的寄存器和源寄存器的交叠部分就发⽣在源寄存器的低索引部分,这种交叠不会有问题,但是倘若把⽬的寄存器由v0换成了v1,那么当源寄存器v0在执⾏时,就覆盖了v1中的部分元素,也就是运算结果覆盖了还没有执⾏的元素的内容,这就会导致运算结果出错。
3.⽬的向量寄存器EEW⽐源向量寄存器的EEW⼤,⾸先源向量寄存器LMUL必须⼤于等于1,并且交叠部分只在⽬的寄存器组最⾼索引部分。
eg:vext.vf4 v0,v6 #Zero-extend SEW/4 source to SEW destination,当前LMUL=8
该指令为0扩展指令,该指令为了使数据匹配对应的格式位宽。源操作数EEW=SEW/4,⽬的操作数EEW=SEW。
src 6 7
dst 0 1 2 3 4 5 6 7
src中的v6寄存器将会扩展到⽬的寄存器的v0-3处。src中v7的第⼀个1/4的部分扩展到v4,第⼆个1/4的部分扩展到v5,第三个1/4的部分扩展到v6,此时v6⾥⾯的源操作数早已被⽤掉,因此该覆盖不会有问题,v7中最后1/4就覆盖整个v7。假如该指令变成vext.vf4 v4,v6,那么会出现源寄存器中的元素还没被执⾏,就被覆盖的错误情况。
以上三条约束,均是为了保证还没有被操作的源不要被运算结果所覆盖。⽰意图如下:
5.3.⽮量掩码
很多向量指令都⽀持掩码执⾏。被遮罩的元素不会产⽣异常。⽬的向量寄存器被遮罩的元素根据vtype.vma进⾏操作。遮罩的需求来源于循环中的if语句,本来单纯的循环,并且循环内元素没有dependency,是可以靠编译器进⾏向量化的。但是有⼀类循环内插⼊了条件语句,要当条件满⾜时,才能执⾏后续的操做,这种情况,如果指令集架构上没⽀持,那么编译器就不能成功将代码向量化。
eg:
for (i = 0 ; i <64 ; i++) {
if(X[i] != 0) {
Y[i] = a * X[i];
}
}
将上诉代码转换为带有遮罩的向量指令:
vmul.vx v2,v1,t0,v0.t
在基向量扩展中,⽤来控制掩码向量指令执⾏的掩码值通过向量寄存器v0提供。⼀个v0寄存器就⾜够,是因为⾸先掩码寄存器中的元素都是1bit,其次是⼀条向量指令中能操作的最⼤元素个数由最⼤分组和最⼩元素位宽决定,前⾯已经分析过最⼤能取到的元素个数实际上就是VLEN⼤⼩。因此刚好⼀个向量寄存器就可以表⽰所有向量指令的掩码。riscv-v-spec中定义了⼀些掩码操做的指令,这些指令,源寄存器和⽬的寄存器中的元素都是掩码,并且这些指令不会受LMUL和S EW的影响。
这⾥掩码在指令⾥,也可以看作操作数。只是受限于指令编码的位宽限制,没有多余的域再去规定掩码操作数的寄存器索引了,所以当前的spec上将v0作为掩码寄存器,任何掩码指令得到的掩码结果,在被需要掩码的指令使⽤之前,需要将掩码值写到v0寄存器中。
5.3.1.掩码编码