Recursion 递归调用签名不断变化
我将要实现一个使用递归的程序。所以,在我开始获取堆栈溢出异常之前,我想最好实现一个蹦床,并在需要时使用thunks 我做的第一次尝试是使用阶乘。代码如下:Recursion 递归调用签名不断变化,recursion,julia,Recursion,Julia,我将要实现一个使用递归的程序。所以,在我开始获取堆栈溢出异常之前,我想最好实现一个蹦床,并在需要时使用thunks 我做的第一次尝试是使用阶乘。代码如下: callable(f) = !isempty(methods(f)) function trampoline(f, arg1, arg2) v = f(arg1, arg2) while callable(v) v = v() end return v end function factorial(
callable(f) = !isempty(methods(f))
function trampoline(f, arg1, arg2)
v = f(arg1, arg2)
while callable(v)
v = v()
end
return v
end
function factorial(n, continuation)
if n == 1
continuation(1)
else
(() -> factorial(n-1, (z -> (() -> continuation(n*z)))))
end
end
function cont(x)
x
end
此外,我还实现了一个简单的阶乘,以检查事实上是否可以防止堆栈溢出:
function factorial_overflow(n)
if n == 1
1
else
n*factorial_overflow(n-1)
end
end
结果是:
julia> factorial_overflow(140000)
ERROR: StackOverflowError:
#JITing with a small input
julia> trampoline(factorial, 10, cont)
3628800
#Testing
julia> trampoline(factorial, 140000, cont)
0
所以,是的,我在避免StacksOverflows。是的,我知道结果毫无意义,因为我得到的是整数溢出,但这里我只关心堆栈。当然,制作版会修复这个问题
(另外,我知道对于阶乘的情况,有一个内置的,我不会使用其中任何一个,我做它们是为了测试我的蹦床)
蹦床版在第一次跑步时会花费很多时间,然后它会变得很快。。。当计算相同或更低的值时。
如果我做了trampoline(阶乘,150000,cont)
我将有一些编译时间
在我看来(有根据的猜测),我正在为阶乘jit许多不同的签名:每生成一个thunk就有一个
我的问题是:我能避免这种情况吗?我认为问题在于每个闭包都有自己的类型,它专门针对捕获的变量。为了避免这种专门化,可以使用未完全专门化的函子:
struct L1
f
n::Int
z::Int
end
(o::L1)() = o.f(o.n*o.z)
struct L2
f
n::Int
end
(o::L2)(z) = L1(o.f, o.n, z)
struct Factorial
f
c
n::Int
end
(o::Factorial)() = o.f(o.n-1, L2(o.c, o.n))
callable(f) = false
callable(f::Union{Factorial, L1, L2}) = true
function myfactorial(n, continuation)
if n == 1
continuation(1)
else
Factorial(myfactorial, continuation, n)
end
end
function cont(x)
x
end
function trampoline(f, arg1, arg2)
v = f(arg1, arg2)
while callable(v)
v = v()
end
return v
end
请注意,函数字段是非类型化的。现在,函数在第一次运行时运行得更快:
julia> @time trampoline(myfactorial, 10, cont)
0.020673 seconds (4.24 k allocations: 264.427 KiB)
3628800
julia> @time trampoline(myfactorial, 10, cont)
0.000009 seconds (37 allocations: 1.094 KiB)
3628800
julia> @time trampoline(myfactorial, 14000, cont)
0.001277 seconds (55.55 k allocations: 1.489 MiB)
0
julia> @time trampoline(myfactorial, 14000, cont)
0.001197 seconds (55.55 k allocations: 1.489 MiB)
0
我刚刚将代码中的每个闭包翻译成了相应的函子。这可能不是必需的,而且可能有更好的解决方案,但它是有效的,并有望证明这种方法
编辑:
为了更清楚地说明经济放缓的原因,可以使用:
function factorial(n, continuation)
if n == 1
continuation(1)
else
tmp = (z -> (() -> continuation(n*z)))
@show typeof(tmp)
(() -> factorial(n-1, tmp))
end
end
这将产生:
julia> trampoline(factorial, 10, cont)
typeof(tmp) = ##31#34{Int64,#cont}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,#cont}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}}}}}
typeof(tmp) = ##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,##31#34{Int64,#cont}}}}}}}}}
3628800
tmp
是一个闭包。它自动创建的类型与
struct Tmp{T,F}
n::T
continuation::F
end
continuation
字段的F
类型的专门化是编译时间长的原因
通过使用
L2
,而不是在相应字段f
上专门化,factorial
的continuation
参数始终为L2
类型,从而避免了问题。您是否尝试将v
更改为匿名函数?我认为这是一个泛型函数的事实会在这里引起一些问题。@ChrisRackauckas非常感谢您的评论。我不确定我在跟踪你;如果它是一个匿名函数,我怎么能循环使用v
!非常感谢。但是,我不得不使用type
而不是struct
。继续看这个,但这解决了一个大瓶颈。谢谢请注意,您现在非常接近于实现具有start/next/done的迭代器。@CristiánAntuña抱歉,我使用的是Julia 0.6的夜间版本。在那里,struct
与immutable
相同。在这种情况下,类型
(0.6上的可变结构
)也可以@马特布。什么意思?我的代码应该与原始代码相同,只是手动实现了闭包,而不是自动匿名闭包。这并不奇怪——当然递归可以用迭代来表示。该循环的结构非常类似于迭代器协议,现在您有了一个表示阶乘的自定义对象,您可以类似地使用start/next/done方法,然后从迭代器中获取last
元素。