Scheme Compiler の勉強(24) - 比較

※適当な粒度で書いていたら 24 回にもなってしまった。元の文書だと全部で24ステップなのに。

コンパイラ自体は単純だけど、それが吐き出す結果は十分ややこしい。procedure の定義までできるようになったので、結果をよく眺めてみることにする。また、いまの直接命令を出力する単純なプログラムを、すこし奇麗にしたい。このあとは Iteration via Proper Tail Calls と Heap Allocation で、ちょっと立ち止まって準備しないとできる気がしない。

fib

まずフィボナッチ数列で。

(letrec ((fib (lambda (n)
		(if (fx< n 2)
		    1
		    (fx+ (fib (fx- n 2)) (fib (fx- n 1)))))))
  (fib 9))
=> 55

これをそのままコンパイルして LLVM IR にする。一行ずつ追って見よう、と思ったけどあまりに面倒なので、 compiler の方で適当にコメントを出力することにした。何の最適化もしていないのでとても冗長。

; emit-letrec: (letrec ((fib (lambda (n) (if (fx< n 2) 1 (fx+ (fib (fx- n 2)) (fib (fx- n 1))))))) (fib 9))
define i32 @Label_1(i32 %arg1) nounwind {
entry:
    %ret = alloca i32
    %sta1 = alloca i32
    store i32 %arg1, i32* %sta1
; emit-if (if (fx< n 2) 1 (fx+ (fib (fx- n 2)) (fib (fx- n 1))))
; emit-if-test
; fx<: 
; eval and store arg1
    %var9 = load i32* %sta1
    store i32 %var9, i32* %ret
    %sta2 = alloca i32
    %var3 = load i32* %ret
    store i32 %var3, i32* %sta2
; eval and store arg2
    store i32 8, i32* %ret
    %sta3 = alloca i32
    %var4 = load i32* %ret
    store i32 %var4, i32* %sta3
    %var5 = load i32* %sta2
    %var6 = load i32* %sta3
; icmp with 'slt
    %var7 = icmp slt i32 %var5, %var6
    %var8 = select i1 %var7, i32 111, i32 47
    store i32 %var8, i32* %ret
    %var1 = load i32* %ret
; compare %ret to #f
    %var2 = icmp eq i32 %var1, 47
    br i1 %var2, label %Label_3, label %Label_2
Label_2:
    store i32 4, i32* %ret
    br label %Label_4
Label_3:
; fx+
; eval and store arg1
; fx-
; eval and store arg1
    %var22 = load i32* %sta1
    store i32 %var22, i32* %ret
    %sta6 = alloca i32
    %var17 = load i32* %ret
    store i32 %var17, i32* %sta6
; eval and store arg2
    store i32 8, i32* %ret
    %sta7 = alloca i32
    %var18 = load i32* %ret
    store i32 %var18, i32* %sta7
    %var19 = load i32* %sta6
    %var20 = load i32* %sta7
; sub arg1 arg2
    %var21 = sub i32 %var19, %var20
    store i32 %var21, i32* %ret
    %var16 = load i32* %ret
    %var15 = call i32 @Label_1(i32 %var16)
    store i32 %var15, i32* %ret
    %sta4 = alloca i32
    %var10 = load i32* %ret
    store i32 %var10, i32* %sta4
; eval and store arg1
; fx-
; eval and store arg1
    %var30 = load i32* %sta1
    store i32 %var30, i32* %ret
    %sta8 = alloca i32
    %var25 = load i32* %ret
    store i32 %var25, i32* %sta8
; eval and store arg2
    store i32 4, i32* %ret
    %sta9 = alloca i32
    %var26 = load i32* %ret
    store i32 %var26, i32* %sta9
    %var27 = load i32* %sta8
    %var28 = load i32* %sta9
; sub arg1 arg2
    %var29 = sub i32 %var27, %var28
    store i32 %var29, i32* %ret
    %var24 = load i32* %ret
    %var23 = call i32 @Label_1(i32 %var24)
    store i32 %var23, i32* %ret
    %sta5 = alloca i32
    %var11 = load i32* %ret
    store i32 %var11, i32* %sta5
    %var12 = load i32* %sta4
    %var13 = load i32* %sta5
; add arg1 arg2
    %var14 = add i32 %var12, %var13
    store i32 %var14, i32* %ret
    br label %Label_4
Label_4:
    %retval = load i32* %ret
    ret i32 %retval
}
define i32 @scheme_entry() nounwind {
entry:
    %ret = alloca i32
    store i32 36, i32* %ret
    %var32 = load i32* %ret
    %var31 = call i32 @Label_1(i32 %var32)
    store i32 %var31, i32* %ret
    %retval = load i32* %ret
    ret i32 %retval
}

LLVM の opt コマンドで最適化すると、以下のようになる。最適化するとコメントが消されるので、再度手で付加した。

; ModuleID = 'stst.opt.bc'

define i32 @Label_1(i32 %arg1) nounwind  { ;; fib 本体
entry:
	%var7 = icmp slt i32 %arg1, 8		;; (if (< n 2) 
	br i1 %var7, label %UnifiedReturnBlock, label %Label_3 ;; jump

Label_3:		; 
	%var21 = add i32 %arg1, -8		; var21 = n - 2
	%var15 = call i32 @Label_1( i32 %var21 )		; var15 = (fib var21) = (fib (- n 2))
	%var29 = add i32 %arg1, -4		; var29 = n - 1
	%var23 = call i32 @Label_1( i32 %var29 )		; var23 = (fib var29) = (fib (- n 1))
	%var14 = add i32 %var23, %var15		; <i32> [#uses=1] ; var14 = (+ var23 var15)
	ret i32 %var14

UnifiedReturnBlock:		;
	ret i32 4 ; return 1
}

define i32 @scheme_entry() nounwind  {
entry:
	%var15.i = tail call i32 @Label_1( i32 28 ) nounwind 		; var15.i = (fib 7)
	%var23.i = tail call i32 @Label_1( i32 32 ) nounwind 		; var23.i = (fib 8)
	%var14.i = add i32 %var23.i, %var15.i		; var14.i = (+ (fib 7) (fib 8))
	ret i32 %var14.i
}
  • fib の本体はほとんど scheme での関数の定義そのものになった。
  • 呼び出し側(scheme_entry) では (fib 9) が (+ (fib 7) (fib 8)) になった。比較演算を減らすため?また call が tail call になった(あとで調べる)。

PowerPC 用にはこうなる。うーん、"32ビットPowerPCアーキテクチャ プログラミング環境"を一行一行しらべないと。

	.machine ppc7400
	.section __TEXT,__textcoal_nt,coalesced,pure_instructions
	.section __TEXT,__symbol_stub1,symbol_stubs,pure_instructions,16
	.text


	.globl	_Label_1
	.align	4
_Label_1:
	mflr r0
	stw r0, 8(r1)
	stwu r1, -64(r1)
	stw r29, 60(r1)
	stw r30, 56(r1)
	cmpwi cr0, r3, 8
	mr r30, r3
	blt cr0, LBB1_2	; UnifiedReturnBlock
LBB1_1:	; Label_3
	addi r3, r30, -8
	bl _Label_1
	mr r29, r3
	addi r3, r30, -4
	bl _Label_1
	add r3, r3, r29
	lwz r30, 56(r1)
	lwz r29, 60(r1)
	addi r1, r1, 64
	lwz r0, 8(r1)
	mtlr r0
	blr 
LBB1_2:	; UnifiedReturnBlock
	li r3, 4
	lwz r30, 56(r1)
	lwz r29, 60(r1)
	addi r1, r1, 64
	lwz r0, 8(r1)
	mtlr r0
	blr 


	.globl	_scheme_entry
	.align	4
_scheme_entry:
	mflr r0
	stw r0, 8(r1)
	stwu r1, -64(r1)
	stw r30, 60(r1)
	li r3, 28
	bl _Label_1
	mr r30, r3
	li r3, 32
	bl _Label_1
	add r3, r3, r30
	lwz r30, 60(r1)
	addi r1, r1, 64
	lwz r0, 8(r1)
	mtlr r0
	blr 

	.subsections_via_symbols

llc コマンドに -march=x86 を与えるとこうなる。少しだけみやすい?
※現在 www.intel.com にアクセスできないのでインストラクションセットが入手できない‥

	.text
	.align	4,0x90
	.globl	_Label_1
_Label_1:
	pushl	%edi
	pushl	%esi
	subl	$4, %esp
	movl	16(%esp), %esi
	cmpl	$8, %esi
	jge	LBB1_3	## Label_3
LBB1_1:	## UnifiedReturnBlock
	movl	$4, %eax
LBB1_2:	## UnifiedReturnBlock
	addl	$4, %esp
	popl	%esi
	popl	%edi
	ret
LBB1_3:	## Label_3
	leal	-8(%esi), %eax
	movl	%eax, (%esp)
	call	_Label_1
	movl	%eax, %edi
	addl	$4294967292, %esi
	movl	%esi, (%esp)
	call	_Label_1
	addl	%edi, %eax
	jmp	LBB1_2	## UnifiedReturnBlock


	.align	4,0x90
	.globl	_scheme_entry
_scheme_entry:
	pushl	%esi
	subl	$8, %esp
	movl	$28, (%esp)
	call	_Label_1
	movl	%eax, %esi
	movl	$32, (%esp)
	call	_Label_1
	addl	%esi, %eax
	addl	$8, %esp
	popl	%esi
	ret

	.subsections_via_symbols

「コンピュータの構成と設計」p.171

実際、最近のコンパイラに太刀打ちしようとすると、アセンブリ言語プログラマはパイプライン処理や記憶階層の概念を完全に理解していなければならない(それらについては、第6章と第7章で説明する)。

このテキストは10年位前のもの。コンパイラを本気で書く、とか、マシンの性能の限界に挑戦、なんて人たちはどこまで知識を獲得しておく必要があるのか‥。まあ逆に言えばまだまだ正解がなさそうで楽しそうな領域とも言える。