---
# System prepended metadata

title: 淺談 `RISCV` 裡的 save-restore call

---

# 淺談 `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