Prolog DCG语法规则中的堆栈溢出:如何有效或懒惰地处理大型列表

时间:2012-10-17 17:19:04

标签: prolog swi-prolog dcg

我正在解析一个由一系列行组成的相当简单的文件格式,每行都有一些空格分隔的字段,如下所示:

l 0x9823 1
s 0x1111 3
l 0x1111 12
⋮

我正在使用SWI-Prolog。这是我到目前为止的DCG:

:- consult(library(pure_input)).

load_trace(Filename, Traces) :-
    phrase_from_file(trace_file_phrase(Traces), Filename).

trace_file_phrase([]) --> [].
trace_file_phrase([T|Ts]) --> trace_phrase(T), trace_file_phrase(Ts).

trace_phrase(access(Type, Address, SinceLast)) -->
    access_type(Type), space,
    address(Address),  space,
    nat(SinceLast),    newline.

access_type(load)  --> "l".
access_type(store) --> "s".

address(Number) --> "0x", hexnum(Number).

hexdigit(N)  --> digit(N).
hexdigit(10) --> "a". hexdigit(11) --> "b". hexdigit(12) --> "c".
hexdigit(13) --> "d". hexdigit(14) --> "e". hexdigit(15) --> "f".
hexnum(N) --> hexdigit(D), hexnum(D, N).
hexnum(N, N) --> [].
hexnum(A, N) --> hexdigit(D), { A1 is A*16 + D }, hexnum(A1, N).

newline --> "\n".
space --> " ".

%% the following two productions are courtesy of Lars Mans at
%% https://stackoverflow.com/questions/3279822/parsing-numbers-with-multiple-digits-in-prolog
digit(0) --> "0". digit(1) --> "1". digit(2) --> "2".
digit(3) --> "3". digit(4) --> "4". digit(5) --> "5".
digit(6) --> "6". digit(7) --> "7". digit(8) --> "8".
digit(9) --> "9".

nat(N)   --> digit(D), nat(D,N).
nat(N,N) --> [].
nat(A,N) --> digit(D), { A1 is A*10 + D }, nat(A1, N).

正如评论中所提到的,我从Parsing numbers with multiple digits in Prolog处理了数字处理。

我遇到的问题是这些文件中的一些很大,例如,大约5-10 MB。 SWI-Prolog中的默认堆栈不足以解决这个问题,解析这些文件需要花费大量时间,大约5-15秒。关于这种情况,我有几个问题:

  1. 此代码中的效率问题在哪里?我认为它可以在trace_file_phrase//1nat//1,但这些只是预感。
  2. 如果问题是列表,是否有更好的方法来处理带有DCG的列表?
  3. 一般来说,如何诊断和处理DCG这样的性能问题?

3 个答案:

答案 0 :(得分:16)

作为一般性评论,您可以在名称library(pio)下找到有关它的更多信息。另外,干净利用它的方式是:

:- use_module(library(pio)).

你的例子过于复杂,所以我只考虑一个稍微简单的案例,换行符分隔的新行:

nats([]) --> [].
nats([N|Ns]) --> nat(N), newline, nats(Ns).

那么,你怎么能有效地测试呢? (这是你的问题3)library(pio)的基本要点是你可以使用常规DCG进行文件处理。但是对于小型测试,您仍然可以使用简单的phrase/2。所以我这样做:

?- phrase(nats(Ns),"1\n").
Ns = [1] ;
false.

您看到;提示了吗?这意味着:Prolog无法决定是否可以计算进一步的答案 - 因此它会打开一个或多个选择点。 只有一个数字你可以想象事情会如何堆积。

让我们深入挖掘:

?- phrase(digit(D),"1").
D = 1 ;
false.

又是邪恶的;!为了使这项工作,一切都必须确定。有三种方法:

使用削减(并失去灵魂)

祝你好运 - 最好的似乎就在重复元素之后:

trace_file_phrase([]) --> [].
trace_file_phrase([T|Ts]) -->
   trace_phrase(T),
   !, % ugly, but...
   trace_file_phrase(Ts).

(这应该回答问题1)

但是,等一下!这个!有什么不好的?只要有完全trace_phrase//1的一个答案是完美的。只有在有更多答案(或实际解决方案)的情况下,裁剪可能会删除宝贵的答案。你怎么知道,如果有更多的解决方案?好吧,你没有。你不会看到它们,因为它们已经被切掉了。

call_semidet/1

这是一种确保不会发生这种情况的方法。这仅适用于可以被调用两次而没有任何效果的无副作用目标:

call_semidet(Goal) :-
   (  call_nth(Goal, 2)
   -> throw(error(mode_error(semidet,Goal),_))
   ;  once(Goal)
   ).

这使用call_nth/2,如另一篇文章中所定义。 (作为优化,当没有选择点打开时,实现可以避免两次调用Goal ...)只是为了弄清楚它是如何工作的:

?- phrase(nat(N),"1234").
N = 1234 ;
false.

?- call_semidet(phrase(nat(N),"1234")).
N = 1234.

?- call_semidet((X=1;X=2)).
ERROR: Unknown error term: mode_error(semidet, (2=1;2=2))

因此它可以使您的小语法有效确定!因此没有必要重新制定任何东西!

现在缺少的是将其整合到语法中。您可以使用library(lambda)进行非常低级别的操作,或者干净利落。

phrase_semidet(NT) -->
   call(S0^S^call_semidet(phrase(NT,S0,S))).

请注意,在这种特殊情况下,我们不会使用\进行重命名。

trace_file_phrase([]) --> [].
trace_file_phrase([T|Ts]) -->
   phrase_semidet(trace_phrase(T)),
   trace_file_phrase(Ts).

利用索引编制

最后,一个非常费力但干净的方法是重写所有内容以便从索引中获得更好的利润(并且可能有助于改进索引...)但这是一条漫长的道路。刚开始:

digit(D) --> [C],
   {c_digit(C,D)}.

c_digit(0'0,0).
c_digit(0'1,1).
c_digit(0'2,2).
c_digit(0'3,3).
c_digit(0'4,4).
c_digit(0'5,5).
c_digit(0'6,6).
c_digit(0'7,7).
c_digit(0'8,8).
c_digit(0'9,9).

现在给你:

?- phrase(digit(D),"1").
D = 1.

但是你有另一个非确定性的来源,这是由于你定义语法的方式。在nat//2中,您可以看到它:

nat(N,N) --> [].
nat(A,N) --> digit(D), ... .

第一条规则始终适用,即"1234\n"将被解析为"1" "12" "123" "1234"只有以下newline//0才能实现它足以满足最后 - 然后坚持下去。

你可以为此重写一些东西,但是,代码不再是你喜欢的纯粹的小规格,不是吗?好吧,未来可能会有所改善。

E.g。 SWI中的索引比过去好得多,也许事情也在这里发展....

library(pio)的目的是让这个过程开始。将此与Haskell相比 - 我们远离interact效率!但是没有固有的成本:

... --> [] | [_], ... .

?- phrase_from_file((...,"searchstring",...),fichier).

与grep一样有效 - 空间方面。也就是说,它在恒定的空间中运行。所以希望将来会有更多代码运行得更好。

编辑:BTW,library(pio)确实已经产生了影响效率:GC阶段得到了显着改善,与Wadler修复一些空间泄漏的方式非常相似 - 四分之一世纪前。事情在发展......

答案 1 :(得分:7)

我已经验证了2Mb文件上的stackoverflow。然后我用库(dcg / basics)重写了语法,现在正在工作。

:- [library(dcg/basics)].

load_trace_0(Filename, Ls) :-
    phrase_from_file(lines(Ls), Filename).

lines([s(H,I)|R]) -->
    "s 0x", xinteger(H), " ",
    integer(I), blanks,
    !, lines(R).
lines([l(H,I)|R]) -->
    "l 0x", xinteger(H), " ",
    integer(I), blanks,
    !, lines(R).
lines([]) --> [].

但是后来我试着把你的语法剪下来,并且也在努力。所以@gusbro(+1)的答案解决了你的问题。

答案 2 :(得分:5)

关于效率问题:

如果您的输入通常是完整的,那么我认为您应该交换nat/4hexnum/4的条款,以便他们阅读:

nat(A,N) --> digit(D), { A1 is A*10 + D }, nat(A1, N).
nat(N,N) --> [].

hexnum(A, N) --> hexdigit(D), { A1 is A*16 + D }, hexnum(A1, N).
hexnum(N, N) --> [].

因为您只想在没有更多数字要消费时停止解析数字。

如果明智地使用,剪切(!)可以帮助您在性能方面以及堆栈溢出,因为它修剪了prolog评估树。例如,您可以在!的末尾(即trace_file_phrase/3之后)提交(newline),因为您不需要再次重新分析输入的那部分以查找其他解决方案。