LLVM tailcallopt について

Ghuloum さんの "An Incremental Approach to Compiler Construction", 3.10 Proper Tail Calls の実装のための準備。以下、嘘が含まれている可能性大ですのでご注意。

まず LLVMhttp://www.llvm.org/docs/CodeGenerator.html#tailcallopt にあるように、Tail call optimization をサポートしている。これが Scheme の仕様が要求している「properly tail-recursive」と完全に同じ意味かどうかは私の理解が怪しいですが、LLVM を正しく使えば properly tail-recursive が簡単に実装できる、と期待。そのためにまずは手を動かしてみる。

前準備

  • LLVM 2.4, PowerPC G4, Mac OS X 10.4.11
  • PowerPC なので LLVM Tail call optimization サポート対象
  • オプション -tailcallopt は、llc と lli に存在する。opt には -tailcallelim、-tailduplicate という紛らわしいオプションあり。llvm-as には存在しない。

テスト

まず、自分自身を沢山呼ぶ関数 fn を作成し、これを基本のテスト対象とする。Scheme で書くと以下のような、無意味な関数。

(letrec ((fn (lambda (n)
                 (if (= n 1)
                      1
                      (fn (- n 1)))))
   (fn 10000000000)

以下が LLVM 版。本質的なのは call i32 @fn の部分のみで、それ以外は結果を出力(printf)しているだけ。

;; simple ver.
@fmt_d = internal constant [3 x i8] c"%d\00"
declare i32 @printf(i8* noalias, ...)
declare i32 @putchar(i32)

define i32 @main ()  {
entry:
    %ret = call i32 @fn(i32 100000000)
    %fmt = getelementptr [3 x i8 ]* @fmt_d, i64 0, i64 0
    call i32 (i8*, ...)* @printf(i8* %fmt, i32 %ret)
    call i32 @putchar(i32 10)
    ret i32 0
}

define i32 @fn (i32 %x)  {
entry:
    %cmp = icmp eq i32 %x, 1
    br i1 %cmp, label %if_true, label %if_else
    if_else:
    %x2 = sub i32 %x, 1
    %ret = call i32 @fn(i32 %x2)
    ret i32 %ret
    if_true:
    ret i32 1
}

上記を test.1.ll とする。test.1.ll を以下のように実行コマンドにする。

/usr/local/bin/llvm-as -f -o test.1.bc test.1.ll
/usr/local/bin/llc -f -o test.1.s test.1.bc
/usr/local/bin/llvm-gcc -o test.1.exe test.1.s

こうしてできた test.1.exe を実行すると、私の環境では予定どおり segmentation fault でエラーとなった。gdb から実行すると EXC_BAD_ACCESS と出力。llc に -tailcallopt を与えても同じ。これは http://www.llvm.org/docs/CodeGenerator.html#tailcallopt の最適化がされる1つ目の条件

  • Caller and callee have the calling convention fastcc.

を満たしていないので当然か。

つぎに、上記の fastcc を満たす版 test.3 を作成。微妙な違いだけど、define の後に fastcc の指定があり、@fn 内部での呼び出しを tail call fastcc に変えてある。

;; fastcc ver.

@fmt_d = internal constant [3 x i8] c"%d\00"
declare i32 @printf(i8* noalias, ...)
declare i32 @putchar(i32)

define i32 @main ()  {
entry:
    %ret = call fastcc i32 @fn(i32 100000000)
    %fmt = getelementptr [3 x i8 ]* @fmt_d, i64 0, i64 0
    call i32 (i8*, ...)* @printf(i8* %fmt, i32 %ret)
    call i32 @putchar(i32 10)
    ret i32 0
}

define fastcc i32 @fn (i32 %x)  {
entry:
    %cmp = icmp eq i32 %x, 1
    br i1 %cmp, label %if_true, label %if_else
    if_else:
    %x2 = sub i32 %x, 1
    %ret = tail call fastcc i32 @fn(i32 %x2)
    ret i32 %ret
    if_true:
    ret i32 1
}

これを オプション無しの llc で .s を作り llvm-gcc で実行ファイルを作るとやはり segmentation fault 。が、ちゃんと llc -tailcallopt とすると無事エラーが出なくなった。本当に最適化されたかどうか見るため、PowerPCアセンブリを見てみる。

;; test 1
	.globl	_fn
	.align	4
_fn:
Leh_func_begin2:
	mflr r0
	stw r0, 8(r1)
Llabel3:
	stwu r1, -64(r1)
Llabel4:
	cmplwi cr0, r3, 1
	beq cr0, LBB2_2	; if_true
LBB2_1:	; if_else
	addi r3, r3, -1
	bl _fn
	addi r1, r1, 64
	lwz r0, 8(r1)
	mtlr r0
	blr 
LBB2_2:	; if_true
	li r3, 1
	addi r1, r1, 64
	lwz r0, 8(r1)
	mtlr r0
	blr 
;; test 3
	.globl	_fn
	.align	4
_fn:
Leh_func_begin2:
	mflr r0
	stw r31, 20(r1)
	stw r0, 8(r1)
Llabel3:
	stwu r1, -64(r1)
Llabel4:
	mr r31, r1
	cmplwi cr0, r3, 1
	bne cr0, LBB2_2	; if_else
LBB2_1:	; if_true
	li r3, 1
	addi r1, r31, 64
	lwz r0, 8(r1)
	lwz r31, 20(r1)
	mtlr r0
	addi r1, r1, 64
	blr 
LBB2_2:	; if_else
	addi r3, r3, -1
	addi r1, r31, 64
	lwz r0, 8(r1)
	lwz r31, 20(r1)
	mtlr r0
	b _fn
	#TC_RETURNd _fn 0
Leh_func_end2:

残念ながら私の能力では PowerPCアセンブリを読めないので、 b と blr が違う、位しか言えないが、 X86 用を出力すると少し分かり良い感じ(X86 も読める訳では無いけど、、)。llc -march=x86 とすれば PowerPC 環境でも X86アセンブリが参照できる。

;; test 1
_fn:
Leh_func_begin2:
Llabel2:
	subl	$12, %esp
	movl	16(%esp), %eax
	cmpl	$1, %eax
	je	LBB2_2	## if_true
LBB2_1:	## if_else
	decl	%eax
	movl	%eax, (%esp)
	call	_fn
	addl	$12, %esp
	ret
LBB2_2:	## if_true
	movl	$1, %eax
	addl	$12, %esp
	ret
Leh_func_end2:
;; test 3
_fn:
Leh_func_begin2:
Llabel2:
	subl	$12, %esp
	cmpl	$1, %ecx
	jne	LBB2_2	## if_else
LBB2_1:	## if_true
	movl	$1, %eax
	addl	$12, %esp
	ret	$12
LBB2_2:	## if_else
	decl	%ecx
	addl	$12, %esp
	jmp	_fn  # TAILCALL
Leh_func_end2:

Tail call optimization によって jmp 命令に変わっている。

また、それぞれの .bc ファイルを直接 lli コマンドで実行してみたところ、以下のようになった。

  • test.1.bc - -tailcallopt を付けても付けなくても Illegal instruction エラー
  • test.3.bc - -tailcallopt を付けた場合のみ正常終了, 付けないと test.1.bc と同じ

まとめとして、(こうやって書いてしまうと面白くもなんともない、、)


次はこれと自作 Scheme コンパイラもどきの関連。