在开始学习Lisp的同时,我遇到了术语“ 尾递归”。到底是什么意思?
在开始学习Lisp的同时,我遇到了术语“ 尾递归”。到底是什么意思?
Answers:
考虑一个简单的函数,该函数将前N个自然数相加。(例如sum(5) = 1 + 2 + 3 + 4 + 5 = 15
)。
这是一个使用递归的简单JavaScript实现:
function recsum(x) {
if (x === 1) {
return x;
} else {
return x + recsum(x - 1);
}
}
如果您调用recsum(5)
,这是JavaScript解释器将评估的内容:
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)))
15
请注意,在JavaScript解释器开始实际执行计算总和之前,必须完成每个递归调用。
这是该函数的尾递归版本:
function tailrecsum(x, running_total = 0) {
if (x === 0) {
return running_total;
} else {
return tailrecsum(x - 1, running_total + x);
}
}
这是如果您调用会发生的事件序列tailrecsum(5)
(tailrecsum(5, 0)
由于默认的第二个参数,实际上是)。
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
在尾部递归的情况下,每次对递归调用进行评估时,running_total
都会更新。
注意:原始答案使用了Python中的示例。这些已更改为JavaScript,因为Python解释器不支持尾部调用优化。但是,尽管尾部调用优化是ECMAScript 2015规范的一部分,但大多数JavaScript解释器都不支持它。
tail recursion
用一种不会优化掉尾声的语言来实现。
在传统的递归中,典型的模型是先执行递归调用,然后取递归调用的返回值并计算结果。以这种方式,直到您从每个递归调用中返回后,您才能获得计算结果。
在tail递归中,首先执行计算,然后执行递归调用,将当前步骤的结果传递到下一个递归步骤。这导致最后一个语句的形式为(return (recursive-function params))
。基本上,任何给定递归步骤的返回值都与下一个递归调用的返回值相同。
这样的结果是,一旦您准备好执行下一个递归步骤,就不再需要当前的堆栈框架。这样可以进行一些优化。实际上,使用适当编写的编译器,您永远都不应通过尾部递归调用来实现堆栈溢出窃笑。只需将当前堆栈框架重用于下一步递归步骤。我很确定Lisp会这样做。
重要的一点是,尾递归实质上等同于循环。这不仅是编译器优化的问题,还是表达能力的基本事实。这是双向的:您可以采用任何形式的循环
while(E) { S }; return Q
where E
和Q
是表达式,S
是语句序列,并将其转换为尾递归函数
f() = if E then { S; return f() } else { return Q }
当然E
,S
和Q
必须定义来计算对一些变量的一些有趣的价值。例如,循环功能
sum(n) {
int i = 1, k = 0;
while( i <= n ) {
k += i;
++i;
}
return k;
}
等效于尾递归函数
sum_aux(n,i,k) {
if( i <= n ) {
return sum_aux(n,i+1,k+i);
} else {
return k;
}
}
sum(n) {
return sum_aux(n,1,0);
}
(这种尾部递归函数与较少参数的函数的“包装”是常见的函数习语。)
else { return k; }
可以更改为return k;
摘自《Lua编程》一书的摘录显示了如何进行适当的尾递归(在Lua中,但也应适用于Lisp)以及为什么更好。
甲尾调用 [尾递归]是一种跳转的穿着作为一个呼叫。当一个函数调用另一个函数作为其最后一个动作时,就会发生尾部调用,因此它无事可做。例如,在以下代码中,对的调用
g
是尾部调用:function f (x) return g(x) end
后
f
来电g
,它有没有别的事情可做。在这种情况下,当被调用函数结束时,程序无需返回到调用函数。因此,在进行尾部调用之后,程序无需在堆栈中保留有关调用函数的任何信息。...因为适当的尾调用不占用堆栈空间,所以程序可以进行的“嵌套”尾调用的数量没有限制。例如,我们可以使用任何数字作为参数调用以下函数;它永远不会溢出堆栈:
function foo (n) if n > 0 then return foo(n - 1) end end
就像我之前说的,尾部呼叫是goto的一种。这样,在Lua中正确尾调用的一个非常有用的应用是对状态机进行编程。这样的应用程序可以通过函数表示每个状态。更改状态是转到(或调用)特定功能。例如,让我们考虑一个简单的迷宫游戏。迷宫有几个房间,每个房间最多有四个门:北,南,东和西。在每个步骤,用户输入移动方向。如果在该方向上有一扇门,则用户转到相应的房间;否则,程序将打印警告。目标是从最初的房间转到最后的房间。
该游戏是典型的状态机,当前房间是状态。我们可以为每个房间使用一个功能来实现这样的迷宫。我们使用尾部呼叫从一个房间移动到另一个房间。一个带有四个房间的小迷宫看起来像这样:
function room1 () local move = io.read() if move == "south" then return room3() elseif move == "east" then return room2() else print("invalid move") return room1() -- stay in the same room end end function room2 () local move = io.read() if move == "south" then return room4() elseif move == "west" then return room1() else print("invalid move") return room2() end end function room3 () local move = io.read() if move == "north" then return room1() elseif move == "east" then return room4() else print("invalid move") return room3() end end function room4 () print("congratulations!") end
因此,当您进行如下递归调用时,您会看到:
function x(n)
if n==0 then return 0
n= n-2
return x(n) + 1
end
这不是尾部递归,因为在进行递归调用后,您仍然需要在该函数中做一些事情(加1)。如果输入很高的数字,可能会导致堆栈溢出。
这里有一个例子,而不是用语言解释。这是阶乘函数的Scheme版本:
(define (factorial x)
(if (= x 0) 1
(* x (factorial (- x 1)))))
这是尾递归的阶乘版本:
(define factorial
(letrec ((fact (lambda (x accum)
(if (= x 0) accum
(fact (- x 1) (* accum x))))))
(lambda (x)
(fact x 1))))
在第一个版本中,您会注意到对事实的递归调用被输入到乘法表达式中,因此在进行递归调用时必须将状态保存在堆栈中。在尾递归版本中,没有其他S表达式在等待递归调用的值,并且由于没有其他工作要做,因此状态不必保存在堆栈中。通常,Scheme尾递归函数使用恒定的堆栈空间。
list-reverse
程序将在恒定的堆栈空间中运行,但将在堆上创建并增长数据结构。遍历树可以在附加参数中使用模拟堆栈。等等
尾递归是指递归调用在递归算法中的最后逻辑指令中位于最后。
通常,在递归中,您有一个基本情况,即停止递归调用并开始弹出调用堆栈。使用经典示例,尽管C函数比Lisp多,但阶乘函数说明了尾递归。在检查基本情况后,将进行递归调用。
factorial(x, fac=1) {
if (x == 1)
return fac;
else
return factorial(x-1, x*fac);
}
对阶乘的初始调用将是factorial(n)
其中fac=1
(默认值),而n是要为其计算阶乘的数字。
else
是您可能会称为“基本案例”的步骤,但跨几行。我是在误解您还是我的假设正确?尾递归只对一个班轮有好处吗?
factorial
示例仅是经典的简单示例,仅此而已。
这是比较两个功能的快速代码段。第一种是用于查找给定数字阶乘的传统递归。第二种使用尾递归。
非常简单直观。
判断递归函数是否为尾递归的一种简单方法是在基本情况下是否返回具体值。这意味着它不会返回1或true或类似的东西。它很可能返回方法参数之一的某些变体。
另一种方法是判断递归调用是否没有任何加法,算术,修改等。这意味着它只是一个纯递归调用。
public static int factorial(int mynumber) {
if (mynumber == 1) {
return 1;
} else {
return mynumber * factorial(--mynumber);
}
}
public static int tail_factorial(int mynumber, int sofar) {
if (mynumber == 1) {
return sofar;
} else {
return tail_factorial(--mynumber, sofar * mynumber);
}
}
我最好的理解方法tail call recursion
是递归的特殊情况,其中最后一个调用(或尾调用)是函数本身。
比较Python中提供的示例:
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
^回归
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
^尾随
如您在一般的递归版本中所见,代码块中的最终调用是x + recsum(x - 1)
。因此,在调用该recsum
方法之后,还有另一个操作x + ..
。
但是,在尾部递归版本中,代码块中的最终调用(或尾部调用)为 tailrecsum(x - 1, running_total + x)
指对方法本身进行的最后一次调用,此后不进行任何操作。
这一点很重要,因为如此处所示的尾部递归并不能使内存增加,因为当基础VM看到某个函数在尾部位置(函数中要评估的最后一个表达式)调用自身时,它将消除当前的堆栈帧,从而被称为尾叫优化(TCO)。
注意 请记住,上面的示例是用Python编写的,其运行时不支持TCO。这只是一个解释这一点的例子。TCO支持Scheme,Haskell等语言
在Java中,这是Fibonacci函数的可能的尾部递归实现:
public int tailRecursive(final int n) {
if (n <= 2)
return 1;
return tailRecursiveAux(n, 1, 1);
}
private int tailRecursiveAux(int n, int iter, int acc) {
if (iter == n)
return acc;
return tailRecursiveAux(n, ++iter, acc + iter);
}
将此与标准递归实现进行比较:
public int recursive(final int n) {
if (n <= 2)
return 1;
return recursive(n - 1) + recursive(n - 2);
}
iter
到acc
时候iter < (n-1)
。
简而言之,尾部递归将递归调用作为函数中的最后一条语句,因此它不必等待递归调用。
因此,这是一个尾递归,即N(x-1,p * x)是函数中的最后一条语句,编译器巧妙地指出它可以优化为for循环(阶乘)。第二参数p带有中间乘积值。
function N(x, p) {
return x == 1 ? p : N(x - 1, p * x);
}
这是编写上述阶乘函数的非尾递归方式(尽管某些C ++编译器仍然能够对其进行优化)。
function N(x) {
return x == 1 ? 1 : x * N(x - 1);
}
但这不是:
function F(x) {
if (x == 1) return 0;
if (x == 2) return 1;
return F(x - 1) + F(x - 2);
}
我确实写了一篇长文章,标题为“ 了解尾递归– Visual Studio C ++ –程序集视图 ”
这摘自有关尾递归的计算机程序的结构和解释。
与迭代和递归相反,我们必须注意不要将递归过程的概念与递归过程的概念相混淆。当我们将过程描述为递归时,我们指的是过程定义(直接或间接)引用过程本身的语法事实。但是,当我们按照线性递归的模式描述流程时,我们是在谈论流程的发展方式,而不是过程的语法。我们将诸如事实事实之类的递归过程称为生成迭代过程似乎令人不安。但是,该过程实际上是迭代的:其状态完全由其三个状态变量捕获,并且解释器只需跟踪三个变量即可执行该过程。
流程和过程之间的区别可能造成混淆的一个原因是,大多数通用语言的实现(包括Ada,Pascal和C)的设计方式都使得对任何递归过程的解释都消耗了一定数量的内存,而这种内存随即使原则上描述的过程是迭代的,过程调用的数量也是如此。结果,这些语言只能通过诉诸特殊的“循环结构”来描述迭代过程,例如“做”,“重复”,“直到”,“为时”和“为时”。Scheme的实施不存在此缺陷。即使迭代过程是通过递归过程描述的,它也会在恒定空间中执行迭代过程。具有此属性的实现称为尾递归。 通过尾递归实现,可以使用常规过程调用机制来表示迭代,因此特殊的迭代构造仅用作语法糖。
递归函数是一个自行调用的函数
它允许程序员使用最少的代码编写高效的程序。
缺点是,如果编写不正确,它们可能会导致无限循环和其他意外结果。
我将解释简单递归函数和尾递归函数
为了编写一个简单的递归函数
从给定的示例:
public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
从上面的例子
if(n <=1)
return 1;
是何时退出循环的决定因素
else
return n * fact(n-1);
是否要进行实际处理
为了便于理解,让我一步一步地完成任务。
让我们看看如果我跑步会在内部发生什么 fact(4)
public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
If
循环失败,因此进入else
循环,因此返回4 * fact(3)
在堆栈内存中,我们有 4 * fact(3)
代入n = 3
public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
If
循环失败,因此进入else
循环
所以它返回 3 * fact(2)
记住我们称呼为``4 * fact(3)``
输出为 fact(3) = 3 * fact(2)
到目前为止,堆栈已经 4 * fact(3) = 4 * 3 * fact(2)
在堆栈内存中,我们有 4 * 3 * fact(2)
代入n = 2
public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
If
循环失败,因此进入else
循环
所以它返回 2 * fact(1)
记得我们打过电话 4 * 3 * fact(2)
输出为 fact(2) = 2 * fact(1)
到目前为止,堆栈已经 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
在堆栈内存中,我们有 4 * 3 * 2 * fact(1)
代入n = 1
public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
If
循环为真
所以它返回 1
记得我们打过电话 4 * 3 * 2 * fact(1)
输出为 fact(1) = 1
到目前为止,堆栈已经 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
最后,fact(4)的结果= 4 * 3 * 2 * 1 = 24
在尾递归会
public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
If
循环失败,因此进入else
循环,因此返回fact(3, 4)
在堆栈内存中,我们有 fact(3, 4)
代入n = 3
public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
If
循环失败,因此进入else
循环
所以它返回 fact(2, 12)
在堆栈内存中,我们有 fact(2, 12)
代入n = 2
public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
If
循环失败,因此进入else
循环
所以它返回 fact(1, 24)
在堆栈内存中,我们有 fact(1, 24)
代入n = 1
public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
If
循环为真
所以它返回 running_total
输出为 running_total = 24
最后,fact(4,1)= 24的结果
尾递归是您当前的生活。您无休止地循环使用同一堆栈框架,因为没有理由或方法返回到“上一个”框架。过去已经结束,可以将其丢弃。您得到一帧,直到永远消失在未来,直到永远消失。
当您认为某些进程可能会利用其他帧,但如果堆栈没有无限增长时,仍被认为是尾递归的,这种类比就失败了。
尾递归是一种递归函数,其中该函数在函数的末尾(“尾部”)调用自身,其中在递归调用返回后不进行任何计算。许多编译器进行了优化,以将递归调用更改为尾递归或迭代调用。
考虑计算阶乘的问题。
一种简单的方法是:
factorial(n):
if n==0 then 1
else n*factorial(n-1)
假设您调用阶乘(4)。递归树将是:
factorial(4)
/ \
4 factorial(3)
/ \
3 factorial(2)
/ \
2 factorial(1)
/ \
1 factorial(0)
\
1
在上述情况下,最大递归深度为O(n)。
但是,请考虑以下示例:
factAux(m,n):
if n==0 then m;
else factAux(m*n,n-1);
factTail(n):
return factAux(1,n);
factTail(4)的递归树为:
factTail(4)
|
factAux(1,4)
|
factAux(4,3)
|
factAux(12,2)
|
factAux(24,1)
|
factAux(24,0)
|
24
同样,最大递归深度为O(n),但没有一个调用会向堆栈中添加任何额外的变量。因此,编译器可以消除堆栈。
一尾递归函数是一个递归函数,其中最后一个动作恢复之前它使递归函数调用。即,立即返回递归函数调用的返回值。例如,您的代码如下所示:
def recursiveFunction(some_params):
# some code here
return recursiveFunction(some_args)
# no code after the return statement
实现尾部调用优化或尾部调用消除的编译器和解释器可以优化递归代码以防止堆栈溢出。如果您的编译器或解释器未实现尾部调用优化(例如CPython解释器),则以这种方式编写代码没有任何其他好处。
例如,这是Python中的标准递归阶乘函数:
def factorial(number):
if number == 1:
# BASE CASE
return 1
else:
# RECURSIVE CASE
# Note that `number *` happens *after* the recursive call.
# This means that this is *not* tail call recursion.
return number * factorial(number - 1)
这是阶乘函数的尾调用递归版本:
def factorial(number, accumulator=1):
if number == 0:
# BASE CASE
return accumulator
else:
# RECURSIVE CASE
# There's no code after the recursive call.
# This is tail call recursion:
return factorial(number - 1, number * accumulator)
print(factorial(5))
(请注意,即使这是Python代码,CPython解释器也不会进行尾调用优化,因此按这种方式排列代码不会给运行时带来任何好处。)
如阶乘示例所示,您可能必须使代码更加不可读才能利用尾部调用优化。(例如,基本情况现在有点不直观,并且该accumulator
参数有效地用作一种全局变量。)
但是尾部调用优化的好处是它可以防止堆栈溢出错误。(我会注意到,通过使用迭代算法而不是递归算法,您可以获得相同的好处。)
当调用堆栈中放入过多帧对象时,会导致堆栈溢出。调用函数时,将框架对象压入调用堆栈,并在函数返回时从调用堆栈弹出。框架对象包含信息,例如局部变量和函数返回时返回的代码行。
如果您的递归函数进行过多的递归调用而没有返回,则调用堆栈可能超出其框架对象限制。(该数目因平台而异;在Python中,默认为1000个框架对象。)这会导致堆栈溢出错误。(嘿,这就是这个网站的名称!)
但是,如果递归函数做的最后一件事是进行递归调用并返回其返回值,则没有理由需要将当前帧对象保留在调用堆栈中。毕竟,如果在递归函数调用之后没有任何代码,则没有理由继续使用当前框架对象的局部变量。因此,我们可以立即摆脱当前框架对象,而不必将其保留在调用堆栈中。这样的最终结果是您的调用堆栈不会增加大小,因此不会导致堆栈溢出。
编译器或解释器必须具有尾调用优化功能,才能识别何时可以应用尾调用优化。即使这样,您也可能已经在递归函数中重新安排了代码以利用尾部调用优化,如果这种潜在的可读性下降值得优化,则取决于您。
为了了解尾调用递归和非尾调用递归之间的一些核心区别,我们可以探索这些技术的.NET实现。
这是一篇在C#,F#和C ++ \ CLI中带有一些示例的文章:在C#,F#和C ++ \ CLI 中进行尾递归的历险。
C#不会为尾调用递归进行优化,而F#会为。
原理上的差异涉及循环与Lambda微积分。C#在设计时考虑了循环,而F#是根据Lambda微积分的原理构建的。有关Lambda演算原理的非常好(免费)的书,请参阅Abelson,Sussman和Sussman撰写的《计算机程序的结构和解释》。
关于F#中的尾部调用,有关非常好的介绍性文章,请参见F#中的尾部调用详细介绍。最后,这是一篇文章,介绍了非尾递归和尾调用递归之间的区别(在F#中):F sharp中的尾递归与非尾递归。
如果您想了解C#和F#之间的尾调用递归的一些设计差异,请参阅在C#和F#中生成尾调用操作码。
如果您足够关心要知道什么条件阻止C#编译器执行尾部调用优化,请参阅本文:JIT CLR尾部调用条件。
递归有两种基本类型:头递归和尾递归。
在head递归中,函数进行递归调用,然后执行更多计算,例如,可能使用递归调用的结果。
在尾部递归函数中,所有计算都首先发生,而递归调用是最后发生的事情。
摘自这个超级棒的帖子。请考虑阅读。
递归意味着一个调用自身的函数。例如:
(define (un-ended name)
(un-ended 'me)
(print "How can I get here?"))
Tail-Recursion表示结束函数的递归:
(define (un-ended name)
(print "hello")
(un-ended 'me))
可以看到,无止境的函数(过程,用Scheme术语)的最后一件事就是调用自身。另一个(更有用的)示例是:
(define (map lst op)
(define (helper done left)
(if (nil? left)
done
(helper (cons (op (car left))
done)
(cdr left))))
(reverse (helper '() lst)))
在帮助程序过程中,如果左边不为零,它做的最后一件事就是调用自身(AFTER CONS某物和CDR某物)。基本上,这就是映射列表的方式。
尾递归具有很大的优势,即解释器(或编译器,取决于语言和供应商)可以对其进行优化,并将其转换为等同于while循环的内容。实际上,在Scheme传统中,大多数“ for”和“ while”循环都是通过尾递归方式完成的(据我所知,没有for和while)。
这个问题有很多很好的答案...但是我不禁要提出关于如何定义“尾部递归”或至少“适当的尾部递归”的替代方法。即:是否应该将其视为程序中特定表达式的属性?还是应该将其视为编程语言实现的一种特性?
关于后一种观点的更多信息,有一篇经典论文,威尔·克林格(Will Clinger)了,“正确的尾递归和空间效率”(PLDI 1998),其中将“适当的尾递归”定义为编程语言实现的一个属性。定义的构造允许忽略执行细节(例如,是通过运行时堆栈还是通过堆的帧分配的链接列表实际表示调用堆栈)。
为了实现这一点,它使用渐进分析:不是通常所见的程序执行时间,而是程序空间使用率。这样,堆分配的链表与运行时调用栈的空间使用最终在渐近上等效。因此,人们会忽略该编程语言的实现细节(在实践中当然很重要,但是当人们试图确定给定的实现是否满足“属性尾递归”的要求时,这一细节可能会有些混乱。 )
出于以下几个原因,值得对本文进行认真研究:
它给出了程序的尾部表达式和尾部调用的归纳定义。(这样的定义以及为什么这样的调用很重要,似乎是这里给出的大多数其他答案的主题。)
这些定义只是为了提供一种文字风味:
定义1用Core Scheme编写的程序的尾部表达式归纳定义如下。
- Lambda表达式的主体是尾部表达式
- 如果
(if E0 E1 E2)
是尾表达式,则E1
和E2
均为尾表达式。- 没什么是尾巴表达。
定义2甲尾呼叫是尾表达式,它是一个过程调用。
(尾部递归调用,或如论文所述,“自尾调用”是尾部调用的特例,其中过程本身被调用。)
它提供了一个评估核心方案,其中每一台机器都有相同的观察行为的六种不同的“机器”正式定义,除了为渐近空间复杂度类,每一个在不在。
例如,在分别给出具有1.基于堆栈的内存管理,2。垃圾收集但不进行尾调用,3。垃圾收集和尾调用的计算机的定义之后,本文将继续采用更高级的存储管理策略,例如4.“evlis尾递归”,在这里不需要环境中的尾调用跨越最后一个子表达式参数的评价保存,5减少封闭的环境,只是那个封闭的自由变量,和6. Appel和Shao定义的所谓“空间安全”语义。
为了证明这些机器实际上属于六个不同的空间复杂度类别,本文针对要比较的每一对机器,提供了具体的程序示例,这些程序将在一台机器上而不是另一台机器上显示渐近空间爆炸。
(现在读完我的答案,我不确定我是否能够真正掌握克林格论文的要点。但是,las,我现在不能花更多的时间来制定这个答案。)
许多人已经在这里解释了递归。我想引用一些关于递归从Riccardo Terrell的书“ .NET中的并发,并发和并行编程的现代模式”中获得的一些优点的想法:
“功能递归是在FP中进行迭代的自然方法,因为它避免了状态突变。在每次迭代期间,都会将新值传递到循环构造函数中,而不是进行更新(变异)。另外,可以组成一个递归函数,使您的程序更具模块化,并为利用并行化提供了机会。”
这也是同一本书中有关尾递归的一些有趣的注释:
尾调用递归是一种将常规递归函数转换为可处理大量输入而没有任何风险和副作用的优化版本的技术。
注意进行优化的主要原因是为了提高数据局部性,内存使用率和缓存使用率。通过进行尾部调用,被调用方使用与调用方相同的堆栈空间。这样可以减少内存压力。由于可以将相同的内存重新用于后续的调用者,并且可以保留在缓存中,而不是逐出较旧的缓存行为新的缓存行腾出空间,因此可以从某种程度上改善缓存。