族谱网 头条 人物百科

尾调用

2020-10-16
出处:族谱网
作者:阿族小谱
浏览:335
转发:0
评论:0
描述当一个函数调用发生时,电脑必须“记住”调用函数的位置—返回位置,才可以在调用结束时带着返回值回到该位置,返回位置一般存在调用栈上。在尾调用的情况中,电脑不需要记住尾调用的位置而可以从被调用的函数直接带着返回值返回调用函数的返回位置(相当于直接连续返回两次),尾调用消除即是在不改变当前调用栈(也不添加新的返回位置)的情况下跳到新函数的一种优化(完全不改变调用栈是不可能的,还是需要校正调用栈上形参与局部变量的信息。)对函数调用在尾位置的递归或互相递归的函数,由于函数自身调用次数很多,递归层级很深,尾递归优化则使原本O(n)的调用栈空间只需要O(1)。因此一些编程语言的标准要求语言实现进行尾调用消除,例如Scheme与ML家族的语言。在Scheme中,语言标准还将尾位置形式化,指定了各种语法中允许尾调用的地方。以Python为例,主要区分普通递归和尾递归对栈空间的使用:defrecsum(x...

描述

当一个函数调用发生时,电脑必须 “记住” 调用函数的位置 — 返回位置,才可以在调用结束时带着返回值回到该位置,返回位置一般存在调用栈上。在尾调用的情况中,电脑不需要记住尾调用的位置而可以从被调用的函数直接带着返回值返回调用函数的返回位置(相当于直接连续返回两次),尾调用消除即是在不改变当前调用栈(也不添加新的返回位置)的情况下跳到新函数的一种优化(完全不改变调用栈是不可能的,还是需要校正调用栈上形参与局部变量的信息。)

对函数调用在尾位置的递归或互相递归的函数,由于函数自身调用次数很多,递归层级很深,尾递归优化则使原本 O(n) 的调用栈空间只需要 O(1)。因此一些编程语言的标准要求语言实现进行尾调用消除,例如Scheme与ML家族的语言。在Scheme中,语言标准还将尾位置形式化,指定了各种语法中允许尾调用的地方。

以Python为例,主要区分普通递归和尾递归对栈空间的使用:

defrecsum(x):ifx==1:returnxelse:returnx+recsum(x-1)

调用recsum(5)为例,SICP中描述了相应的栈空间变化:

recsum(5)5+recsum(4)5+(4+recsum(3))5+(4+(3+recsum(2)))5+(4+(3+(2+recsum(1))))5+(4+(3+(2+1)))5+(4+(3+3))5+(4+6)5+1015

可观察,堆栈从左到右,增加到一个峰值后再计算从右到左缩小,这往往是我们不希望的,所以在C语言等语言中设计for, while, goto等特殊结构语句,使用迭代、尾递归,对普通递归进行优化,减少可能对内存的极端消耗。修改以上代码,可以成为尾递归:

deftailrecsum(x,running_total=0):ifx==0:returnrunning_totalelse:returntailrecsum(x-1,running_total+x)

或者使用迭代:

foriinrange(6):sum+=i

对比后者尾递归对内存的消耗:

tailrecsum(5,0)tailrecsum(4,5)tailrecsum(3,9)tailrecsum(2,12)tailrecsum(1,14)tailrecsum(0,15)15

则是线性的。

语法上的表现

尾调用可能位于一个函数语法上最后的位置:

functionfoo(data){a(data);returnb(data);}

在这里,a(data)、b(data) 都是函数调用,但是 b(data) 是函数返回前的最后运行的东西,所以也是所谓的尾位置。然后,并非所有的尾调用都必须在一个函数语法上最后的位置。考虑:

functionbar(data){if(a(data)){returnb(data);}returnc(data);}

在这里,b、c 的调用都在尾位置。这是因为尽管 b(data) 不在 bar 语法上最后的位置,它是 if 叙述其中一个分支最后的位置。

现在考虑以下代码:

functionfoo1(data){returna(data)+1;}

functionfoo2(data){varret=a(data);returnret;}

functionfoo3(data){varret=a(data);return(ret===0)?1:ret;}

在这里,a(data) 处于 foo2 的尾位置,但不处于 foo1 或 foo3 的尾位置。这是因为程序必须返回这两个 a 函数的调用以检查、更动 a 的返回值。

然而,对于 C++ 等语言来说,在函数最后 return g(x); 并不一定是尾递归——在返回之前很可能涉及到对象的析构函数,使得 g(x) 不是最后执行的那个。这可以通过返回值优化来解决。

实例

通常被用于解释递归的程序是计算阶乘。以下计算阶乘的Scheme程序不是尾部递归,而只是一般递归:

(define (factorialn)(if (= n1)1(* n(factorial(- n1)))))

因此,如果调用 factorial 时的参数 n 足够大,这一程序会出现堆栈溢出。然而,如果将同一程序写作尾部递归,按 Scheme 的标准将不会出现溢出:

(define (factorialn)(define (iterproductcounter)(if (> countern)product(iter(* counterproduct)(+ counter1))))(iter11))

在第二个程序中,注意 iter 函数直接返回其递归调用,而没有对其进行运算。因此,这是一个尾部递归,这让直译器或编译器将本来是

的运行过程组合成在时间、空间上性能都较好的类型:

因为在中间过程中重复使用 iter 的函数帧,这种重组节省了空间。这也代表程序员不需要为了担心栈空间或是堆空间用完。在一般的实现中,尾部递归的型态也比其他型态更快速,不过仅仅是常量倍数的差异(非指数差异)。

很多使用函数语言的程序员会为了使用这个优化将递归的代码写成为尾部递归的形式。这通常需要一个多出来代表 “搜集器” 的形参(上述例子的 product 参数)。在一些语言中的一些函数的实现中(像是过滤一个列的实现等等),如果要使用尾部递归则需要将本来没有副作用的纯函数改写成会更动其他参引的形式。

实现

在Perl里,程序员可以直接用一种带有函数名称的 “goto” 叙述变体:goto &NAME; 直接使用尾调用。

在程序语言实现中,消除尾递归里的尾调用比消除一般的尾调用容易很多。举里来说,Java 虚拟机(JVM)的实现会消除尾递归里的尾调用(因为重新使用了原来的调用栈),但是不会消除一般的尾调用(因为改变了的调用栈)。因此,Scala等等使用 JVM 平台的函数语言可以有效的实现一个函数的尾递归,但是两个函数以上的尾递归就不行。

尾递归的实现方法有几个:

汇编重组

对于直接生成汇编的编译器,尾部调用消除很简单:只要校正栈上的形参之后把 “call” 的机器码换成一个 “jump” 的就行了。从编译器的观点,以下代码

先会被翻译成(这是合法的 x86 汇编):

然后,尾部调用消除指的是将最后两个指令以一个 “jump” 指令替换掉:

在 a 函数完成的时候,它会直接返回到 foo 的返回地址,省去了不必要的 ret 指令。

函数调用可能带有参数,因此生成的汇编必须确保被调用函数的函数帧在跳过去之前已设置好。举例来说,若是平台的调用栈除了返回位置以外还有函数参数,编译器需要输出调整调用栈的指令。在这类平台上,考虑代码:

其中 data1、data2 是参数。编译器会把这个代码翻译成以下汇编:

尾部调用优化会将代码变成:

更改后的代码不管在执行速度或是栈空间的使用上的性能都比较好。

透过弹跳床

然而,由于很多Scheme的编译器使用C作为中间目标语言,问题变成如何在 C 里在不让栈向上长的前提下实现尾部递归(假设 C 的编译器不优化尾部调用)。很多实现透过一种叫做弹跳床的装置,也就是一块不断进行函数调用的代码。所有进入函数的过程都透过这个弹跳床。当一个个函数需要尾部调用另一个函数时,它不是直接调用该函数,而是将该函数的位置、该调用使用的参数等等返回给弹跳床。这样就可以确保 C 的栈不会不会向上长而可以让循环继续运行。

用Groovy、Visual Basic .NET、C#等等支持高阶函数的语言实现弹跳床是可能的。


免责声明:以上内容版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。感谢每一位辛勤著写的作者,感谢每一位的分享。

——— 没有了 ———
编辑:阿族小谱
发表评论
写好了,提交
{{item.label}}
{{commentTotal}}条评论
{{item.userName}}
发布时间:{{item.time}}
{{item.content}}
回复
举报
点击加载更多
打赏作者
“感谢您的打赏,我会更努力的创作”
— 请选择您要打赏的金额 —
{{item.label}}
{{item.label}}
打赏成功!
“感谢您的打赏,我会更努力的创作”
返回

更多文章

更多精彩文章
打赏
私信

推荐阅读

· 调用栈
功能调用栈的主要功能是存放返回地址。除此之外,调用栈还用于存放:本地变量:子程序的变量可以存入调用栈,这样可以达到不同子程序间变量分离开的作用。参数传递:如果寄存器不足以容纳子程序的参数,可以在调用栈上存入参数。环境传递:有些语言(如Pascal与Ada)支持“多层子程序”,即子程序中可以利用主程序的本地变量。这些变量可以通过调用栈传入子程序。实例汇编语言以下MIPS汇编语言程序计算32+42{\displaystyle3^{2}+4^{2}},并将结果存至寄存器s0。main:li$a0,3li$a1,4jalsumsqmove$s0,$v0jmainendsumsq:addi$sp,$sp,-4#在堆疊上分配空間sw$ra,0($sp)#將sumsq的返回位址存入堆疊中jalsquaremove$t0,$v0move$a0,$a1jalsquareadd$v0,$v0,$t0lw$ra...
· 远程过程调用
历史起源有关RPC的想法至少可以追溯到1976年以“信使报”(Courier)的名义使用。RPC首次在UNIX平台上普及的执行工具程序是SUN公司的RPC(现在叫ONCRPC)。它被用作SUN的NFC的主要部件。ONCRPC今天仍在服务器上被广泛使用。另一个早期UNIX平台的工具是“阿波罗”计算机网络计算系统(NCS),它很快就用做OSF的分布计算环境(DCE)中的DCE/RPC的基础,并补充了DCOM。信息传递远程过程调用是一个分布式计算的客户端-服务器(Client/Server)的例子,它简单而又广受欢迎。远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。由于存在各式各样的变体和细节差异,对应地派生了各式远程过程调用协议,而且它们并不互相兼容。标准化的沟通机制为了允许不同的客户端均能访问服务器,许多标准化的RPC系统应运而生了。其...
· 开放网络运算远程过程调用
技术规格于1995年出版的RFC1831描述了ONCRPC的内容。出版于2009年的RFC5531是现行版本。至于ONCRPC的认证机制则在RFC2695,RFC2203,与RFC2623中描述。参考文献参见XDR
· 尾
功能动物的尾巴可以用于各种不同的功能,例如鱼类和其他的海洋生物能够利用尾巴来游动,而有些陆生动物则以尾巴保持平衡(例如猫)、或是抓牢身边的东西(例如猴)。鱼类的尾巴蝎子的尾巴狐猴的尾巴鸟类以尾巴控制方向与保持平衡尾巴也能够作为动物社会的信号,例如鹿会以尾巴来警告可能发生的危险、还有饲养犬会利用尾巴来表达情绪。例如摇尾表示友善,直竖表示攻击。在一些特殊的进化上的压力之下,会导致动物出现武装而具攻击性的尾巴,甚至含有毒液,例如蝎子。某些蜥蜴品种的尾巴能够在必要的情况下,永久地分离(脱落)躯体,像是为了逃脱某些肉食动物的控制、或是分散其注意力以得到逃脱的机会。通常这些蜥蜴的尾巴会在一段时间之后再长出来,然而新长出的尾巴颜色,会比原先的更暗。对于大部分的鸟类而言,尾巴是由羽毛延伸组成的,充当鸟类的方向舵以保持平衡、并控制本身飞行,同时也能在鸟类于高处歇息时提供平衡。孔雀的尾巴可以用来求偶。松鼠的尾...
· 鲂鱼赪尾
【成语】鲂鱼赪尾【成语】鲂鱼赪尾【拼音】fángyúchēngwěi【解释】后因以形容人困苦劳累,负担过重。【出处】《毛诗正义》卷一之三〈国风·周南·汝坟〉~43~遵彼汝坟,伐其条枚,未见君子,惄如调饥。遵彼汝坟,伐其条肄,既见君子,不我遐弃。鲂鱼赪尾,王室如毁,虽则如毁,父母孔迩。

关于我们

关注族谱网 微信公众号,每日及时查看相关推荐,订阅互动等。

APP下载

下载族谱APP 微信公众号,每日及时查看
扫一扫添加客服微信