関数内関数
例外はうまく動かない(llvm-g++でも無効にされている)みたいですし、拡張精度浮動小数点型はサポートしてないですし、GCはやる気ないし、ということで関数内関数です。
これがしっかりインプリメントされていれば、まだ気力が維持できるというものです。
a.c
int out(int x, int y) { int in(int z) { return x + z; } return in(y); }
a.ll
; ModuleID = 'a.c' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64" target triple = "mingw32" %struct.FRAME.out = type { i32 } define i32 @out(i32 %x, i32 %y) { entry: %x_addr = alloca i32 ; <i32*> [#uses=2] %y_addr = alloca i32 ; <i32*> [#uses=2] %retval = alloca i32, align 4 ; <i32*> [#uses=2] %FRAME.0 = alloca %struct.FRAME.out, align 4 ; <%struct.FRAME.out*> [#uses=2] %tmp = alloca i32, align 4 ; <i32*> [#uses=2] "alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] store i32 %x, i32* %x_addr store i32 %y, i32* %y_addr %tmp1 = getelementptr %struct.FRAME.out* %FRAME.0, i32 0, i32 0 ; <i32*> [#uses=1] %tmp2 = load i32* %x_addr ; <i32> [#uses=1] store i32 %tmp2, i32* %tmp1 %tmp3 = load i32* %y_addr ; <i32> [#uses=1] %tmp4 = call i32 @in.930( %struct.FRAME.out* %FRAME.0 inreg , i32 %tmp3 ) ; <i32> [#uses=1] store i32 %tmp4, i32* %tmp %tmp5 = load i32* %tmp ; <i32> [#uses=1] store i32 %tmp5, i32* %retval br label %return return: ; preds = %entry %retval6 = load i32* %retval ; <i32> [#uses=1] ret i32 %retval6 } define internal i32 @in.930(%struct.FRAME.out* inreg %CHAIN.1, i32 %z) { entry: %CHAIN.1_addr = alloca %struct.FRAME.out* ; <%struct.FRAME.out**> [#uses=2] %z_addr = alloca i32 ; <i32*> [#uses=2] %retval = alloca i32, align 4 ; <i32*> [#uses=2] %tmp = alloca i32, align 4 ; <i32*> [#uses=2] %tmp1 = alloca i32, align 4 ; <i32*> [#uses=2] "alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] store %struct.FRAME.out* %CHAIN.1, %struct.FRAME.out** %CHAIN.1_addr store i32 %z, i32* %z_addr %tmp2 = load %struct.FRAME.out** %CHAIN.1_addr ; <%struct.FRAME.out*> [#uses=1] %tmp3 = getelementptr %struct.FRAME.out* %tmp2, i32 0, i32 0 ; <i32*> [#uses=1] %tmp4 = load i32* %tmp3 ; <i32> [#uses=1] store i32 %tmp4, i32* %tmp %tmp5 = load i32* %tmp ; <i32> [#uses=1] %tmp6 = load i32* %z_addr ; <i32> [#uses=1] %tmp7 = add i32 %tmp5, %tmp6 ; <i32> [#uses=1] store i32 %tmp7, i32* %tmp1 %tmp8 = load i32* %tmp1 ; <i32> [#uses=1] store i32 %tmp8, i32* %retval br label %return return: ; preds = %entry %retval9 = load i32* %retval ; <i32> [#uses=1] ret i32 %retval9 }
親のローカル変数全部構造体にぶち込む作りだー。
llc。
a.s
.text .align 16 .globl _out .def _out; .scl 2; .type 32; .endef _out: subl $28, %esp movl 32(%esp), %eax movl %eax, 24(%esp) movl 36(%esp), %eax movl %eax, 20(%esp) movl 24(%esp), %eax movl %eax, 8(%esp) movl 20(%esp), %eax movl %eax, (%esp) leal 8(%esp), %eax call _in.930 movl %eax, 4(%esp) movl %eax, 16(%esp) LBB1_1: #return movl 16(%esp), %eax addl $28, %esp ret .align 16 .def _in.930; .scl 3; .type 32; .endef _in.930: subl $20, %esp movl %eax, 16(%esp) movl 24(%esp), %eax movl %eax, 12(%esp) movl 16(%esp), %eax movl (%eax), %eax movl %eax, 4(%esp) addl 12(%esp), %eax movl %eax, (%esp) movl %eax, 8(%esp) LBB2_1: #return movl 8(%esp), %eax addl $20, %esp ret
llcは本当に機械的なトランスレートしかしないな……。
いきなりやる気減。親のebp触れよ!
もっとも、llvm.frameaddressはあるのですが、インライン展開どころかもっと過激に最適化するのでデバッグ以外じゃ使えねーよ、などと書いてあります。
ようしそれなら見せてもらおうその最適化能力。
optの使い方がいまいちわかりませんでしたので、llvm-ldに-O5を渡して最適化します。
a-o.ll
; ModuleID = 'a.out.bc' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64" target triple = "mingw32" %struct.FRAME.out = type { i32 } define i32 @out(i32 %x, i32 %y) { entry: %tmp7.i = add i32 %x, %y ; <i32> [#uses=1] ret i32 %tmp7.i }
a-o.s
.text .align 16 .globl _out .def _out; .scl 2; .type 32; .endef _out: movl 4(%esp), %eax addl 8(%esp), %eax ret
見事にインライン化されました。
次は勿論downward closureですね。
thunk.c
#include<stdio.h> void f(void (*c)(void)) { c(); } int main(int argc, char **argv) { void i(void) { printf("%d\n", argc); } f(i); return 0; }
もしllvm-gccにAdaが含まれるようになりましたら、これがどうなるかが全てを分けます。大袈裟です。
thunk.ll
; ModuleID = 'thunk.bc' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64" target triple = "mingw32" %struct.FRAME.main = type { i32, %struct.__builtin_trampoline } %struct.__builtin_trampoline = type { [10 x i8] } @.str = internal constant [4 x i8] c"%d\0A\00" ; <[4 x i8]*> [#uses=1] define void @f(void ()* %c) { entry: %c_addr = alloca void ()* ; <void ()**> [#uses=2] "alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] store void ()* %c, void ()** %c_addr %tmp = load void ()** %c_addr ; <void ()*> [#uses=1] call void %tmp( ) br label %return return: ; preds = %entry ret void } define i32 @main(i32 %argc, i8** %argv) { entry: %argc_addr = alloca i32 ; <i32*> [#uses=2] %argv_addr = alloca i8** ; <i8***> [#uses=1] %retval = alloca i32, align 4 ; <i32*> [#uses=2] %FRAME.4 = alloca %struct.FRAME.main, align 16 ; <%struct.FRAME.main*> [#uses=4] %tmp = alloca i8*, align 4 ; <i8**> [#uses=2] %tmp1 = alloca void ()*, align 4 ; <void ()**> [#uses=2] %tmp2 = alloca i32, align 4 ; <i32*> [#uses=2] "alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] store i32 %argc, i32* %argc_addr store i8** %argv, i8*** %argv_addr %tmp3 = getelementptr %struct.FRAME.main* %FRAME.4, i32 0, i32 0 ; <i32*> [#uses=1] %tmp4 = load i32* %argc_addr ; <i32> [#uses=1] store i32 %tmp4, i32* %tmp3 %tmp5 = getelementptr %struct.FRAME.main* %FRAME.4, i32 0, i32 1 ; <%struct.__builtin_trampoline*> [#uses=1] %tmp56 = bitcast %struct.__builtin_trampoline* %tmp5 to i8* ; <i8*> [#uses=1] %FRAME.47 = bitcast %struct.FRAME.main* %FRAME.4 to i8* ; <i8*> [#uses=1] call void @__builtin_init_trampoline( i8* %tmp56, i8* bitcast (void (%struct.FRAME.main* inreg )* @i.1376 to i8*), i8* %FRAME.47 ) %tmp8 = getelementptr %struct.FRAME.main* %FRAME.4, i32 0, i32 1 ; <%struct.__builtin_trampoline*> [#uses=1] %tmp89 = bitcast %struct.__builtin_trampoline* %tmp8 to i8* ; <i8*> [#uses=1] %tmp10 = call i8* @__builtin_adjust_trampoline( i8* %tmp89 ) ; <i8*> [#uses=1] store i8* %tmp10, i8** %tmp %tmp11 = load i8** %tmp ; <i8*> [#uses=1] %tmp1112 = bitcast i8* %tmp11 to void ()* ; <void ()*> [#uses=1] store void ()* %tmp1112, void ()** %tmp1 %tmp13 = load void ()** %tmp1 ; <void ()*> [#uses=1] call void @f( void ()* %tmp13 ) store i32 0, i32* %tmp2 %tmp14 = load i32* %tmp2 ; <i32> [#uses=1] store i32 %tmp14, i32* %retval br label %return return: ; preds = %entry %retval15 = load i32* %retval ; <i32> [#uses=1] ret i32 %retval15 } declare void @__builtin_init_trampoline(i8*, i8*, i8*) declare i8* @__builtin_adjust_trampoline(i8*) define internal void @i.1376(%struct.FRAME.main* inreg %CHAIN.5) { entry: %CHAIN.5_addr = alloca %struct.FRAME.main* ; <%struct.FRAME.main**> [#uses=2] %tmp = alloca i32, align 4 ; <i32*> [#uses=2] "alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] store %struct.FRAME.main* %CHAIN.5, %struct.FRAME.main** %CHAIN.5_addr %tmp1 = load %struct.FRAME.main** %CHAIN.5_addr ; <%struct.FRAME.main*> [#uses=1] %tmp2 = getelementptr %struct.FRAME.main* %tmp1, i32 0, i32 0 ; <i32*> [#uses=1] %tmp3 = load i32* %tmp2 ; <i32> [#uses=1] store i32 %tmp3, i32* %tmp %tmp4 = getelementptr [4 x i8]* @.str, i32 0, i32 0 ; <i8*> [#uses=1] %tmp5 = load i32* %tmp ; <i32> [#uses=1] %tmp6 = call i32 (i8*, ...)* @printf( i8* %tmp4, i32 %tmp5 ) ; <i32> [#uses=0] br label %return return: ; preds = %entry ret void } declare i32 @printf(i8*, ...)
へえ……トランポリンをサポートしていますね。
thunk.s
.text .align 16 .globl _f .def _f; .scl 2; .type 32; .endef _f: subl $12, %esp movl 16(%esp), %eax movl %eax, 8(%esp) call *%eax LBB1_1: #return addl $12, %esp ret .align 16 .globl _main .def _main; .scl 2; .type 32; .endef _main: subl $60, %esp movl %ebp, 56(%esp) leal 56(%esp), %ebp andl $4294967280, %esp movl $16, %eax call __alloca movl %esi, -4(%ebp) call ___main fnstcw -54(%ebp) movb $2, -53(%ebp) fldcw -54(%ebp) movl 8(%ebp), %eax movl %eax, -8(%ebp) movl 12(%ebp), %eax movl %eax, -12(%ebp) movl -8(%ebp), %eax movl %eax, -40(%ebp) subl $16, %esp leal -40(%ebp), %eax movl %eax, 8(%esp) leal -36(%ebp), %esi movl %esi, (%esp) movl $_i.1376, 4(%esp) call ___builtin_init_trampoline addl $16, %esp subl $16, %esp movl %esi, (%esp) call ___builtin_adjust_trampoline addl $16, %esp movl %eax, -44(%ebp) movl %eax, -48(%ebp) subl $16, %esp movl %eax, (%esp) call _f addl $16, %esp movl $0, -52(%ebp) movl $0, -16(%ebp) LBB2_1: #return movl -16(%ebp), %eax movl -4(%ebp), %esi movl %ebp, %esp popl %ebp ret .align 16 .def _i.1376; .scl 3; .type 32; .endef _i.1376: subl $28, %esp movl %eax, 24(%esp) movl (%eax), %eax movl %eax, 20(%esp) movl %eax, 4(%esp) movl $_.str, (%esp) call _printf LBB3_1: #return addl $28, %esp ret .data _.str: # .str .asciz "%d\n" .def ___builtin_adjust_trampoline; .scl 2; .type 32; .endef .def ___builtin_init_trampoline; .scl 2; .type 32; .endef .def _printf; .scl 2; .type 32; .endef
___builtin_init_trampolineと___builtin_adjust_trampolineというランタイムが必要みたいです。
で、これを最適化しますと。
thunk-o.ll
; ModuleID = 'thunk_o.bc' target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64" target triple = "mingw32" %struct.FRAME.main = type { i32, %struct.__builtin_trampoline } %struct.__builtin_trampoline = type { [10 x i8] } @.str = internal constant [4 x i8] c"%d\0A\00" ; <[4 x i8]*> [#uses=1] define i32 @main(i32 %argc, i8** %argv) { entry: %FRAME.4 = alloca %struct.FRAME.main, align 16 ; <%struct.FRAME.main*> [#uses=3] %tmp3 = getelementptr %struct.FRAME.main* %FRAME.4, i32 0, i32 0 ; <i32*> [#uses=1] store i32 %argc, i32* %tmp3 %tmp56 = getelementptr %struct.FRAME.main* %FRAME.4, i32 0, i32 1, i32 0, i32 0 ; <i8*> [#uses=2] %FRAME.47 = bitcast %struct.FRAME.main* %FRAME.4 to i8* ; <i8*> [#uses=1] call void @__builtin_init_trampoline( i8* %tmp56, i8* bitcast (void (%struct.FRAME.main* inreg )* @i.1376 to i8*), i8* %FRAME.47 ) %tmp10 = call i8* @__builtin_adjust_trampoline( i8* %tmp56 ) ; <i8*> [#uses=1] %tmp1112 = bitcast i8* %tmp10 to void ()* ; <void ()*> [#uses=1] call void %tmp1112( ) ret i32 0 } declare void @__builtin_init_trampoline(i8*, i8*, i8*) declare i8* @__builtin_adjust_trampoline(i8*) define internal void @i.1376(%struct.FRAME.main* inreg %CHAIN.5) { entry: %tmp2 = getelementptr %struct.FRAME.main* %CHAIN.5, i32 0, i32 0 ; <i32*> [#uses=1] %tmp3 = load i32* %tmp2 ; <i32> [#uses=1] %tmp6 = call i32 (i8*, ...)* @printf( i8* getelementptr ([4 x i8]* @.str, i32 0, i32 0), i32 %tmp3 ) ; <i32> [#uses=0] ret void } declare i32 @printf(i8*, ...)
___builtin_init_trampolineと___builtin_adjust_trampolineってintrinsicじゃ無いのか!
外部関数が間に入っているせいで関数内関数のコールバックはインライン展開できない、と。まあgccでもインライン展開できませんが。
thunk-o.s
.text .align 16 .globl _main .def _main; .scl 2; .type 32; .endef _main: subl $44, %esp movl %ebp, 40(%esp) leal 40(%esp), %ebp andl $4294967280, %esp movl $16, %eax call __alloca movl %esi, -4(%ebp) call ___main fnstcw -26(%ebp) movb $2, -25(%ebp) fldcw -26(%ebp) movl 8(%ebp), %eax movl %eax, -24(%ebp) subl $16, %esp leal -24(%ebp), %eax movl %eax, 8(%esp) leal -20(%ebp), %esi movl %esi, (%esp) movl $_i.1376, 4(%esp) call ___builtin_init_trampoline addl $16, %esp subl $16, %esp movl %esi, (%esp) call ___builtin_adjust_trampoline addl $16, %esp call *%eax xorl %eax, %eax movl -4(%ebp), %esi movl %ebp, %esp popl %ebp ret .align 16 .def _i.1376; .scl 3; .type 32; .endef _i.1376: subl $12, %esp movl (%eax), %eax movl %eax, 4(%esp) movl $_.str, (%esp) call _printf addl $12, %esp ret .data _.str: # .str .asciz "%d\n" .def ___builtin_adjust_trampoline; .scl 2; .type 32; .endef .def ___builtin_init_trampoline; .scl 2; .type 32; .endef .def _printf; .scl 2; .type 32; .endef
トランポリンだけ見るとgccと等価ですのでそれはいいです。
それよりひどいのが親の引数の扱い。
丁寧にローカルにコピーし直すなよ!最初から親のebpを(ry
だめだこのVMは。何のためにスタックフレーム上でebpがリンクリストを構成してると思って(ry
LLVMには未来を見るべきなのであって、現状は見ちゃだめなんでしょうね……。
ところで、呼び出し規約などもひっくるめて真面目にやるなら、こんなめんどくさい中間言語になるなあ。
%out = type i32 (caller i32, callee i32, save i32 eip, save i32 ebp, local i32) define i32 @in(callee i32 arg, save i32 eip, save frameof<%out> ebp){ ...
callerは呼び出し側が開放する引数。全部そうなってるのがcdecl。calleeは関数側が開放する引数。全部そうなってるのがstdcall。他にregisterがある。
saveで少なくともeipとebpが保存される位置を示さないといけない。ebpを省略したらebpを省略する最適化OKの意味。
localは固定位置に確保するローカル変数。frameof<%関数型>で特定の関数型の中のebpを指す意味のつもり。%関数型*だと関数ポインタになってしまうので。
ここまでやるなら「最適化したあとのプログラムが使うスタックの量だけSPを移動」する命令も欲しいな……。
この手のバックエンドを本気で使おうとするなら、CPUの扱える数値型が全部使えること、OSがSEHをサポートしてるならそれをサポートすること、一般的な呼び出し規約は全部サポートむしろコルーチン用にスタックではなくヒープを使う関数を書けるぐらいの柔軟性、あと関数内関数を少なくともその辺の大した最適化なんかしないPascalコンパイラと同程度には扱えること、程度は必須と思う。最適化能力よりも優先的に。勿論最適化能力としてはバックエンドでしかできないレジスタ割りつけのほうがフロントエンドでもなんとかなるインライン化より重要。バックエンドのせいでできることが限定されたり、最適化なんかしないベタ機械語出力するコンパイラより効率が落ちたりしたら、嫌じゃないですか。
贅沢でしょうか?