【问题标题】:Will this be tail call optimized in SWI-Prolog这将是 SWI-Prolog 中优化的尾调用吗
【发布时间】:2020-10-30 08:38:41
【问题描述】:
step_n(0, I, I).
step_n(N, In, Out) :-
    N > 0, plus(N1, 1, N), phase_step(In, T),
    step_n(N1, T, Out).

phase_step 是一个转换数据的函数。

这个step_n 会和phase_step 在几乎相同的内存中运行吗?如果没有,我应该如何重写它来做到这一点?这是否取决于 phase_step 有一个单一的解决方案?

编辑:在使用prolog_current_frame 进行一些调试后,我发现如果phase_step 是像Out is In + 1 这样的简单函数,那么优化会发生但my use case 不会发生。

为什么 TCO 依赖于 phase_step 谓词?

【问题讨论】:

  • 堆栈空间不足。这也发生在我尝试迭代的其他情况下,写为尾调用递归。
  • @GuyCoder : 你的意思是关闭尾调用优化(调试、跟踪时)?
  • 所以我能找到的与该谓词相关的最好方法是鲍里斯在post 中使用prolog_current_frame/1format/2 的方式
  • 请注意,使用调试器和跟踪会关闭(感谢@rajashekar)尾调用优化,所以不要依赖它来获得明确的答案。

标签: prolog tail-call-optimization


【解决方案1】:

这是否取决于 phase_step 是否只有一个解决方案?

有点,但更强大一点:这取决于phase_step确定性,这意味着不留下任何“选择点”。一个选择点是未来要探索的路径;不一定会产生进一步的解决方案,但 Prolog 仍然需要检查。

例如,这是确定性的:

phase_step_det(X, X).

它只有一个解决方案,Prolog 不会提示我们更多:

?- phase_step_det(42, Out).
Out = 42.

以下有一个单一的解决方案,但它不是确定性的:

phase_step_extrafailure(X, X).
phase_step_extrafailure(_X, _Y) :-
    false.

看到解决方案后,Prolog 仍有一些需要检查的地方。即使我们可以通过查看代码来判断某些东西(第二个子句)会失败:

?- phase_step_extrafailure(42, Out).
Out = 42 ;
false.

以下有多个解决方案,因此不是确定性的:

phase_step_twosolutions(X, X).
phase_step_twosolutions(X, Y) :-
    plus(X, 1, Y).

?- phase_step_twosolutions(42, Out).
Out = 42 ;
Out = 43.

为什么 TCO 依赖于 phase_step 谓词?

如果还有更多路径需要探索,那么关于这些路径的数据必须存储在某处。那个“某处”是某种堆栈数据结构,对于每个未来的路径,堆栈上都必须有一个框架。这就是您的内存使用量增加的原因。随之而来的是计算时间(以下使用您的 step_n 的副本以及我上面对应的 phase_step 变体):

?- time(step_n_det(100_000, 42, Out)).
% 400,002 inferences, 0.017 CPU in 0.017 seconds (100% CPU, 24008702 Lips)
Out = 42 ;
% 7 inferences, 0.000 CPU in 0.000 seconds (87% CPU, 260059 Lips)
false.

?- time(step_n_extrafailure(100_000, 42, Out)).
% 400,000 inferences, 4.288 CPU in 4.288 seconds (100% CPU, 93282 Lips)
Out = 42 ;
% 100,005 inferences, 0.007 CPU in 0.007 seconds (100% CPU, 13932371 Lips)
false.

?- time(step_n_twosolutions(100_000, 42, Out)).
% 400,000 inferences, 4.231 CPU in 4.231 seconds (100% CPU, 94546 Lips)
Out = 42 ;
% 4 inferences, 0.007 CPU in 0.007 seconds (100% CPU, 548 Lips)
Out = 43 ;
% 8 inferences, 0.005 CPU in 0.005 seconds (100% CPU, 1612 Lips)
Out = 43 ;
% 4 inferences, 0.008 CPU in 0.008 seconds (100% CPU, 489 Lips)
Out = 44 ;
% 12 inferences, 0.003 CPU in 0.003 seconds (100% CPU, 4396 Lips)
Out = 43 ;
% 4 inferences, 0.009 CPU in 0.009 seconds (100% CPU, 451 Lips)
Out = 44 .  % many further solutions

探索这一点的一种方法是使用 SWI-Prolog 调试器,它可以向您展示备选方案(= 选择点 = 未来要探索的路径):

?- trace, step_n_det(5, 42, Out).
   Call: (9) step_n_det(5, 42, _1496) ? skip           % I typed 's' here.
   Exit: (9) step_n_det(5, 42, 42) ? alternatives      % I typed 'A' here.
 [14] step_n_det(0, 42, 42)
   Exit: (9) step_n_det(5, 42, 42) ? no debug          % I typed 'n' here.
Out = 42 ;
false.

?- trace, step_n_extrafailure(5, 42, Out).
   Call: (9) step_n_extrafailure(5, 42, _1500) ? skip
   Exit: (9) step_n_extrafailure(5, 42, 42) ? alternatives
 [14] step_n_extrafailure(0, 42, 42)
 [14] phase_step_extrafailure(42, 42)
 [13] phase_step_extrafailure(42, 42)
 [12] phase_step_extrafailure(42, 42)
 [11] phase_step_extrafailure(42, 42)
 [10] phase_step_extrafailure(42, 42)
   Exit: (9) step_n_extrafailure(5, 42, 42) ? no debug
Out = 42 ;
false.

所有这些选择都对应于额外的解释器框架。如果您使用 SWI-Prolog 的可视化调试器,它还会向您显示堆栈的图形表示,包括所有打开的选择点(尽管我一直觉得这很难理解)。

因此,如果您想要 TCO 并且不增加堆栈,您需要确定性地执行阶段步骤。您可以通过使phase_step 谓词本身具有确定性来做到这一点。您也可以在 step_n 内部的 phase_step 调用之后进行剪切。

以下是来自上方的呼叫,每个 phase_step 后面都有一个剪辑:

?- time(step_n_det(100_000, 42, Out)).
% 400,001 inferences, 0.017 CPU in 0.017 seconds (100% CPU, 24204529 Lips)
Out = 42 ;
% 7 inferences, 0.000 CPU in 0.000 seconds (83% CPU, 737075 Lips)
false.

?- time(step_n_extrafailure(100_000, 42, Out)).
% 400,000 inferences, 0.023 CPU in 0.023 seconds (100% CPU, 17573422 Lips)
Out = 42 ;
% 5 inferences, 0.000 CPU in 0.000 seconds (93% CPU, 220760 Lips)
false.

?- time(step_n_twosolutions(100_000, 42, Out)).
% 400,000 inferences, 0.023 CPU in 0.023 seconds (100% CPU, 17732727 Lips)
Out = 42 ;
% 5 inferences, 0.000 CPU in 0.000 seconds (94% CPU, 219742 Lips)
false.

不要盲目地进行切割,只有在了解您真正需要它们的位置和原因之后。请注意,在extrafailure 的情况下,cut 只删除了失败,但在twosolutions 的情况下,它删除了实际的解决方案。

【讨论】:

【解决方案2】:

一个有助于理解代码性能问题(尤其是不需要的非确定性)的有用工具是 ports profiler 工具,例如ECLiPSe 和 Logtalk。 Logtalk ports_profiler 工具是可移植的,所以我们可以在这里使用它。我们首先包装您的代码(来自您的 gist 链接):

:- use_module(library(lists), []).


:- object(step).

    :- public(step_n/3).
    :- use_module(lists, [reverse/2]).

    % pattern for the nth digit mth-coeffcient
    digit_m(N, M, D) :-
        divmod(M, N, Q, _),  divmod(Q, 4, _, C),
        (C = 0, D = 0; C = 1, D = 1; C = 2, D = 0; C = 3, D = -1).
    
    calculate_digit_n(N, In, D) :-
        calculate_digit_n_(N, In, D, 1, 0).
    calculate_digit_n_(_, [], D, _, Acc) :- D1 is abs(Acc), divmod(D1, 10, _, D).
    calculate_digit_n_(N, [I | Is], D, M, Acc) :-
        digit_m(N, M, C), P is C*I, M1 is M+1, Acc1 is Acc+P,
        calculate_digit_n_(N, Is, D, M1, Acc1).
    
    phase_step(In, Out) :-
        length(In, L), L1 is L + 1, phase_step_(In, Out, L1, 1, []).
    phase_step_(_, Out, L, L, Acc) :- reverse(Out, Acc).
    phase_step_(In, Out, L, N, Acc) :-
        N < L, calculate_digit_n(N, In, D), N1 is N + 1,
        phase_step_(In, Out, L, N1, [D | Acc]).
    
    step_n(0, I, I).
    step_n(N, In, Out) :-
        prolog_current_frame(Fr), format('~w ', Fr),
        N > 0, N1 is N - 1, phase_step(In, T),
        step_n(N1, T, Out).

:- end_object.

%:- step_n(10, [1, 2, 3, 4, 5, 6, 7, 8], X).

然后(使用 SWI-Prolog 作为后端,因为这是您告诉我们您正在使用的 Prolog 系统):

$ swilgt
...
?- {ports_profiler(loader)}.
% [ /Users/pmoura/logtalk/tools/ports_profiler/ports_profiler.lgt loaded ]
% [ /Users/pmoura/logtalk/tools/ports_profiler/loader.lgt loaded ]
% (0 warnings)
true.

?- logtalk_load(step, [debug(on), source_data(on)]).
% [ /Users/pmoura/step.pl loaded ]
% (0 warnings)
true.

?- step::step_n(10, [1, 2, 3, 4, 5, 6, 7, 8], X).
340 15578 30816 46054 61292 76530 91768 107006 122244 137482 
X = [3, 6, 4, 4, 0, 6, 7, 8] .

?- ports_profiler::data.
------------------------------------------------------------------------------
Entity  Predicate               Fact  Rule  Call  Exit *Exit  Fail  Redo Error
------------------------------------------------------------------------------
step    calculate_digit_n/3        0    80    80     0    80     0     0     0
step    calculate_digit_n_/5       0   720   720     0   720     0     0     0
step    digit_m/3                  0   640   640    40   600     0     0     0
step    phase_step/2               0    10    10     0    10     0     0     0
step    phase_step_/5              0    90    90     0    90     0     0     0
step    step_n/3                   1    10    11     0    11     0     0     0
------------------------------------------------------------------------------
true.

*Exit 列用于过程框中的非确定性存在。有关该工具和解释表格结果的帮助,请参阅 https://logtalk.org/manuals/devtools/ports_profiler.html 但只要看一眼表格就可以清楚地看到 phase_step/2step_n/3 都是不确定的。

更新

请注意,尾调用优化 (TCO) 并不意味着或要求谓词是确定性的。在您的情况下,TCO 可以由 Prolog 编译器应用,因为 step_n/3 谓词的规则中的 last 调用是对其自身的调用。这意味着堆栈帧可以保存在 那个 特定的递归调用中。这并不意味着递归调用之前没有创建选择点。使用 once/1 (正如您在 cmets 上提到的那样)只会丢弃在调用 phase_step/2 时创建的选择点,因为该谓词本身是不确定的。这就是表格显示的内容。 step_n/3 谓词也是非确定性的,因此当第一个参数是 0 时调用它会创建一个选择点,当您在第一个参数 上使用零调用谓词时会发生这种情况当查询的证明达到此递归定义的基本情况时。

【讨论】:

  • 我正在试用您的 logtalk 程序,但无法理解为什么使用 once/1 不会使程序尾调用可优化。在正常的 swi-prolog 中,它正在被优化 gist.github.com/k3ut0i/8e0db10915d5a59d13d79b5a3e5a2005>。 logtalk中对应的程序还是显示不同的栈帧和*Exit列gist.github.com/k3ut0i/14d2e25b513a6b4645eb1782c8955436>,为什么?
  • @rajashekar 更新了我的答案,以解释使用 once/1 时发生的情况。
  • 我知道phase_step/2 本身是不确定的。我是说 swi-prolog 和 logtalk 版本的行为不相似。使用 once/1 我有 swi-prolog 的 TCO,但我没有获得 logtalk 的 TCO。
  • 您确实获得了使用 Logtalk 版本的 TCO。只需加载对象(不带debug(on) 标志)并尝试查询:{step}, step::step_n(10, 1, X)。您将获得与 TCO 预期相同的堆栈帧。但是使用ports_profiler 工具可以检测代码,并且与大多数调试器(包括 Prolog 调试器)一样,可以防止 TCO。
  • 谢谢,我忘了调试(开启)。是的,使用 debug(off),我确实有 TCO。
猜你喜欢
  • 2016-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-16
  • 1970-01-01
  • 2021-09-04
相关资源
最近更新 更多