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