Recursion 递归调用签名不断变化

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(

我将要实现一个使用递归的程序。所以,在我开始获取堆栈溢出异常之前,我想最好实现一个蹦床,并在需要时使用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(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
元素。