Cの末尾再帰

最近のCコンパイラは末尾再帰の最適化を行ってくれるそうです。
しかし、Cにはポインタがある以上、上手く最適化できない場合もあるよなー
みたいなことを一年ほど前に思いついたんですが、試すのが面倒で放置してました。
それが、今さらになって気になってきたので試してみることにしました。
コンパイラには『gcc (GCC) 4.2.1 20070719 [FreeBSD]』を使用。
まずは、最適化されそうな関数。

int f(int n, int acc) {
if (n <= 0) {
return acc;
}
return f(n-1, acc+nanika(n));
}

これを『gcc -c -S -O2』でコンパイルしたところ、以下のようになりました。


f:
pushl   %ebp
movl    %esp, %ebp
pushl   %esi
pushl   %ebx
subl    $16, %esp
movl    8(%ebp), %ebx
movl    12(%ebp), %esi
testl   %ebx, %ebx
jle     .L8
.p2align 4,,7
.L11:
movl    %ebx, (%esp)
call    nanika
addl    %eax, %esi
subl    $1, %ebx
jne     .L11
.L8:
addl    $16, %esp
movl    %esi, %eax
popl    %ebx
popl    %esi
popl    %ebp
ret

再帰がジャンプになってます。さすがGCC。
で、次は去年思いついてた最適化されなさそうなコード。

int g(int x, int *p, int count)
{
x = 0;
x = *p + 1;
if (count <= 0) {
return x;
}
return g(x, &x, count -1);
}

もし、これがループに置き換われば、一つ前のxがx=0により消されてしまうため、
その後で*pで参照するときにおかしなことになってしまいます。
上と同様のオプションでコンパイルしたところ、以下のようになりました。


g:
pushl   %ebp
movl    %esp, %ebp
subl    $12, %esp
movl    12(%ebp), %eax
movl    16(%ebp), %ecx
movl    $0, 8(%ebp)
movl    (%eax), %edx
addl    $1, %edx
testl   %ecx, %ecx
jle     .L2
leal    -1(%ecx), %eax
movl    %eax, 8(%esp)
leal    8(%ebp), %eax
movl    %edx, 8(%ebp)
movl    %edx, (%esp)
movl    %eax, 4(%esp)
call    g
movl    %eax, %edx
.L2:
leave
movl    %edx, %eax
ret

想像通り、最適化は行われずにcallになっていました。
Cにはポインタがある以上、末尾再帰の最適化は完全には行えないよなー、
という夢のないお話でした。

Leave a Reply