ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

Exercise 2.16-2.18 Fibonacci number

フィボナッチ数を、再帰バージョンと末尾再帰バージョンにしたときの比較です。

SICPの1.2.2 Tree Recursionと全く同じ問題ですね。
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-11.html#%_sec_1.2.2

再帰バージョン

iint fib(int n){
	if (n == 0)
		return 0;
	else if (n == 1)
		return 1;
	else
		return fib(n-1) + fib(n-2);
}

コンパイルした結果です。
普通の再帰関数の呼出になっています。

fib:
	
	addiu	$sp,$sp,-40
	sw	$31,32($sp)
	sw	$fp,28($sp)
	sw	$16,24($sp)
	move	$fp,$sp
	sw	$4,40($fp)
	lw	$2,40($fp)
	bne	$2,$0,.L2
	nop

	sw	$0,16($fp)
	b	.L1
	nop

.L2:
	lw	$3,40($fp)
	li	$2,1			# 0x1
	bne	$3,$2,.L4
	nop

	li	$2,1			# 0x1
	sw	$2,16($fp)
	b	.L1
	nop

.L4:
	lw	$2,40($fp)
	addiu	$2,$2,-1
	move	$4,$2
	jal	fib     # 再帰呼び出し
	nop

	move	$16,$2
	lw	$2,40($fp)
	addiu	$2,$2,-2
	move	$4,$2
	jal	fib     # 再帰呼び出し
	nop

	addu	$16,$16,$2
	sw	$16,16($fp)
.L1:
	lw	$2,16($fp)
	move	$sp,$fp
	lw	$31,32($sp)
	lw	$fp,28($sp)
	lw	$16,24($sp)
	addiu	$sp,$sp,40
	j	$31
	nop

末尾再帰バージョン

コンパイル

int fib_iter (int a, int b, int count) {
	if (count == 0)
		return b;
	else
		return fib_iter(a + b, a, count - 1);
}

コンパイル後のアセンブラです。
末尾再帰の最適化がされており、関数呼び出しが無くなっています。

fib_iter:
	b	.L4
	move	$2,$5

.L7:
	move	$2,$3
	addiu	$6,$6,-1
.L4:
	move	$3,$4
	bne	$6,$0,.L7
	addu	$4,$4,$2

	j	$31
	nop

Exercise 2.18

ざっくり計算量を見積もる。
再帰バージョンの場合、f(n+1) = f(n) + f(n-1)で、nが増える度に関数呼び出し回数が約倍になります。なので、計算量はO(2^n) で近似できます。

末尾再帰バージョンの場合、,L7からはじめる5命令のループが実際に計算をしているところになります。計算量はO(n)で表現でき、 メモリにはアクセスしていないのでn × 5 [命令] で実行時間も見積もれます。

再帰バージョンの計算量を厳密に求めると、(1+√5)/2のn乗になります。SICPの練習問題にもなっています。
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-11.html#%_thm_1.13

フィボナッチ数について

フィボナッチ数といえば、上の再帰iteratorの変換だけでなく、メモ化の題材としてもよく使われます。

Higher-order PerlからPerlで最適化をしている例を紹介します。
http://hop.perl.plover.com/Examples/Chap5/
fib-0からfib-12まで順番に見てみましょう。

他にも不動点を使ってフィボナッチ数を求める話もあります。
http://www.kmonos.net/wlog/52.php#_0212050903