如何提高词法分析效率?

4
在解析一个3GB的文件DCG时,效率很重要。
我的词法分析器当前版本主要使用或预测;/2,但我读到索引可以帮助。 索引是一种用于快速选择特定目标谓词的候选子句的技术。在大多数Prolog系统中,索引仅在头部的第一个参数上完成。如果将此参数实例化为原子、整数、浮点数或复合项,散列将用于快速选择所有子句,其中第一个参数可能与目标的第一个参数一致。SWI-Prolog支持即时和多参数索引。请参见2.18章节。

有人能举例说明使用索引进行词法分析,并可能解释它如何提高效率吗?


细节

注意:在将源代码复制到此问题之前,我更改了一些名称。如果您发现错误,请随时在此处进行编辑或留下评论,我将很乐意修复它。

目前我的词法分析器/标记生成器(基于mzapotoczny/prolog-interpreter parser.pl)是这样的

% N.B.
% Since the lexer uses "" for values, the double_quotes flag has to be set to `chars`.
% If double_quotes flag is set to `code`, the the values with "" will not be matched.

:- use_module(library(pio)). 
:- use_module(library(dcg/basics)).
:- set_prolog_flag(double_quotes,chars).

lexer(Tokens) -->
   white_space,
   (
       (  ":",       !, { Token = tokColon }
      ;  "(",       !, { Token = tokLParen }
      ;  ")",       !, { Token = tokRParen }
      ;  "{",       !, { Token = tokLMusta}
      ;  "}",       !, { Token = tokRMusta}
      ;  "\\",      !, { Token = tokSlash}
      ;  "->",      !, { Token = tokImpl}
      ;  "+",       !, { Token = tokPlus }
      ;  "-",       !, { Token = tokMinus }
      ;  "*",       !, { Token = tokTimes }
      ;  "=",       !, { Token = tokEqual }
      ;  "<",       !, { Token = tokLt }
      ;  ">",       !, { Token = tokGt }
      ;  "_",       !, { Token = tokUnderscore }
      ;  ".",       !, { Token = tokPeriod }
      ;  "/",       !, { Token = tokForwardSlash }
      ;  ",",       !, { Token = tokComma }
      ;  ";",       !, { Token = tokSemicolon }
      ;  digit(D),  !,
            number(D, N),
            { Token = tokNumber(N) }
      ;  letter(L), !, identifier(L, Id),
            {  member((Id, Token), [ (div, tokDiv),
                                     (mod, tokMod),
                                     (where, tokWhere)]),
               !
            ;  Token = tokVar(Id)
            }
      ;  [_],
            { Token = tokUnknown }
      ),
      !,
      { Tokens = [Token | TokList] },
      lexer(TokList)
   ;  [],
         { Tokens = [] }
   ).

white_space -->
   [Char], { code_type(Char, space) }, !, white_space.
white_space -->
    "--", whole_line, !, white_space.
white_space -->
   [].

whole_line --> "\n", !.
whole_line --> [_], whole_line.

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

digits([D|T]) -->
   digit(D),
   !,
   digits(T).
digits([]) -->
   [].

number(D, N) -->
   digits(Ds),
      { number_chars(N, [D|Ds]) }.

letter(L) -->
   [L], { code_type(L, alpha) }.

alphanum([A|T]) -->
   [A], { code_type(A, alnum) }, !, alphanum(T).
alphanum([]) -->
   [].

alphanum([]).
alphanum([H|T]) :- code_type(H, alpha), alphanum(T).

identifier(L, Id) -->
   alphanum(As),
      { atom_codes(Id, [L|As]) }.

以下是一些用于开发和测试的辅助谓词。
read_file_for_lexing_and_user_review(Path) :-
    open(Path,read,Input),
    read_input_for_user_review(Input), !,
    close(Input).

read_file_for_lexing_and_performance(Path,Limit) :-
    open(Path,read,Input),
    read_input_for_performance(Input,0,Limit), !,
    close(Input).

read_input(Input) :-
    at_end_of_stream(Input).

read_input(Input) :-
    \+ at_end_of_stream(Input),
    read_string(Input, "\n", "\r\t ", _, Line),
    lex_line(Line),
    read_input(Input).

read_input_for_user_review(Input) :-
    at_end_of_stream(Input).

read_input_for_user_review(Input) :-
    \+ at_end_of_stream(Input),
    read_string(Input, "\n", "\r\t ", _, Line),
    lex_line_for_user_review(Line),
    nl,
    print('Press spacebar to continue or any other key to exit: '),
    get_single_char(Key),
    process_user_continue_or_exit_key(Key,Input).

read_input_for_performance(Input,Count,Limit) :-
    Count >= Limit.

read_input_for_performance(Input,_,_) :-
    at_end_of_stream(Input).

read_input_for_performance(Input,Count0,Limit) :-
    % print(Count0),
    \+ at_end_of_stream(Input),
    read_string(Input, "\n", "\r\t ", _, Line),
    lex_line(Line),
    Count is Count0 + 1,
    read_input_for_performance(Input,Count,Limit).

process_user_continue_or_exit_key(32,Input) :-  % space bar
    nl, nl,
    read_input_for_user_review(Input).

process_user_continue_or_exit_key(Key) :-
    Key \= 32.

lex_line_for_user_review(Line) :-
    lex_line(Line,TokList),
    print(Line),
    nl,
    print(TokList),
    nl.

lex_line(Line,TokList) :-
    string_chars(Line,Code_line),
    phrase(lexer(TokList),Code_line).

lex_line(Line) :-
    string_chars(Line,Code_line),
    phrase(lexer(TokList),Code_line).

read_user_input_for_lexing_and_user_review :-
    print('Enter a line to parse or just Enter to exit: '),
    nl,
    read_string(user, "\n", "\r", _, String),
    nl,
    lex_line_for_user_review(String),
    nl,
    continue_user_input_for_lexing_and_user_review(String).

continue_user_input_for_lexing_and_user_review(String) :-
    string_length(String,N),
    N > 0,
    read_user_input_for_lexing_and_user_review.

continue_user_input_for_lexing_and_user_review(String) :-
    string_length(String,0).

read_user_input_for_lexing_and_user_review/0 允许用户在终端输入字符串进行词法分析和查看标记。

read_file_for_lexing_and_user_review/1 读取文件进行词法分析,并逐行查看每行的标记。

read_file_for_lexing_and_performance/2 读取文件进行词法分析,限制了要分析的行数。这用于收集基本性能统计数据以衡量效率。应与 time/1 一起使用。


有趣的内容:Prolog中的选择点和Redo操作 - 索引如何影响SWI-Prolog跟踪器。 - Guy Coder
1
有趣的内容:Prolog DCG语法规则中的堆栈溢出:如何高效或惰性地处理大型列表 这是一个关于使用DCG进行解析的问答,答案中有一个关于利用索引的部分。 - Guy Coder
感兴趣的内容:GitHub SWI-Prolog swipl-devel/src/Tests/core/test_dcg.pl - Guy Coder
感兴趣的内容:GitHub SWI-Prolog swipl-devel/library/statistics.pl time/1 - time/1 的源代码。可以扩展此代码以显示更多统计信息。 - Guy Coder
感兴趣的内容:GitHub SWI-Prolog swipl-devel/library/console_input.pl - 另一个使用带索引的DCG的示例。 - Guy Coder
显示剩余19条评论
2个回答

3

其中一个意思是这是愚蠢的代码:

token(T) -->
    ( "1", !, { T = one }
    ; "2", !, { T = two }
    ; "3", !, { T = three }
    )

这是更为简洁的代码:
token(T) --> one_two_three(T).

one_two_three(one) --> "1".
one_two_three(two) --> "2".
one_two_three(three) --> "3".

但还不太好。也许可以更好:
token(T) --> [X], { one_two_three(X, T) }.

one_two_three(0'1, one).
one_two_three(0'2, two).
one_two_three(0'3, three).

最后一个例子看起来也有些荒谬,但请记住,现在您已经在第一个参数上进行了索引。您只需读取一次,没有选择点,没有回溯。

但如果您真的想知道如何编写高效代码,就需要测量时间和空间的使用情况。您进行过测量吗?

但如果您真的想知道如何修复问题,可能需要阅读《Prolog工艺》这本书,我并不理解其中所有内容,但我记得它有关于DCG的大部分章节。

但如果您真的想解析这种格式的大文件,可以在其他语言中查找现有库,这可能比最快的Prolog更快。


2
解决方案:
你需要替换以下内容:

Original Answer:

lexer(Tokens) -->
   white_space,
   (
      (  ":",       !, { Token = tokColon }
      ;  "(",       !, { Token = tokLParen }
      ;  ")",       !, { Token = tokRParen }
      ;  "{",       !, { Token = tokLMusta}
      ;  "}",       !, { Token = tokRMusta}
      ;  "\\",      !, { Token = tokSlash}
      ;  "->",      !, { Token = tokImpl}
      ;  "+",       !, { Token = tokPlus }
      ;  "-",       !, { Token = tokMinus }
      ;  "*",       !, { Token = tokTimes }
      ;  "=",       !, { Token = tokEqual }
      ;  "<",       !, { Token = tokLt }
      ;  ">",       !, { Token = tokGt }
      ;  "_",       !, { Token = tokUnderscore }
      ;  ".",       !, { Token = tokPeriod }
      ;  "/",       !, { Token = tokForwardSlash }
      ;  ",",       !, { Token = tokComma }
      ;  ";",       !, { Token = tokSemicolon }
      ;  digit(D),  !,
            number(D, N),
            { Token = tokNumber(N) }
      ;  letter(L), !, identifier(L, Id),
            {  member((Id, Token), [ (div, tokDiv),
                                     (mod, tokMod),
                                     (where, tokWhere)]),
               !
            ;  Token = tokVar(Id)
            }
      ;  [_],
            { Token = tokUnknown }
      ),
      !,
      { Tokens = [Token | TokList] },
      lexer(TokList)
   ;  [],
         { Tokens = [] }
   ).

使用

lexer(Tokens) -->
   white_space,
   (
      (
         op_token(Token), ! % replace ;/2 long chain searched blindly with call to new predicate op_token//1 which clauses have indexed access by first arg in Prolog standard way
      ;
         digit(D),  !, number(D, N),
         { Token = tokNumber(N) }
      ;  letter(L), !, identifier(L, Id),
         {  member((Id, Token), [ (div, tokDiv),
                                 (mod, tokMod),
                                 (where, tokWhere)]),
            !
      ;  Token = tokVar(Id)
         }
      ;  [_],
         { Token = tokUnknown }
      ),
      !,
      { Tokens = [Token | TokList] },
      lexer(TokList)
   ;
      [],
      { Tokens = [] }
   ).

%%%
op_token(tokColon)      --> ";".
op_token(tokLParen)     --> "(".
op_token(tokRParen)     --> ")".
op_token(tokLMusta)     --> "{".
op_token(tokRMusta)     --> "}".
op_token(tokBackSlash)  --> "\\".
op_token(tokImpl)       --> "->".
op_token(tokPlus)       --> "+".
op_token(tokMinus)      --> "-".
op_token(tokTimes)      --> "*".
op_token(tokEqual)      --> "=".
op_token(tokLt)         --> "<".
op_token(tokGt)         --> ">".
op_token(tokUnderscore) --> "_".
op_token(tokPeriod)     --> ".".
op_token(tokSlash)      --> "/".
op_token(tokComma)      --> ",".
op_token(tokSemicolon)  --> ";".

我使用问题中发布的示例数据运行了一个测试,将数据中的每一行转换为字符代码,并将其存储到列表中。然后,对于列表中的每个项目调用词法分析器,并重复执行该测试10000次。将数据加载到列表并在time/1之前转换为字符代码的原因是为了避免这些过程对结果产生影响。每个运行都重复5次以获得数据的一致性。
在以下所有运行中,对于所有不同版本,词法分析器都被扩展以涵盖所有7位ASCII字符,这显着增加了特殊字符的情况数。
以下运行所使用的Prolog版本为SWI-Prolog 8.0。
对于问题中的版本。
Version: 1

:- set_prolog_flag(double_quotes,chars).

% 694,080,002 inferences, 151.141 CPU in 151.394 seconds (100% CPU, 4592280 Lips)
% 694,080,001 inferences, 150.813 CPU in 151.059 seconds (100% CPU, 4602271 Lips)
% 694,080,001 inferences, 152.063 CPU in 152.326 seconds (100% CPU, 4564439 Lips)
% 694,080,001 inferences, 151.141 CPU in 151.334 seconds (100% CPU, 4592280 Lips)
% 694,080,001 inferences, 151.875 CPU in 152.139 seconds (100% CPU, 4570074 Lips)

"最初的回答"指的是在本回答中发布的版本。
Version: 2

:- set_prolog_flag(double_quotes,chars).

% 773,260,002 inferences, 77.469 CPU in 77.543 seconds (100% CPU, 9981573 Lips)
% 773,260,001 inferences, 77.344 CPU in 77.560 seconds (100% CPU, 9997705 Lips)
% 773,260,001 inferences, 77.406 CPU in 77.629 seconds (100% CPU, 9989633 Lips)
% 773,260,001 inferences, 77.891 CPU in 77.967 seconds (100% CPU, 9927511 Lips)
% 773,260,001 inferences, 78.422 CPU in 78.644 seconds (100% CPU, 9860259 Lips)

版本2通过使用来自版本1的索引,实现了显著的改进。

在对代码进行进一步研究时,查看 op_token(它是DCG,并且有两个隐藏变量用于隐式传递状态表示),使用listing/1 显示:

op_token(tokUnderscore,['_'|A], A).

注意,第一个参数不是要搜索的字符,在这个最初的回答中,索引代码被写成了这样。
c_digit(0'0,0).

第一个参数是要搜索的字符,第二个参数是结果。

所以将此更改为

op_token(Token), !

to this

[S], { special_character_indexed(S,Token) }

with indexed clauses as

special_character_indexed( ';' ,tokSemicolon).


Version: 3

:- set_prolog_flag(double_quotes,chars).

% 765,800,002 inferences, 74.125 CPU in 74.348 seconds (100% CPU, 10331197 Lips)
% 765,800,001 inferences, 74.766 CPU in 74.958 seconds (100% CPU, 10242675 Lips)
% 765,800,001 inferences, 74.734 CPU in 74.943 seconds (100% CPU, 10246958 Lips)
% 765,800,001 inferences, 74.828 CPU in 75.036 seconds (100% CPU, 10234120 Lips)
% 765,800,001 inferences, 74.547 CPU in 74.625 seconds (100% CPU, 10272731 Lips)

版本 3 的结果略微优于版本 2,而且一贯如此。

最后,按照 AntonDanilov 在评论中的建议将 double_quotes 标志更改为 atom

注:Original Answer 翻译成“最初的回答”。
Version: 4

:- set_prolog_flag(double_quotes,atom).

% 765,800,003 inferences, 84.234 CPU in 84.539 seconds (100% CPU, 9091300 Lips)
% 765,800,001 inferences, 74.797 CPU in 74.930 seconds (100% CPU, 10238396 Lips)
% 765,800,001 inferences, 75.125 CPU in 75.303 seconds (100% CPU, 10193677 Lips)
% 765,800,001 inferences, 75.078 CPU in 75.218 seconds (100% CPU, 10200042 Lips)
% 765,800,001 inferences, 75.031 CPU in 75.281 seconds (100% CPU, 10206414 Lips)

版本4几乎与版本3相同。

仅从CPU数字来看,使用索引更快,例如(版本:1)151.875与(版本:3)74.547

注:此处的“索引”指使用数据结构中的索引加速查询速度。

最初的回答。


1
我不知道在注释“% replace OR sky-scrapper with call to new predicate”中的“sky-scrapper”是什么意思,所以我将其作为一个单独的问题进行了询问。 - Guy Coder
1
摩天大楼是多层极高的建筑物。在美国的大城市中有很多这样的建筑物 =D - Anton Danilov
1
看一下第一个代码片段,它不像摩天大楼、曼哈顿、纽约等吗? - Anton Danilov
我来解释一下:据我所知,排除链只能通过盲目搜索来遍历。我将它们替换为由第一个参数索引的子句链。 - Anton Danilov
1
正确拼写是“skyscraper”(在这种情况下,长“a”发音中的一个'p'很重要,而且它只是一个词)。但我从未听说过这个术语用于任何代码结构的参考。你有参考资料吗? - lurker
显示剩余15条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接