# Tail-call Optimization
如果一個函式 (function) 的最後行為是呼叫函式,我們稱這個最後行為為 tail call。
舉個例子,考慮到以下程式碼,函式 `A` 的最後行為是執行函式 `B`
```c
int A(int data)
{
return B(data);
}
```
要注意的是, tail call 的最後行為只允許呼叫函式,所以以下的程式碼就不是 tail call,因為他的最後行為是進行 2 * 1 + B(data) 的運算,而非單純呼叫函式。
```c
int A(int data)
{
return 2 * 1 + B(data);
}
```
**Tail-call optimization**,每當函式呼叫時,我們需要建立 stack frame 來紀錄函式的參數以及 return address,**tail-call optimization** 最主要的目地是避免建立過多的 stack frame 而造成 stack overflow,所以呼叫函式時,只記錄參數,接著跳到函式執行處開始執行。
如果一個函式的最後行為是呼叫自己本身,那我們稱這個最後行為為 tail recursion。**Tail-call optimization** 最常見的使用情境就是優化 tail-recursion 的函式。我們只要將 tail call 所呼叫函式的參數記錄下來接著跳到 caller 函式所在位置,即可執行。以下用兩個例子來說明 **Tail-call optimization**。
## Example 1
第一個範例是階層計算,以下階層計算程式碼符合 tail recursion,因此可以以 **Tail-call optimization** 來優化
```c
int factorial(int n, int sum)
{
if (n == 0)
return sum;
sum *= n;
return factorial(n - 1, sum);
}
```
分別比較使用以及不是用 **tail-call optimization** 產生之指令,頻繁的使用 load 和 store 指令,操作 stack frame 中的參數或 return address,顯然優化後的程式碼少了這些指令來操作 stack frame,效率較佳。
* Without **tail-call optimization**
```
factorial:
addi sp,sp,-32
sw ra,28(sp)
sw s0,24(sp)
addi s0,sp,32
sw a0,-20(s0)
sw a1,-24(s0)
lw a5,-20(s0)
bne a5,zero,.L2
lw a5,-24(s0)
j .L3
.L2:
lw a4,-24(s0)
lw a5,-20(s0)
mul a5,a4,a5
sw a5,-24(s0)
lw a5,-20(s0)
addi a5,a5,-1
lw a1,-24(s0)
mv a0,a5
call factorial
mv a5,a0
.L3:
mv a0,a5
lw ra,28(sp)
lw s0,24(sp)
addi sp,sp,32
jr ra
```
* With **tail-call optimization**
```
factorial:
mv a5,a0
mv a0,a1
.L3:
beq a5,zero,.L4
mul a0,a0,a5
addi a5,a5,-1
j .L3
.L4:
ret
```
## Example 2
第二個範例是計算 fibonacci number,以下程式碼是典型利用遞迴來計算 fibonacci number,但以下程式碼並不符合 **tail recursion**,因為最後行為是兩個函式的回傳值相加
```c
int fib(int n)
{
if (n == 1 || n == 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
```
以下是已經優化過的指令,可以發現不符合 tail-call 的函式(無法利用 **tail-call optimization**),即使優化過,仍然不可避免的必須對 stack frame 進行 load 和 store 操作。
```
fib:
addi sp,sp,-16
sw s0,8(sp)
sw s1,4(sp)
sw s2,0(sp)
sw ra,12(sp)
addi s0,a0,-1
li s1,0
li s2,1
.L3:
bleu s0,s2,.L5
mv a0,s0
call fib
add s1,s1,a0
addi s0,s0,-2
j .L3
.L5:
lw ra,12(sp)
lw s0,8(sp)
lw s2,0(sp)
addi a0,s1,1
lw s1,4(sp)
addi sp,sp,16
jr ra
```
為了可以成功利用 **tail-call optimization** 優化,我們進一步將這個計算函式改為符合 **tail recursion** 的版本再進行優化,程式碼如下:
```c
// left = fib(n - 2), right = fib(n - 1)
int fib(int n, int left, int right)
{
if (n == 0) {
return left;
}
return fib(n-1, right, left + right);
}
```
以下是已經優化過的指令,可以發現,相較典型利用遞迴來計算 fibonacci number 的寫法,**tail recursion** 版本的 fibonacci number 計算函式帶入的參數較多,可讀性也較差,然而利用 **tail-call optimization** 優化後的指令,明顯簡潔許多,可看出 **tail-call optimization** 顯著的優化。
```
fib:
mv a5,a0
mv a0,a1
.L3:
beq a5,zero,.L4
add a4,a0,a2
addi a5,a5,-1
mv a0,a2
mv a2,a4
j .L3
.L4:
ret
```