简体   繁体   English

Mathematica中的尾调用优化?

[英]Tail call optimization in Mathematica?

While formulating an answer to another SO question , I came across some strange behaviour regarding tail recursion in Mathematica. 在制定另一个SO问题答案时 ,我在Mathematica中遇到了一些关于尾递归的奇怪行为。

The Mathematica documentation hints that tail call optimization might be performed. Mathematica文档暗示可能会执行尾调用优化 But my own experiments give conflicting results. 但我自己的实验给出了相互矛盾的结果。 Contrast, for example, the following two expressions. 对比,例如,以下两个表达式。 The first crashes the 7.0.1 kernel, presumably due to stack exhaustion: 第一个崩溃7.0.1内核,可能是由于堆栈耗尽:

(* warning: crashes the kernel! *)
Module[{f, n = 0},
  f[x_] := (n += 1; f[x + 1]);
  TimeConstrained[Block[{$RecursionLimit = Infinity}, f[0]], 300, n]
]

The second runs to completion, appearing to exploit tail call optimization to return a meaningful result: 第二个运行完成,似乎利用尾调用优化来返回有意义的结果:

Module[{f, n = 0},
  f[x_] := Null /; (n += 1; False);
  f[x_] := f[x + 1];
  TimeConstrained[Block[{$IterationLimit = Infinity}, f[0]], 300, n]
]

Both expressions define a tail recursive function f . 两个表达式都定义了尾递归函数f In the case of the first function, Mathematica apparently regards the presence of a compound statement enough to defeat any chance of tail call optimization. 在第一个函数的情况下,Mathematica显然认为复合语句的存在足以击败尾调用优化的任何机会。 Also note that the first expression is governed by $RecursionLimit and the second by $IterationLimit -- a sign that Mathematica is treating the two expressions differently. 另请注意,第一个表达式由$RecursionLimit ,第二个表达式由$IterationLimit - 这是Mathematica以不同方式处理这两个表达式的标志。 (Note: the SO answer referenced above has a less contrived function that successfully exploits tail call optimization). (注意:上面引用的SO答案有一个较少设法的功能,成功利用尾部调用优化)。

So, the question is : does anyone know the circumstances under which Mathematica performs tail-call optimization of recursive functions? 所以,问题是 :有没有人知道Mathematica对递归函数进行尾调用优化的情况? A reference to a definitive statement in the Mathematica documentation or other WRI material would be ideal. 在Mathematica文档或其他WRI材料中提及最终陈述将是理想的。 Speculation is also welcome. 投机也很受欢迎。

I can summarize the conclusions I was led to by my personal experience, with a disclaimer that what follows may not be the entirely right explanation. 我可以总结一下我个人经历所带来的结论,并且免责声明后面的内容可能不是完全正确的解释。 The anwer seems to lie in the differences between Mathematica call stack and traditional call stacks, which originates from Mathematica pattern-defined functions being really rules. anwer似乎存在于Mathematica调用堆栈和传统调用堆栈之间的差异,传统调用堆栈源于Mathematica模式定义的函数,实际上是规则。 So, there are no real function calls. 所以,没有真正的函数调用。 Mathematica needs a stack for a different reason: since normal evaluation happens from the bottom of an expression tree, it must keep intermediate expressions in case when deeper and deeper parts of (sub)expressions get replaced as a result of rule application (some parts of an expression grow from the bottom). Mathematica需要一个堆栈的原因不同:因为正常的评估是从表达式树的底部发生的,所以它必须保留中间表达式,以防(sub)表达式的更深和更深的部分因规则应用而被替换(某些部分)表达从底部增长)。 This is the case, in particular, for rules defining what we'd call non tail-recursive functions in other languages. 特别是对于定义我们在其他语言中称为非尾递归函数的规则的情况就是这种情况。 So, once again, the stack in Mathematica is a stack of intermediate expressions, not function calls. 所以,再一次,Mathematica中的堆栈是一堆中间表达式,而不是函数调用。

This means that if, as a result of rule application, an (sub)expression can be rewritten in its entirety, the expression branch need not be kept on the expression stack. 这意味着,如果作为规则应用的结果,(子)表达式可以完整地重写,则表达式分支不需要保留在表达式堆栈上。 This is probably what is referred as tail call optimization in Mathematica - and this is why in such cases we have iteration rather than recursion (this is one very good example of the differences between rule applications and function calls). 这可能是Mathematica中所谓的尾调用优化 - 这就是为什么在这种情况下我们有迭代而不是递归(这是规则应用程序和函数调用之间差异的一个非常好的例子)。 Rules like f[x_]:=f[x+1] are of this type. f[x_]:=f[x+1]这样的规则属于这种类型。 If, however, some sub-expression get rewritten, producing more expression structure, then expression must be stored on the stack. 但是,如果某个子表达式被重写,产生更多的表达式结构,则表达式必须存储在堆栈中。 The rule f[x_ /; x < 5] := (n += 1; f[x + 1]) 规则f[x_ /; x < 5] := (n += 1; f[x + 1]) f[x_ /; x < 5] := (n += 1; f[x + 1]) is of this type, which is a bit hidden until we recall that () stand for CompoundExpression[] . f[x_ /; x < 5] := (n += 1; f[x + 1])属于这种类型,在我们回想起()代表CompoundExpression[]之前,它有点隐藏。 Schematically what happens here is f[1] -> CompoundExpression[n+=1, f[2]] -> CompoundExpression[n+=1,CompoundExpression[n+=1,f[3]]]->etc . 示意性地,这里发生的是f[1] -> CompoundExpression[n+=1, f[2]] -> CompoundExpression[n+=1,CompoundExpression[n+=1,f[3]]]->etc Even though the call to f is the last every time, it happens before the full CompoundExpression[] executes, so this still must be kept on the expression stack. 即使每次调用f都是最后一次,它也会在完整的CompoundExpression[]执行之前发生,所以这仍然必须保留在表达式堆栈上。 One could perhaps argue that this is a place where optimization could be made, to make an exception for CompoundExpression, but this is probably not easy to implement. 有人可能会争辩说,这是一个可以进行优化的地方,为CompoundExpression做一个例外,但这可能不容易实现。

Now, to illustrate the stack accumulation process which I schematically described above, let us limit the number of recursive calls: 现在,为了说明我上面示意性描述的堆栈累积过程,让我们限制递归调用的数量:

Clear[n, f, ff, fff];
n = 0;
f[x_ /; x < 5] := (n += 1; f[x + 1]);

ff[x_] := Null /; (n += 1; False);
ff[x_ /; x < 5] := ff[x + 1];

fff[x_ /; x < 5] := ce[n += 1, fff[x + 1]];

Tracing the evaluation: 跟踪评估:

In[57]:= Trace[f[1],f]
Out[57]= {f[1],n+=1;f[1+1],{f[2],n+=1;f[2+1],{f[3],n+=1;f[3+1],{f[4],n+=1;f[4+1]}}}}

In[58]:= Trace[ff[1],ff]
Out[58]= {ff[1],ff[1+1],ff[2],ff[2+1],ff[3],ff[3+1],ff[4],ff[4+1],ff[5]}

In[59]:= Trace[fff[1],fff]
Out[59]= {fff[1],ce[n+=1,fff[1+1]],{fff[2],ce[n+=1,fff[2+1]],{fff[3],ce[n+=1,fff[3+1]],   
{fff[4],ce[n+=1,fff[4+1]]}}}}

What you can see from this is that the expression stack accumulates for f and fff (the latter used just to show that this is a general mechanism, with ce[] just some arbitrary head), but not for ff , because, for the purposes of pattern matching, the first definition for ff is a rule tried but not matched, and the second definition rewrites ff[arg_] in its entirety, and does not generate deeper sub-parts that need further rewriting. 你可以从中看到的是表达式堆栈为ffff积累(后者用于表示这是一般机制, ce[]只是一些任意的头),但不适用于ff ,因为,为此目的对于模式匹配, ff的第一个定义是尝试但未匹配的规则,第二个定义完整地重写ff[arg_] ,并且不生成需要进一步重写的更深层子部分。 So, the bottom line seems that you should analyze your function and see if its recursive calls will grow the evaluated expression from the bottom or not. 所以,底线似乎是你应该分析你的函数,看看它的递归调用是否会从底部增长评估表达式。 If yes, it is not tail-recursive as far as Mathematica is concerned. 如果是,就Mathematica而言,它不是尾递归的。

My answer would not be complete without showing how to do the tail call optimization manually. 如果不显示如何手动执行尾调用优化,我的答案就不会完整。 As an example, let us consider recursive implementation of Select. 作为示例,让我们考虑Select的递归实现。 We will work with Mathematica linked lists to make it reasonably efficient rather than a toy. 我们将使用Mathematica链表来使其合理有效而不是玩具。 Below is the code for the non tail-recursive implementation: 下面是非尾递归实现的代码:

Clear[toLinkedList, test, selrecBad, sel, selrec, selTR]
toLinkedList[x_List] := Fold[{#2, #1} &, {}, Reverse[x]];
selrecBad[fst_?test, rest_List] := {fst,If[rest === {}, {}, selrecBad @@ rest]};
selrecBad[fst_, rest_List] := If[rest === {}, {}, selrecBad @@ rest];
sel[x_List, testF_] := Block[{test = testF}, Flatten[selrecBad @@ toLinkedList[x]]]

The reason I use Block and selrecBad is to make it easier to use Trace. 我使用Block和selrecBad的原因是为了更容易使用Trace。 Now, this blows the stack on my machine: 现在,这会炸掉我机器上的堆栈:

Block[{$RecursionLimit = Infinity}, sel[Range[300000], EvenQ]] // Short // Timing

You can trace on small lists to see why: 您可以跟踪小列表以查看原因:

In[7]:= Trace[sel[Range[5],OddQ],selrecBad]

Out[7]= {{{selrecBad[1,{2,{3,{4,{5,{}}}}}],{1,If[{2,{3,{4,{5,{}}}}}==={},{},selrecBad@@{2,{3,{4, 
{5,{}}}}}]},{selrecBad[2,{3,{4,{5,{}}}}],If[{3,{4,{5,{}}}}==={},{},selrecBad@@{3,{4,{5, 
{}}}}],selrecBad[3,{4,{5,{}}}],{3,If[{4,{5,{}}}==={},{},selrecBad@@{4,{5,{}}}]},{selrecBad[4,
{5,{}}],If[{5,{}}==={},{},selrecBad@@{5,{}}],selrecBad[5,{}],{5,If[{}==={},{},selrecBad@@{}]}}}}}}

What happens is that the result gets accumulated deeper and deeper in the list. 结果是,结果在列表中越来越深入。 The solution is to not grow the depth of the resulting expression, and one way to achieve that is to make selrecBad accept one extra parameter, which is the (linked) list of accumulated results: 解决方案是不增加结果表达式的深度,实现这一目的的一种方法是使selrecBad接受一个额外的参数,即累积结果的(链接)列表:

selrec[{fst_?test, rest_List}, accum_List] := 
    If[rest === {}, {accum, fst}, selrec[rest, {accum, fst}]];
selrec[{fst_, rest_List}, accum_List] := 
    If[rest === {}, accum, selrec[rest, accum]]

And modify the main function accordingly: 并相应地修改主要功能:

selTR[x_List, testF_] := Block[{test = testF}, Flatten[selrec[toLinkedList[x], {}]]]

This will pass our power test just fine: 这将通过我们的功率测试就好了:

In[14]:= Block[{$IterationLimit= Infinity},selTR[Range[300000],EvenQ]]//Short//Timing

Out[14]= {0.813,{2,4,6,8,10,12,14,16,18,20,
<<149981>>,299984,299986,299988,299990,299992,299994,299996,299998,300000}}

(note that here we had to modify $IterationLimit, which is a good sign). (注意,这里我们必须修改$ IterationLimit,这是一个好兆头)。 And using Trace reveals the reason: 并使用Trace揭示了原因:

In[15]:= Trace[selTR[Range[5],OddQ],selrec]

Out[15]= {{{selrec[{1,{2,{3,{4,{5,{}}}}}},{}],If[{2,{3,{4,{5,{}}}}}==={},{{},1},selrec[{2,{3,{4, 
{5,{}}}}},{{},1}]],selrec[{2,{3,{4,{5,{}}}}},{{},1}],If[{3,{4,{5,{}}}}==={},{{},1},selrec[{3, 
{4,{5,{}}}},{{},1}]],selrec[{3,{4,{5,{}}}},{{},1}],If[{4,{5,{}}}==={},{{{},1},3},selrec[{4, 
{5,{}}},{{{},1},3}]],selrec[{4,{5,{}}},{{{},1},3}],If[{5,{}}==={},{{{},1},3},selrec[{5, 
{}},{{{},1},3}]],selrec[{5,{}},{{{},1},3}],If[{}==={},{{{{},1},3},5},selrec[{},{{{{},1},3},5}]]}}}

which is, this version does not accumulate the depth of the intermediate expression, since the results are kept in a separate list. 也就是说,此版本不会累积中间表达式的深度,因为结果保存在单独的列表中。

The idea of this answer is to replace the brackets () by a wrapper that does not make our expressions grow. 这个答案的想法是用一个不会使我们的表达式增长的包装器替换方括号()。 Note that the function we are finding an alternative for is really CompoundExpression, as the OP was correct in remarking this function was ruining the tail recursion (see also the answer by Leonid). 请注意,我们正在寻找替代方法的函数实际上是CompoundExpression,因为OP正确地说明这个函数破坏了尾递归(参见Leonid的答案)。 Two solutions are provided. 提供了两种解决方案。 This defines the first wrapper 这定义了第一个包装器

SetAttributes[wrapper, HoldRest];
wrapper[first_, fin_] := fin
wrapper[first_, rest__] := wrapper[rest]

We then have that 然后我们有了

Clear[f]
k = 0;
mmm = 1000;
f[n_ /; n < mmm] := wrapper[k += n, f[n + 1]];
f[mmm] := k + mmm
Block[{$IterationLimit = Infinity}, f[0]]

Correctly calculates Total[Range[1000]]. 正确计算总计[范围[1000]]。

------Note----- - - - 注意 - - -

Note that it would be misleading to set 请注意,设置会产生误导

wrapper[fin_] := fin;

As in the case 与案件一样

f[x_]:= wrapper[f[x+1]]

Tail recursion does not occur (because of the fact that wrapper, having HoldRest, will evaluate the singular argument before applying the rule associated with wrapper[fin_]). 尾递归不会发生(因为具有HoldRest的包装器将在应用与包装器[fin_]相关联的规则之前评估单数参数)。

Then again, the definition above for f is not useful, as one could simply write 然后,上面对f的定义没有用,因为人们可以简单地写

f[x_]:= f[x+1]

And have the desired tail recursion. 并有所需的尾递归。

------Another note----- ------另一个注意事项-----

In case we supply the wrapper with a lot arguments, it may be slower than necessary. 如果我们为包装器提供了很多参数,它可能比必要的慢。 The user may opt to write 用户可以选择写

f[x_]:=wrapper[g1;g2;g3;g4;g5;g6;g7  , f[x+1]]

Second wrapper 第二个包装

The second wrapper feeds its arguments to CompoundExpression and will therefore be faster than the first wrapper if many arguments are provided. 第二个包装器将其参数提供给CompoundExpression,因此如果提供了许多参数,它将比第一个包装器更快。 This defines the second wrapper. 这定义了第二个包装器。

SetAttributes[holdLastWrapper, HoldAll]
holdLastWrapper[fin_] := fin
holdLastWrapper[other_, fin_] := 
 Function[Null, #2, HoldRest][other, fin]
holdLastWrapper[others__, fin_] := 
 holdLastWrapper[
  Evaluate[CompoundExpression[others, Unevaluated[Sequence[]]]], fin]

Note: Returning (empty) Sequences might be very useful in recursion in general. 注意:返回(空)序列通常在递归时非常有用。 See also my answer here 另见我的答案

https://mathematica.stackexchange.com/questions/18949/how-can-i-return-a-sequence https://mathematica.stackexchange.com/questions/18949/how-can-i-return-a-sequence

Note that this function will still work if only one argument is provided, as it has attribute HoldAll rather than HoldRest, so that setting 请注意,如果只提供一个参数,此函数仍然有效,因为它具有属性HoldAll而不是HoldRest,因此该设置

f[x]:= holdLastWrapper[f[x+1]]

Will yield a tail recursion (wrapper does not have this behavior). 将产生尾递归(包装器没有此行为)。

Speed comparison 速度比较

Let's create a nice long list (actually an expression with Head Hold) of instructions 让我们创建一个很好的长列表(实际上是一个带有Head Hold的表达式)的指令

nnnn = 1000;
incrHeld = 
  Prepend[DeleteCases[Hold @@ ConstantArray[Hold[c++], nnnn], 
    Hold, {2, Infinity}, Heads -> True], Unevaluated[c = 0]];

For these instructions, we can compare the performance (and outcome) of our wrappers with CompoundExpression 对于这些说明,我们可以将包装器的性能(和结果)与CompoundExpression进行比较

holdLastWrapper @@ incrHeld // Timing
CompoundExpression @@ incrHeld // Timing
wrapper @@ incrHeld // Timing

--> {{0.000856, 999}, {0.000783, 999}, {0.023752, 999}} - > {{0.000856,999},{0.000783,999},{0.023752,999}}

Conclusion 结论

The second wrapper is better if you are not exactly sure when tail recursion will happen or how many arguments you will feed to the wrapper. 如果您不确定何时会发生尾递归,或者您将向包装器提供多少参数,则第二个包装器会更好。 If you are intent on feeding the wrapper 2 arguments, for example in the case where you realize all the second wrapper does is feed to CompoundExpression and you decide to do this yourself, the first wrapper is better. 如果您打算提供包装器2参数,例如在您意识到所有第二个包装器都是FeedExpression并且您决定自己执行此操作的情况下,第一个包装器会更好。

-----final note---- -----最后的注意事项----

In CompoundExpression[args, Unevaluated[expr]], expr still gets evaluated before CompoundExpression is stripped, so solutions of this type are no use. 在CompoundExpression [args,Unevaluated [expr]]中,在剥离CompoundExpression之前仍然会计算expr,因此这种类型的解决方案没有用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM