背景
由于RISCV采用了弱内存模型RVWMO,对内存访问的约束较少,而在现代的处理器设计中,因为流水线,乱序执行,store buffer等的存在,对IO, memory的访问往往是乱序的。
而软件在特定的场景下,需要硬件对IO,memory的访问能够保序发生,此时就需要fence指令,保证fence前的访存行为先于fence后的访存行为发生。
spec中对fence的定义如下:
Informally, no other RISC-V hart or external device can observe any operation in the successor set following a FENCE before any operation in the predecessor set preceding the FENCE.
举个例子,软件需要:hart0写一个flag,然后再写一个数据,hart1读到flag之后,读hart0写的数据。软件程序如下
此时hart0就需要fence w,w来保证两条store保序写出,hart1就需要fence r,r来保证两条load保序执行。如果没有fence,这两个核的执行结果将不可预期。
FENCE指令
fence指令的格式如下:
除了基础的指令码外,fence中还有如下位域需要关注:
- PI,PO,…,SR,SW位域。
P代表predecessor(先前的),S代表successor(后续的),I代表IO的input,O代表IO的output,R代表对memory的read,W代表对memory的write。
比如PW=1和SW=1时,就代表对fence前的store和后的store进行保序。这些细分位域可以使得fence的执行效率更高,例如上面的案例中hart0就使用了FENCE W,W,没有对FENCE前后的load的执行效率产生影响。 - fm位域
fm位域用以选择内存模型:
如果是0000,就是普通的fence,采用上述所说保序要求。
如果是1000,就代表此时选择TSO的内存模型,此时predecessor=RW,successor=RW。在TSO中,除了load(new)+store(old)可以乱序执行之外,其他都需保序执行。
在RVWMO模型中,仅推荐以下组合,其他的组合可能会出现不可预期的结果:
• FENCE RW,RW
• FENCE.TSO
• FENCE RW,W
• FENCE R,RW
• FENCE R,R
• FENCE W,W
FENCE.I指令
刚才的讨论,是对内存的数据访问进行保序,如果想对取指也进行保序呢?比如core希望用一条store指令更改程序区,而后面的程序在程序区被修改后再进行取指呢?
此时就要用到fence.i指令,fence.i确保fence.i前的store指令能被fence.i后续指令的取指看到:
A FENCE.I instruction ensures that a subsequent instruction fetch on a RISC-V hart will see any previous data stores already visible to the same RISC-V hart.
对于一个简单的无ICACHE,DCACHE的系统,fence.i只需要刷流水重新取指即可。
但对于一个有ICACHE,DCACHE的系统来说,如果让store的内容及时更新到指令侧是个难题,比较简单粗暴的做法是:刷流水,并将ICACHE和DCACHE清空。但这样代价较大,复杂的系统可以通过L2的一致性缓存,在store改写程序区后,将ICACHE中的该条cacheline给snoop掉。在有些实现中,自定义了清除icache的指令,如C910中的icache.iva等指令,可以通过这种指令,以软件的方式,让ICACHE同步。