在Scala或Lua等编程语言中,我们可以定义嵌套函数,例如
function factorial(n)
function _fac(n, acc)
if n == 0 then
return acc
else
return _fac(n-1, acc * n)
end
end
return _fac(n, 1)
end
这种方法是否会导致效率低下,因为每次调用外部函数时都会定义或创建嵌套函数实例?
答案 0 :(得分:23)
这种方法是否会导致效率低下,因为嵌套函数 每次调用外部时,都会定义或创建实例 功能
效率是一个广泛而广泛的话题。我假设效率低下"你的意思是"每次递归调用该方法都会产生开销"?
我只能代表Scala发言,特别是针对JVM的风格,因为其他风格的行为可能不同。
根据你的真实含义,我们可以将这个问题分成两部分。
Scala中的嵌套(本地范围)方法是一个词法范围功能,这意味着它们为您提供了外部方法值的可访问性,但是一旦我们发出字节码,它们就会在类级别定义,就像普通的java方法一样。
为了完整性,请确保Scala还具有函数值,它们是一等公民,这意味着您可以将它们传递给其他函数,然后这些会产生分配开销,因为它们是使用类实现的。
Factorial可以以尾递归的方式编写,就像你在你的例子中写的一样。 Scala编译器足够智能,它会注意到你的方法是尾递归并将其转换为迭代循环,避免了每次迭代的方法调用。如果可能的话,它也可能尝试内联factorial
方法,避免额外堆栈帧分配的开销。
例如,请考虑Scala中的以下因子实现:
def factorial(num: Int): Int = {
@tailrec
def fact(num: Int, acc: Int): Int = num match {
case 0 => acc
case n => fact(n - 1, acc * n)
}
fact(num, 1)
}
从表面上看,我们有一个递归方法。让我们看一下JVM字节码的样子:
private final int fact$1(int, int);
Code:
0: iload_1
1: istore 4
3: iload 4
5: tableswitch { // 0 to 0
0: 24
default: 28
}
24: iload_2
25: goto 41
28: iload 4
30: iconst_1
31: isub
32: iload_2
33: iload 4
35: imul
36: istore_2
37: istore_1
38: goto 0
41: ireturn
我们在这里看到的是递归变成了一个迭代循环(tableswitch +一个跳转指令)。
关于方法实例的创建,如果我们的方法不是尾递归的,那么JVM运行时需要为每次调用解释它,直到C2 compiler找到它足以使JIT编译它并重新使用之后每个方法调用的机器代码。
一般来说,我会说这不应该让你担心,除非你已经注意到这个方法正在执行你的热门路径并且分析代码导致你提出这个问题。
总而言之,效率是一个非常微妙的用例特定主题。我认为,根据您提供的简化示例,我们没有足够的信息告诉您,如果这是您选择用例的最佳选择。我再说一遍,如果这不是你的探查器上出现的东西,不要担心这个。
答案 1 :(得分:9)
让我们在Lua中使用/不使用嵌套函数对其进行基准测试。
Variant 1(每次调用都会创建内部函数对象)
local function factorial1(n)
local function _fac1(n, acc)
if n == 0 then
return acc
else
return _fac1(n-1, acc * n)
end
end
return _fac1(n, 1)
end
变体2(函数未嵌套)
local function _fac2(n, acc)
if n == 0 then
return acc
else
return _fac2(n-1, acc * n)
end
end
local function factorial2(n)
return _fac2(n, 1)
end
基准测试代码(计算12!
10毫升,并以秒为单位显示已用CPU时间):
local N = 1e7
local start_time = os.clock()
for j = 1, N do
factorial1(12)
end
print("CPU time of factorial1 = ", os.clock() - start_time)
local start_time = os.clock()
for j = 1, N do
factorial2(12)
end
print("CPU time of factorial2 = ", os.clock() - start_time)
Lua 5.3(口译员)的输出
CPU time of factorial1 = 8.237
CPU time of factorial2 = 6.074
LuaJIT(JIT编译器)的输出
CPU time of factorial1 = 1.493
CPU time of factorial2 = 0.141
答案 2 :(得分:8)
答案取决于当然的语言。
Scala中特别发生的事情是内部函数被编译为它们位于函数范围之外的函数。
通过这种方式,语言只允许您从定义它们的词法范围中调用它们,但实际上并没有多次实例化该函数。
我们可以通过编译
的两个变体来轻松测试第一个是Lua代码的相当忠实的端口:
class Function1 {
def factorial(n: Int): Int = {
def _fac(n: Int, acc: Int): Int =
if (n == 0)
acc
else
_fac(n-1, acc * n)
_fac(n, 1)
}
}
第二个或多或少相同,但尾递归函数是在factorial
范围之外定义的:
class Function2 {
def factorial(n: Int): Int = _fac(n, 1)
private final def _fac(n: Int, acc: Int): Int =
if (n == 0)
acc
else
_fac(n-1, acc * n)
}
我们现在可以使用scalac
编译这两个类,然后使用javap
来查看编译器输出:
javap -p Function*.scala
将产生以下输出
Compiled from "Function1.scala"
public class Function1 {
public int factorial(int);
private final int _fac$1(int, int);
public Function1();
}
Compiled from "Function2.scala"
public class Function2 {
public int factorial(int);
private final int _fac(int, int);
public Function2();
}
我添加了private final
关键字以最小化两者之间的差异,但需要注意的是,在两种情况下,定义都出现在类级别,内部函数自动定义为private
和final
以及一个小的装饰以确保没有名称类(例如,如果您在两个不同的内部定义loop
内部函数)。
不确定Lua或其他语言,但我可以预期至少大多数编译语言都采用类似的方法。
答案 3 :(得分:4)
是(或以前),Lua在执行多次执行函数定义时重复使用函数值的努力证明了这一点。
功能值之间的平等已经改变。现在,一个功能 定义可能不会创造新的价值;它可以重复使用以前的一些 如果新函数没有可观察到的差异,则为值。
由于您已编码(假设Lua)分配给在更高范围内声明的全局或本地的函数,您可以自己编写短路代码(假设没有其他代码将其设置为除nil
以外的任何其他内容或false
):
function factorial(n)
_fac = _fac or function (n, acc)
…
end
…
end
答案 4 :(得分:1)
我不知道lua,但在Scala中非常常见并且在递归函数中使用以确保尾部安全优化:
def factorial(i: Int): Int = {
@tailrec
def fact(i: Int, accumulator: Int): Int = {
if (i <= 1)
accumulator
else
fact(i - 1, i * accumulator)
}
fact(i, 1)
}
有关tail-safe和recursion here
的更多信息