尾递归

1.什么是递归?

递归就是自己调用自己

2.编译器是如何实现递归的?

编译器是通过栈来实现递归,其实编译器也是通过栈来实现函数调用的,为了明白递归,我们先来看看我们的程序是如何实现函数调用的吧。

下面我们看一个函数调用的栗子

int adder(int x,int y)
{
return x+y;
}

void call()
{
int x=2;
int y=3;
adder(x,y);
}
 

我们使用gcc编译成汇编(PS:要想明白一个程序是怎么运行的最好的方式还是看汇编吧)

gcc -S test.c

 

下面我们看看汇编片段


.file “recu.c”
.text
.globl adder
.type adder, @function
adder:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size adder, .-adder
.globl call
.type call, @function
call:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $2, -4(%rbp)
movl $3, -8(%rbp)
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %edx, %esi
movl %eax, %edi
call adder
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size call, .-call
.ident “GCC: (GNU) 4.8.2”
.section .note.GNU-stack,””,@progbits

 

擦!汇编怎么那么多看不懂的符号啊?!!真拙计!

**我们这里是r开头的,原因是我们使用的是64位的操作系统**

为了读懂汇编,我还是先去复习了一下,首先先明白几个寄存器吧

在X86上,用户寄存器为eax, ebx, ecx, edx, esi, edi, ebp, esp 以及eip

eax、ebx、ecx以及edx寄存器作为通用寄存器,可以用来进行临时存储

esi和edi可以用来存储,但对操作字符串类的函数有其他意义,在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串

ebp通常用来容纳当前栈帧(stack frame)的内存地址,esp保存栈顶地址
eip保存当前执行指令的内存地址。机器代码不能直接修改该寄存器,只能通过jmp和call指令族进行间接修改,实现循环,调用等

 

然后我们在明白一下重要术语:

栈帧:值得是ebpesp的这一段内存区间,每一个函数的调用都会生成一个栈帧,这里面保存着函数里的变量和指令。

我们主要关心call函数以及adder函数,所以单独拿出来

我们先来看call函数:

**这里的栈类别是指的向下满栈
call:
.LFB1:
.cfi_startproc                 ##这个表示函数的入口参见.cfi_startproc pushq %rbp                 ##把rbp寄存器入栈
.cfi_def_cfa_offset 16         ##定义CFI(call frame information)的cfa(Canonical Frame Address)的偏移量,主要是由于push了%rbp造成的偏移量
.cfi_offset 6, -16             ##.cfi我也不明
movq %rsp, %rbp ##把rsp的值搞到rbp中去,表示现在进入了新栈
.cfi_def_cfa_register 6 ##说明现在的cfa register是rbp
subq $16, %rsp              ##空出两个单位,根据程序,我们应该猜得到是为x、y变量提供空间,为毛是压16个字节?难道是内存对其?
movl $2, -4(%rbp)           ##把rbp下面的一个4字节定义为x的空间
movl $3, -8(%rbp)           ##把rbp下面的第8个字节处定义成y的空间
movl -8(%rbp), %edx         ##把x、y的地址赋给寄存器edx和eax,这两个是通用寄存器
movl -4(%rbp), %eax
movl %edx, %esi             ##又把他们送个esi、edi这里是为传值做准备
movl %eax, %edi
call adder                 ##这里调用adder函数,同时这个指令暗含一个把当前栈顶压入栈
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

下面用图来说明
   
 
好了,下面我们来谈谈尾递归。
主要围绕下面一个方面
1.什么是尾递归?与递归有什么区别?
我们先来一段代码来看看这两者之间的区别
 

int fact(int n){
if(n <0) return 0;
if(n ==0) return 1;
if(n ==1) return 1;
return n*fact(n-1);
}

int facttail(int n,int a){
if(n <0) return 0;
if(n ==0) return 1;
if(n ==1) return a;

return facttail(n-1,n*a);

}
 
fact函数是递归版本,facttail函数是尾递归版本。
 
我们在文章最初知道了函数调用是通过栈帧来实现的,因为栈帧中需要保存运算的变量,在使用递归的时候,比如函数fact,如果n非常大的话,很显然栈帧会越来越多,直到撑爆内存,这显然是我们不乐意见到的。
但是我们来看factail这个函数,它也是使用递归来实现求n的阶乘,但是注意到,这个函数中,其栈帧是没有变量需要保存的,这就是说我们的栈帧是不需要的,我们完全可以就在原来的栈帧上进行运算!!
于是乎C编译器抖了个机灵,它如果发现了你使用了尾递归,他就会自动帮你优化,现在我们来看着两个函数的汇编。
为了看着方便,我们把判断全部删除,如下
 

int fact(int n){
return n*fact(n-1);
}

int facttail(int n,int a){
return facttail(n-1,n*a);
}
汇编代码如下:
 
 

fact:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
subl $1, %eax
movl %eax, %edi
call fact
imull -4(%rbp), %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size fact, .-fact
.globl facttail
.type facttail, @function
facttail:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
imull -8(%rbp), %eax
movl -4(%rbp), %edx
subl $1, %edx
movl %eax, %esi
movl %edx, %edi
call facttail
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

卧槽!怎么都是递归呢?和传说中主动优化的不一样呢?
 
原来是我搞忘记加入优化参数了,现在把优化参数加上,汇编代码如下
 

.file   “fact.c”
.text
.p2align 4,,15
.globl fact
.type fact, @function
fact:
.LFB7:
.cfi_startproc
.p2align 4,,10
.p2align 3
.L2:
jmp .L2
.cfi_endproc
.LFE7:
.size fact, .-fact
.p2align 4,,15
.globl facttail
.type facttail, @function
facttail:
.LFB8:
.cfi_startproc
.p2align 4,,10
.p2align 3
.L4:
jmp .L4
.cfi_endproc
.LFE8:
.size facttail, .-facttail
.ident “GCC: (GNU) 4.8.2”
.section .note.GNU-stack,””,@progbits

卧槽,我完全看不懂了,不过估计这东西优化成为了一个循环,看.L4,不过这个怎么把递归也搞成循环了?!这个真心不懂,求大家指教