RISCV
1. 香山工具记录
1.1 基础工具介绍
- 香山处理器使用 DiffTest 协同仿真框架进行仿真验证:对于根据riscv手册的两种实现, 给定相同的正确程序, 它 们的状态变化应当一致,其中一种是我们的CPU,另一种模拟器就可以了
lightSSS
,它可以在仿真进程出错时自动保存出错点附近的波形和debug信息- Nexus-am,生成workload
- NEMU是参考模型,辅助香山比较和验证微架构,而XiangShan
ChiselDB
来将结构化数据存储在数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
#1)generate workload
cd xs-env/nexus-am/apps/xxx
make ARCH=riscv64-xs -j48
#2)using NEMU running workload
cd xs-env/NEMU
make clean && make defconfig riscv64-xs_defconfig && make -j48
$NEMU_HOME/build/riscv64-nemu-interpreter -b xxx.bin
#3)在香山核仿真程序上仿真运行 workload
##这个命令会将 NEMU 模拟器编译成动态链接库,将会在 build 目录下生成文件 interpreter-so ,从而接入到香山仿真差分测试中
make clean && make defconfig riscv64-xs-ref_defconfig && make -j48
#running xiangshan && NEMU to difftest
cd xs-env/XiangShan
./build/emu -i xxx.bin
性能评估验证—还没看
1.2 workload生成(nexus-am/apps代码阅读)
1.3 gdb调试
1
2
3
4
5
6
7
qemu-riscv64 -g 1234 ./main.elf
# 另开一个终端
riscv64-unknown-linux-gnu-gdb main.elf
(gdb) target remote :1234
(gdb) continue
# 程序崩溃后:
(gdb) bt
1.4 qemu运行
accelr-net/tvm-riscv-demo: Demonstrator on running TVM on RISC-V with RN18 and KWS examples
1
2
3
4
5
6
7
8
#1.启动
qemu-system-riscv64 -cpu rv64,v=true -machine virt -nographic -m 8192 -smp 8 -bios opensbi/build/platform/generic/firmware/fw_jump.bin -kernel u-boot/u-boot -device virtio-net-device,netdev=eth0 -netdev user,id=eth0,hostfwd=tcp::2222-:22 -device virtio-rng-pci -drive file=./ubuntu.img,format=raw,if=virtio
#2.ssh
ssh ubuntu@localhost -p 2222
#3.关机
sudo shutdown -h now
#当你想要关闭 QEMU 时,可以在 QEMU 的窗口(非SSH链接)中按 `Ctrl+A`,然后 `X`
2. RVV标准向量扩展介绍
RVV 拥有独立的32个vector寄存器和7个CSRs寄存器,向量指令:设置向量CSR+访存+计算
RVV的tail agnostic,mask agnostic和vector masking
不可知(agnostic)意味着这部分元素不应成为源操作数
RVV规范也充分考虑了不同实现中如何处理非活跃元素(inactive,tail)更方便高效。
对非重命名向量寄存器的处理器来说,给非active元素应用undisturbed策略最经济。因为这类处理器往往VLEN超长,一次仅处理数个元素,跳过非活跃元素可以加速运算,而写1就无法加速运算。因此非重命名架构直接用undisturbed处理更简单。
而重命名架构VLEN较短,通过检查mask和vl,跳过个别元素的收益并不大。在重命名架构中,每个uop读写的寄存器都是经过重命名映射的物理寄存器,对于同一个vd,读和写分别是不同的物理寄存器。于是undisturbed模式就额外多了一个源操作数,发射队列需要多存一份旧vd对应的物理寄存器,也会给物理寄存器堆增加一个旧vd的读取需求,这样会增加物理实现开销。
所以在重命名架构中,解除指令对旧vd的数据依赖对设计是有优化,按写1实现agnostic更有利于性能。
RISC-V “V” Vector Extension Version 1.0 -知乎
2.1 利用rvv指令的方式
编译器支持的话最好,不支持的话,目前intrinsics 函数编程是最容易的
2.x 矢量加载指令的地址模式
矢量元素个数为VLEN/EEW*EMUL
- unit-stride:矢量中每个元素在内存的地址空间中是连续的
- stride:矢量中元素在内存的地址空间是不连续的,并且每个相邻元素的间距(stride)为固定的
- indexed:矢量中元素在内存的地址空间是不连续的,同时每个相邻元素的间距是不固定的
- indexed地址模式又可以细分为保序(ordered)和非保序(unordered)两种,而unit-stride以及strided均为非保序的。保序要求每个元素的访存行为按照其索引大小的顺序发生
1
2
3
4
5
6
7
8
9
10
#1)unit-stride: vd 为目的矢量寄存器, rs1 为内存基地址, vm为掩码(v0.t or <missing>)
vle8.v vd, (rs1), vm # EEW = 8b
#2)strided: ,vd 为目的矢量寄存器, rs1 为内存基地址, rs2 为stride
vlse8.v vd, (rs1), rs2, vm # EEW = 8b
#3)indexed: vd 为目的矢量寄存器, rs1 为内存机地址, vs2 为offset
vluxei8.v vd, (rs1), vs2, vm # unordered, EEW = 8b
vloxei8.v vd, (rs1), vs2, vm # ordered, EEW = 8b
- Minimum Vector Length Standard Extensions(Minimum VLEN 32~1024):用以拓展向量扩展的最小向量长度,有
Zvl32b
~Zvl1024b
六种 - zve扩展(Vector Extensions for Embedded Processors)
2.3 汇编指令
以下都是根据xiangshan的nexus-am/apps中的代码经过GPT解读后生成的内容解释:
- rvv中的向量load/store如下
1. vle8.v v0, (rs1)
语义:把内存中以字节(8 bit)为单位的连续元素,逐一装入一个向量寄存器(
v0
)。执行过程:
根据当前的
vl
(Vector Length)值,一共加载vl
个元素。从地址
rs1 + 0*1
、rs1 + 1*1
、…、rs1 + (vl−1)*1
依次取出 8 bit,每个元素零扩展(或符号扩展,取决于指令变体)到当前 SEW 宽度后写入v0[i]
。
2. vlm.v v0, (rs1)
语义:按当前 LMUL 配置,将内存中连续数据“一组一组”装入从
v0
开始的一个寄存器组。执行过程:
计算寄存器组宽度:若 LMUL = k,则寄存器组包含
{v0,…,v(k−1)}
。对每个向量寄存器
vj
,按vl
个元素从内存中连续读取 SEW 位宽的数据,依次填满vj[0…vl−1]
。
3. vlse8.v v0, (rs1), stride
语义:以字节为单位、指定步长,从内存加载散步长元素到
v0
。执行过程:
根据
vl
,一共加载vl
个元素。对第
i
个元素,从地址rs1 + i*stride
处取 8 bit,扩展到 SEW 后写入v0[i]
。
4. vloxei8.v v0, (rs1), v1
语义:按索引向量
v1
中的每个字节偏移,从内存加载带散列表的元素到v0
。执行过程:
v1[i]
给出第i
个元素的字节偏移量;从地址
rs1 + v1[i]
取 8 bit,扩展后写入v0[i]
。
5. vle8ff.v v0, (rs1)
语义:Fault-Only-First 变体的
vle8.v
,只在第一次访问错时(i=0)发生异常,其后正常加载。执行过程:
访问第 0 个元素:从
rs1+0
取 8 bit;若发生页错/越界,令vl
更新为已成功加载的元素数(0 或更小),停止后续访问;若未错或第一次成功,继续像
vle8.v
一样加载剩余元素。
6. vlseg2e8.v v0, (rs1)
语义:按结构化段(unit-stride segment)加载,每次从内存连续取出 2 个 8 bit 字节,分别写入
v0
和v1
。执行过程:
对第
i
段,从rs1 + 2*i + 0
、rs1 + 2*i + 1
各取 8 bit;写入寄存器组:
v0[i]
← 第 0 个字节,v1[i]
← 第 1 个字节,共加载vl
段。
7. vlseg2e8ff.v v0, (rs1)
语义:Fault-Only-First 变体的
vlseg2e8.v
,只在第一段访问错时生效。执行过程:
对段 0 执行两次 8 bit 访问,若任一次失败,则根据已加载段数更新
vl
,停止后续。否则像
vlseg2e8.v
继续加载余下段。
8. vlsseg2e8.v v0, (rs1), stride
语义:带步长的结构化段加载,每段内元素间隔
stride
字节。执行过程:
对段
i
:第 0 元素地址rs1 + i*stride + 0
,第 1 元素地址rs1 + i*stride + 1
;分别取 8 bit 写入
v0[i]
和v1[i]
,共vl
段。
9. vloxseg2ei8.v v4, (rs1), v1
语义:按索引向量
v1
加载结构化段,每段两元素,分别写入v4
和v5
。执行过程:
对第
i
段,偏移量v1[2*i + 0]
给出第 0 元素地址,v1[2*i + 1]
给出第 1 元素地址;各自加载 8 bit,并写入
v4[i]
、v5[i]
。
10. vl1re8.v v0, (rs1)
语义:“whole load” 变体,按块大小一次性从内存加载一整组元素到
v0
。执行过程:
将
vl
个 8 bit 元素聚成一个连续块;从
rs1
开始一次性取出vl
字节,分别写入v0[0…vl−1]
。
11. vl4re8.v v0, (rs1)
语义:同
vl1re8.v
,但一次按 4×vl 字节对齐加载(跨 4 个连续向量寄存器)。执行过程:
计算四倍元素块大小(
4*vl
);从对齐地址
rs1
一次性读取该块,填充v0…v3
。
12. vse8.v v0, (rs1)
语义:与
vle8.v
对应的按元素存储,将v0
中每个 8 bit 元素依次写到内存。执行过程:
对第
i
个元素,将v0[i]
的低 8 bit 写到地址rs1 + i*1
;共写
vl
次。
13. vsm.v v0, (rs1)
语义:按掩码存储,多寄存器组存储,与
vlm.v
对应。执行过程:
计算寄存器组
{v0…v(k−1)}
;按掩码位(默认为全真)对每个寄存器依次写
vl
个元素到内存。
14. vsse8.v v0, (rs1), stride
语义:带步长的按元素存储,将
v0[i]
写到rs1 + i*stride
。执行过程:
对第
i
个元素,写 8 bit 到地址rs1 + i*stride
;重复
vl
次。
15. vsoxei8.v v0, (rs1), v1
语义:按索引向量
v1
存储,将v0[i]
写到rs1 + v1[i]
。执行过程:
对第
i
个元素,计算目标地址rs1 + v1[i]
;写出该元素的低 8 bit。
16. vsseg2e8.v v0, (rs1)
语义:单段结构化存储,将寄存器组
{v0,v1}
的vl
段分别写回内存连续两字节。执行过程:
对第
i
段,写v0[i]
→rs1+2*i
,v1[i]
→rs1+2*i+1
;共写
vl
段。
17. vssseg2e8.v v0, (rs1), stride
语义:带步长的分段存储,将
{v0,v1}
写到rs1 + i*stride + {0,1}
。执行过程:
对段
i
,写v0[i]
→rs1+i*stride
,v1[i]
→rs1+i*stride+1
;共
vl
段。
18. vsoxseg2ei8.v v4, (rs1), v1
语义:按索引向量
v1
的分段存储,将两寄存器组的元素写回。执行过程:
对段
i
,偏移量由v1[2*i+0]
、v1[2*i+1]
给出;写
v4[i]
、v5[i]
至对应地址。
19. vs1r.v v0, (rs1)
语义:Whole-store 变体,将
vl
个元素一次性写到内存连续区域。执行过程:
从
v0[0…vl−1]
收集vl
字节;一次性写到
rs1
开始的连续地址。
20. vs4r.v v0, (rs1)
语义:与
vs1r.v
对应,但一次对齐 4×vl 字节跨 4 寄存器写出。执行过程:
聚合
v0…v3
的vl
元素,形成 4×vl 字节块;一次性写到
rs1
。
- rva原子与 LR/SC 指令如下:
1. lr.d t0, (rs1)
语义:Load-Reserved 双字(64-bit)——在地址
rs1
上设置保留(reservation),并把该地址处的 64 位值载入寄存器t0
。执行过程:
在内部为
rs1
对应的内存行/缓存行打保留标记;从地址
rs1
读取 64 位数据,写入t0
;保留状态用于后续的
sc.d
判断。
2. sc.d t0, t1, (rs1)
语义:Store-Conditional 双字(64-bit)——仅当先前对同一地址有未被中断的保留,才把
t1
写回内存,并在t0
中写入 0 表示成功;否则不写内存,在t0
中写入非零表示失败。执行过程:
检查针对地址
rs1
的保留标记是否依旧有效(无其他写入打断);若有效,执行写入:
M[rs1] ← t1
,并t0 ← 0
;若无效,不写内存,仅
t0 ← 1
(或其它非零失败码);清除保留状态。
3. amoswap.d t1, t0, (rs1)
语义:原子交换双字——将
rs1
处的旧值写入t1
,再把t0
写回rs1
,整个操作不可被中断。执行过程:
读出内存
tmp ← M[rs1]
;写回寄存器:
t1 ← tmp
;原子写内存:
M[rs1] ← t0
。
4. amoadd.d t1, t0, (rs1)
语义:原子加双字——将
t0
与内存rs1
处的旧值相加,结果写回内存;旧值写入t1
。执行过程:
读出:
old ← M[rs1]
;写寄存器:
t1 ← old
;计算与写回:
M[rs1] ← old + t0
。
5. amoand.d t1, t0, (rs1)
语义:原子与双字——把
t0
与内存旧值按位与,结果写回内存,旧值写入t1
。执行过程:
old ← M[rs1]
;t1 ← old
;M[rs1] ← old & t0
;
6. amomax.d t1, t0, (rs1)
语义:原子求带符号最大双字——比较
old
(内存原值)和t0
,将二者中的较大者写回内存,旧值写入t1
。执行过程:
old ← M[rs1]
;t1 ← old
;比较(带符号):若
old ≥ t0
则M[rs1] ← old
,否则M[rs1] ← t0
。
7. amominu.d t1, t0, (rs1)
语义:原子求无符号最小双字——以无符号方式比较
old
与t0
,将最小者写回内存,旧值写入t1
。执行过程:
old ← M[rs1]
;t1 ← old
;比较(无符号):若
old ≤ t0
则M[rs1] ← old
,否则M[rs1] ← t0
。