LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。
関数コールの最適化の一つとして、Tail call optimization(末尾再帰呼び出し)を実装してみる。
LLVMにはTail CallというIRが用意されており、これで末尾再帰呼び出しを表現して、最適化に使用することができるようだ。
サンプルプログラム :
int factorial(int x) { if (x > 0) return x*factorial(x-1); else return 1; } int test_tailcall(int a) { return factorial(a); }
これをコンパイルする。まずは-O0
オプションから。
./bin/clang -O0 -c -target mips-unknown-linux-gnu ../lbdex/input/ch9_2_tailcall.cpp -emit-llvm ./bin/llvm-dis ch9_2_tailcall.bc -o -
define dso_local i32 @_Z9factoriali(i32 signext %x) #0 { entry: %retval = alloca i32, align 4 %x.addr = alloca i32, align 4 store i32 %x, i32* %x.addr, align 4 %0 = load i32, i32* %x.addr, align 4 %cmp = icmp sgt i32 %0, 0 br i1 %cmp, label %if.then, label %if.else if.then: ; preds = %entry %1 = load i32, i32* %x.addr, align 4 %2 = load i32, i32* %x.addr, align 4 %sub = sub nsw i32 %2, 1 %call = call i32 @_Z9factoriali(i32 signext %sub) %mul = mul nsw i32 %1, %call store i32 %mul, i32* %retval, align 4 br label %return if.else: ; preds = %entry store i32 1, i32* %retval, align 4 br label %return return: ; preds = %if.else, %if.then %3 = load i32, i32* %retval, align 4 ret i32 %3 } ; Function Attrs: noinline optnone define dso_local i32 @_Z13test_tailcalli(i32 signext %a) #0 { entry: %a.addr = alloca i32, align 4 store i32 %a, i32* %a.addr, align 4 %0 = load i32, i32* %a.addr, align 4 %call = call i32 @_Z9factoriali(i32 signext %0) ret i32 %call }
普通の関数呼び出しが実行されている。これを、最適化していくとどうなるのか。
./bin/clang -O1 -c -target mips-unknown-linux-gnu ../lbdex/input/ch9_2_tailcall.cpp -emit-llvm ./bin/llvm-dis ch9_2_tailcall.bc -o -
; Function Attrs: nounwind readnone define dso_local i32 @_Z9factoriali(i32 signext %x) local_unnamed_addr #0 { entry: %cmp3 = icmp sgt i32 %x, 0 br i1 %cmp3, label %if.then, label %return if.then: ; preds = %entry, %if.then %x.tr5 = phi i32 [ %sub, %if.then ], [ %x, %entry ] %accumulator.tr4 = phi i32 [ %mul, %if.then ], [ 1, %entry ] %sub = add nsw i32 %x.tr5, -1 %mul = mul nsw i32 %x.tr5, %accumulator.tr4 %cmp = icmp sgt i32 %x.tr5, 1 br i1 %cmp, label %if.then, label %return return: ; preds = %if.then, %entry %accumulator.tr.lcssa = phi i32 [ 1, %entry ], [ %mul, %if.then ] ret i32 %accumulator.tr.lcssa } ; Function Attrs: nounwind readnone define dso_local i32 @_Z13test_tailcalli(i32 signext %a) local_unnamed_addr #0 { entry: %call = tail call i32 @_Z9factoriali(i32 signext %a) ret i32 %call }
おや、test_tailcall
が末尾再帰として認識された。
これを-enable-myriscvx-tail-calls
を付加してアセンブリを出力するとどうなるのか。
- O0でコンパイルしたもの
_Z13test_tailcalli: # %bb.0: # %entry lui x10, %hi(_gp_disp) addi x10, x10, %lo(_gp_disp) addi x2, x2, -16 .cfi_def_cfa_offset 16 sw x10, 12(x2) lw x10, 12(x2) lw x3, %call16(_Z9factoriali)(x3) jalr x3 addi x2, x2, 16 jalr x1
- O1でコンパイルしたもの
_Z13test_tailcalli: # %bb.0: # %entry lui x10, %hi(_gp_disp) addi x10, x10, %lo(_gp_disp) lw x3, %call16(_Z9factoriali)(x3) jalr x3
確かに、スタックの操作がなくなってそのままfactorial
を呼び出しているようだ。