# 淺談 `RISCV` 裡的 save-restore call
> 若您發現用詞不精確或語句不通順,請直接修改。
## 前言
`save-restore` 是 `gcc` 與 `llvm` 為了改善 `RISCV` 的 `code size` 在 `prologue` 以及 `epilogue` 上的方案。筆者希望可以從這個技術,探討相關的 `RISCV` 指令集,函式呼叫以及編譯器能做的相關優化。
## 環境
筆者使用的 `cross-compiler` 是 [riscv-gnu-toolchain](https://github.com/riscv/riscv-gnu-toolchain.git) 編出來的 GCC10。
## `callee save`
在函式呼叫這個事件中,caller指為呼叫函式所在的函式,而callee指的是被呼叫的函式。以下方程式碼中,`bar`呼叫`foo`的行為而言,`bar` 為 `caller` 而 `foo` 為 `callee`。
``` cpp
void bar(int x) {
foo();
return x + 4;
}
```
函式呼叫時可能會巢狀結構影響回傳地址,或者回傳值影響暫存器內容。所以每個指令集都會設計一組 `calling convention` 來規範相關行為,如:規範 `callee` 某些暫存器在呼叫結束的時候,要回復為函式呼叫前的狀態,而這些暫存器被稱為 `callee save` 。
以上方的程式我們可以用GCC,搭配參數 `Os` 編譯出以下的組合語言:
```
bar:
addi sp,sp,-16
sw s0,8(sp)
sw ra,12(sp)
mv s0,a0
call foo
lw ra,12(sp)
addi a0,s0,4
lw s0,8(sp)
addi sp,sp,16
jr ra
```
跟據 [riscv calling convention](https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf) 規範 `s0` 為 `callee save`,所以我們不用擔心呼叫 `foo` 會改變 `s0` 的值。但同時因為 `$s0` 為 `callee save` 的特性, `bar` 必須使用前存到 `stack` 中,並在函式回傳前回覆成初始的狀態。而 `$ra` 不是 `callee save` 所以在 `foo` 的呼叫前我們需要把它存起來。
## prolouge & epilouge
`prologue` 以及 `epilouge` 是指在函式開始以及結束需要做的必要操作。以 `RISCV` 來講我們需要儲存會被改變的 `callee save` 以及針對 `callee` 會改變的 `$ra`,還有相對應的 `$sp` 的操作。
## save-restore call
在以上面出現的組合語言為例,我們的 `prologue` 以及 `epilouge` 各用三道指令。其實在一個大型的程式中,同樣或是類似的 `prologue` 和 `epilouge` 會是有很多組。所以就有人提出了 save/resotre call 的概念,把 `prologue` 和 `epilouge` 當成函式,則同樣的程式碼只要出現一次。
## 實驗
我們先加工上面的C Code,利用 `noipa` 這個 `attribute` 來避免編譯器做最佳化。 `noipa` 是杜絕所有 `ipa` (interprocedural) 的函式間最佳化,如 `inline` 或 `interprocedure constant propgation` [0]。在此實驗使用 `noipa` 而非 `noinline` 的原因在於,`noipa` 可以讓 `gcc` 視此函式有 `side-effect` 所以此函式不能被`dead code elimination`清除掉。
```
__attribute__((noipa))
void foo(void) {}
__attribute__((noipa))
int bar(int x) {
foo();
return x + 4;
}
int main(void) {
bar(1);
}
```
在利用 `gcc` 以 `-Os -msave-restore` 編譯,並用 `objdump`,指令如下:
```
riscv32-unknown-elf-gcc -Os test.c -msave-restore
riscv32-unknown-elf-objdump -d a.out
```
```
...
00010114 <bar>:
10114: 302002ef jal t0,10416 <__riscv_save_0>
10118: 842a mv s0,a0
1011a: 3fe5 jal 10112 <foo>
1011c: 00440513 addi a0,s0,4
10120: ae29 j 1043a <__riscv_restore_0>
...
00010416 <__riscv_save_0>:
10416: 1141 addi sp,sp,-16
10418: c04a sw s2,0(sp)
1041a: c226 sw s1,4(sp)
1041c: c422 sw s0,8(sp)
1041e: c606 sw ra,12(sp)
10420: 8282 jr t0
...
0001043a <__riscv_restore_0>:
1043a: 4902 lw s2,0(sp)
1043c: 4492 lw s1,4(sp)
1043e: 4422 lw s0,8(sp)
10440: 40b2 lw ra,12(sp)
10442: 0141 addi sp,sp,16
10444: 8082 ret
...
```
我們可以清楚的看到,`__riscv_save_0` 與 `__riscv_restore_0` 成功了備份了`$s0`與`$ra`。
## 影響
根據這份[報告](https://riscv.org//wp-content/uploads/2019/12/12.10-12.50a-Code-Size-of-RISC-V-versus-ARM-using-the-Embench%E2%84%A2-0.5-Benchmark-Suite-What-is-the-Cost-of-ISA-Simplicity.pdf)指出,save-restore call可減少7%的code size,但同時會有10%的效能損失。
## 缺點
- 呼叫 save/restore call 有效能上的損失。
- 當程式過小的時候,save-restore call影響有限甚至在code size上有害。
- 當程式過大的時候,無法只用一道 `jalr` 跳躍至對應的 `save-restore call` 便需要在搭配 `auipc` 指令。
## 參考文獻
[0] https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html