如何找出 Prolog 是不是执行尾调用优化
Posted
技术标签:
【中文标题】如何找出 Prolog 是不是执行尾调用优化【英文标题】:How to find out if Prolog performs Tail Call Optimization如何找出 Prolog 是否执行尾调用优化 【发布时间】:2013-03-15 13:16:37 【问题描述】:使用SWI Prolog开发版(Win x64), 我为 deterministic lexer (hosted on github) 写了一个 DCG 谓词(因此所有外部谓词都没有选择点):
read_token(parser(Grammar, Tables),
lexer(dfa-DFAIndex, last_accept-LastAccept, chars-Chars0),
Token) -->
( [Input],
dfa:current(Tables, DFAIndex, DFA),
char_and_code(Input, Char, Code),
dfa:find_edge(Tables, DFA, Code, TargetIndex)
-> table:item(dfa_table, Tables, TargetIndex, TargetDFA),
dfa:accept(TargetDFA, Accept),
atom_concat(Chars0, Char, Chars),
NewState = lexer(dfa-TargetIndex,
last_accept-Accept,
chars-Chars)
,
read_token(parser(Grammar, Tables), NewState, Token)
;
( LastAccept \= none
-> Token = LastAccept-Chars0
; ( ground(Input)
-> once(symbol:by_type_name(Tables, error, Index, _)),
try_restore_input(Input, FailedInput, InputR),
Input = [FailedInput | InputR],
format(atom(Error), '~w', [FailedInput]),
Token = Index-Error
; once(symbol:by_type_name(Tables, eof, Index, _)),
Token = Index-''
)
)
).
现在经常使用(;)
和->
,我想知道SWI-Prolog 可以使用Tail-Call-Optimization 优化递归read_token(parser(Grammar, Tables), NewState, Token)
,
或者如果我必须手动将谓词分成几个子句。
我只是不知道如何找出解释器的作用,尤其是知道在运行调试器时 TCO 被禁用。
【问题讨论】:
在不调试的情况下运行解释器时堆栈是否溢出? 看你的代码sn-p,似乎满足了TCO的必要条件(read_token/3
调用自己时没有选择点打开)。我知道您在问 SWI-Prolog 在这种特定情况下是否真的利用了 TCO。
不,因为我使用的是 x64 位,所以堆栈大小非常高。我只是在寻求有关编程技术的建议。我没有足够大的文件让词法分析器引起我想的溢出(因为我可以很好地调试方法,没有应用 TCO)
我可以用减少堆栈大小的命令行选项来回答。这将是我测试 TCO 的粗略方法。
您感兴趣的是代码失败的本地堆栈大小阈值。请参阅页面底部的讨论How do I enlarge the stacks?。
【参考方案1】:
为了回答您的问题,我首先寻找可能会阻止最后调用优化的“琐碎”目标。如果找到一些:
; (接地(输入) -> 一次(符号:by_type_name(表,错误,索引,_)), try_restore_input(输入,失败输入,InputR), 输入 = [失败输入 |输入R], 格式(原子(错误),'〜w',[FailedInput]), 令牌 = 索引错误 ;一次(符号:by_type_name(表,eof,索引,_)), 令牌 = 索引-'' )
在这两种情况下,仅这些目标就可以防止 LCO。
现在,我遵守了你的规则并查看了 listing
的扩展:
ad 1) 这是您最可能正在寻找的递归案例。在这里,一切似乎都很好。
ad 2,3) 上面已经讨论过了,也许你想交换目标
ad 4) 这是在 DCG 中精确、稳定地处理 //1
的效果。根据经验:实施者宁愿坚定不移,也不愿争取 LCO-ness。请参考:DCG Expansion: Is Steadfastness ignored?
还请注意,这不仅仅是调用框架的简单重用。与垃圾收集有很多交互。为了克服 SWI 中的所有这些问题,需要一个额外的 GC 阶段。
有关更多信息,请参阅 Precise Garbage Collection in Prolog 中的微小基准
所以最后回答你的问题:你的规则可能会优化;前提是在递归目标之前没有选择点。
这也有真正的低级方法。我从不将其用于代码开发:vm_list
。该清单最终向您展示了 SWI 是否会考虑 LCO(前提是没有选择点)。
i_call
和 i_callm
永远不会执行 LCO。只有i_depart
可以。在:142 i_depart(read_token/5)
【讨论】:
无论Token
(编译输出中的S
)始终未绑定,选择点都会留下,对吧?
代码本身不会产生多余的选择点。但也许 table:item 或 dfa:accept 确实留下了一个开放的选择点。这一切都取决于。您可以通过在最后立即调用statistics
轻松观察到这一点:本地堆栈上还剩下多少?
哦!我忘了说,所有使用的谓词都没有留下任何选择点
我喜欢你顺便提一下listing,我一直把DCG代码看成源代码,有时会忘记term_Expansion语法糖。
您对使用listing
和statistics
提出了一些好处,但我不明白关于选择点%2、%3、%4 的结论。当read_token
调用自身时,这些不是“打开的”。对read_token
的调用位于if-then-else 构造的一个分支上。 Prolog 提交到一个分支并且不会回溯到另一个分支,因此 AFAIK 上次调用优化将起作用。也许我们需要查看“本地堆栈”统计信息才能确定。以上是关于如何找出 Prolog 是不是执行尾调用优化的主要内容,如果未能解决你的问题,请参考以下文章