我应该避免“|”在柔性模式?

时间:2014-11-14 03:22:14

标签: regex flex-lexer

我听说" |"运算符会减慢正则表达式匹配,例如,在Perl中它似乎也是如此。

当我使用Flex词法分析器等工具构建扫描仪时,我是否需要担心?

1 个答案:

答案 0 :(得分:7)

绝对不是。 Flex(就像它所基于的lex词法分析器,以及大多数其他类似的编译器构造工具)将扫描器中的所有正则表达式编译成单个确定性有限状态自动机(DFA)。在扫描词法标记时,DFA永远不会备份,组件正则表达式的复杂性和它们使用的精确运算符都没有任何区别。

这与Perl的正则表达式匹配方法完全不同。 Perl(至少在某些情况下)将一次一个地在一个交替表达式中探索可能的子模式,结果是可以注意到显着的性能损失。 @sln在this answer中构建的Perl基准证明了这种效果。

为了证明Flex生成的解析器不是这样,我在Flex中构建了一个非常相似的基准测试。两个Flex输入文件:

(使用 | ):

%{
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <time.h>

  static int echo_match = 0;
  static const char* regex="'''([^']|['][^']|[']['][^'])*'''";
%}

%option noyywrap noinput nounput nodefault

%%

[^'\n]+   { }
\n        { }
'[^'\n]*' {
            if (echo_match) printf("%s\n", yytext);
          }
'''([^']|['][^']|[']['][^'])*''' {
            if (echo_match) printf("%s\n", yytext);
          }
'         { fputs("Unmatched quote\n", stderr); }

没有 | 是相同的,除了正则表达式模式(在两个地方):

'''[^']*(?:[']{1,2}[^']+)*'''

然后我使用这个main来测试每个扫描仪:

static const char* dataset[] = {
  "'''Set 1 - this\nis another\nmultiline\nstring'''",
  "'''Set 2 - this\nis' another\nmul'tiline\nst''ring'''"
};

#define COUNTOF(ARY) (sizeof(ARY)/sizeof(ARY[0]))

int main(int argc, char** argv) {
  int reps = 500000;
  printf("-----------------------\nUsing regex: %s\n", regex);
  for (unsigned d = 0; d < COUNTOF(dataset); ++d) {
    echo_match = 1;
    yy_scan_string(dataset[d]);
    yylex();
    yy_delete_buffer(YY_CURRENT_BUFFER);
    echo_match = 0;
    struct timespec before, after;
    if (clock_gettime(CLOCK_REALTIME, &before)) perror("gettime");
    for (int r = 0; r < reps; ++r) {
      yy_scan_string(dataset[d]);
      yylex();
      yy_delete_buffer(YY_CURRENT_BUFFER);
    }
    if (clock_gettime(CLOCK_REALTIME, &after)) perror("gettime");
    unsigned long elapsed =
        (after.tv_sec - before.tv_sec) * 1000000
        + (after.tv_nsec - before.tv_nsec) / 1000;
    printf("Wall-clock: %ld microseconds\n", elapsed);
  }
  return 0;
}

两组基准的随机结果(来自每组10次运行):

$ tail -n+$((1+12*(RAND/10))) threeq.log | head -n12
-----------------------
Using regex: '''([^']|['][^']|[']['][^'])*'''
'''Set 1 - this
is another
multiline
string'''
Wall-clock: 243410 microseconds
'''Set 2 - this
is' another
mul'tiline
st''ring'''
Wall-clock: 233519 microseconds

$ tail -n+$((1+12*(RAND/10))) threeq2.log | head -n12
-----------------------
Using regex: '''[^']*(?:[']{1,2}[^']+)*'''
'''Set 1 - this
is another
multiline
string'''
Wall-clock: 246842 microseconds
'''Set 2 - this
is' another
mul'tiline
st''ring'''
Wall-clock: 242191 microseconds

在某些情况下,Flex在识别出令牌后需要重新扫描输入,但这些与交替运算符 | 无关。由于Flex始终尝试查找最长匹配,因此即使在识别了令牌之后,它也可能需要继续扫描,以防该令牌是另一个可能令牌的前缀。如果证明较长的令牌不匹配,则Flex扫描程序将备份到匹配的最长令牌的末尾,其余字符将在下一个令牌中重新扫描。

例如,在C中,....都是有效令牌,但..不是。如果输入中出现..,那么Flex构建的扫描程序必须在第一个之后继续扫描,以查看它是否匹配...。但是,当第三个字符证明不是时,它必须返回.标记,然后重新扫描第二个。 (这几乎总是语法错误,所以问题不是很严重。)

Flex构建的扫描仪需要重新扫描的另一种情况是使用 / (尾随上下文)运算符。由于尾随上下文实际上不是返回令牌的一部分,因此必须将其重新扫描为下一个令牌的一部分。

这些情况都不常见,重新扫描的序列通常很短(通常是一个字符),因此性能影响微不足道。但是,如果您真的担心这一点,可以向Flex提供--backup选项,要求它准备备份状态报告。如果您设法消除所有备份状态,您将获得较小的性能提升,但大多数情况下这都属于过早优化。