Flex的正则表达式匹配速度与手工代码的比较

  flex是一个词法分析器生成器,它是编译器和解释器编程人员的常用工具之一。flex的程序主要由一系列带有指令(称为动作代码)的正则表达式组成。在匹配输入时,flex会将所有的正则表达式翻译成确定性有穷自动机,这使得flex等词法分析器生成器生成的词法分析器匹配输入模式的效率非常高。当然,有人指责flex不够灵活,功能有限,很多问题都无法解决,比如Javascript、C++等语言中二义性的问题,实际上很多程序(比如Python的解释器)的词法分析器都是用的手工代码而不是flex自动生成的。这些都不在这篇文章的讨论范围内,我主要想通过对flex和手工写的C代码编译的一个词数统计程序(word
counter)来非常粗略地评价一下flex匹配正则表达式的速度如何。

  测试方法:分别运行用flex生的C代码和手工C代码编译的程序,设置三种不同大小的文件,用Shell的time指令来测试两个程序运行所用的时间。

  下面上源码,先是flex源码 flexwc.l:

 1 /*用flex实现的一个词数统计程序。   2     flexwc.l   3 */   4    5 %{   6 /*定义全局变量,分别统计字符数,单词数和行数*/   7 int chars=0;   8 int words=0;   9 int lines=0;  10 %}  11 %%  12 [a-zA-Z]+    { ++words; chars+=strlen(yytext);}/*正则表达式匹配任意单词,用字符串函数统计输入流中的当前字符数*/  13 \n            { ++chars; ++lines; }/*遇到换行符,新的一行开始*/  14 .            {++chars;}/*其它字符*/              15 %%  16   17 main(int argc,char **argv)  18 {  19     yylex();/*调用flex生成的例程*/  20     printf("%8d%8d%8d\n",lines,words,chars);/*打印行号,单词数,字符数*/  21 }

  编译指令:

1 $flex flexwc.l  2 $gcc -o flexwc -O3 flexwc

  下面是C代码,manwc.c:

 1 /*手工C代码的词数统计程序   2     manwc.c   3 */   4    5 #include <stdio.h>   6 #include <ctype.h>/*使用isalpha()*/   7    8 /*全局变量,分别记录文件的字符数,行数和单词数*/   9 int i_char=0,i_line=0,i_word=0;  10   11 int main(void)  12 {  13     int inword=0;  14     /*当inword==1时说明正在处理一个单词的内部(也就是一个单词还没结束)  15     否则意味一个单词的结束    */  16     char ch;  17     while ((ch=getchar())!=EOF)/*文件未结束*/  18     {  19         ++i_char;/*增加字符数*/  20         if ('\n'==ch)/*行数*/  21             ++i_line;  22         if (isalpha(ch) && !inword)/*一个单词的开始*/  23         {  24             inword=1;  25             ++i_word;  26         }  27         if (!isalpha(ch) && inword)/*一个单词的结束*/  28         {  29             inword=0;      30         }  31     }  32     printf("%8d%8d%8d\n",i_line,i_word,i_char);/*打印文件行数、单词数、字符数*/  33     return 0;  34 }

  编译指令:

1 $gcc -o manwc -O3 manwc.c

  下面进行第一次测试,指令及结果如下:

 1 $time ./autowc < foo.txt   2        4       0       7   3    4 real    0m0.014s   5 user    0m0.000s   6 sys    0m0.000s   7    8 $ time ./manwc < foo.txt   9        4       0       7  10   11 real    0m0.024s  12 user    0m0.000s  13 sys    0m0.000s

注意,因为flex的默认输入流和C代码的输入流都是stdin,所以这里用到了输入流重定向'<‘。

  下面进行第二次测试,指令及结果如下:

 1 $time ./autowc < lex.yy.c    2     1823    6705   45887   3    4 real    0m0.008s   5 user    0m0.004s   6 sys    0m0.000s   7    8 $ time ./manwc < lex.yy.c    9     1823    6705   45887  10   11 real    0m0.008s  12 user    0m0.004s  13 sys    0m0.000s

  下面进行第三次测试,指令及结果如下:

 1 $time ./autowc < MAINTAINERS    2     9721   46364  269584   3    4 real    0m0.013s   5 user    0m0.012s   6 sys    0m0.000s   7    8 $ time ./manwc < MAINTAINERS    9     9721   46364  269584  10   11 real    0m0.019s  12 user    0m0.020s  13 sys    0m0.000s

   (此结果受机器影响较大,仅作为参考)

  经过三次规模从小到大的测试,可以看到用flex生成的词法分析器匹配正则表达式的速度几乎总是比手工C代码要快。可以预见,当模式变得更为复杂时,flex生成的代码的执行速度将会比纯手工C代码的效率高更多。这是由于flex处理正则表达式的内部格式(也就是确定性有穷自动机)使得匹配正则表达式时几乎与问题规模无关(也就是说并不是模式越复杂匹配的时间就会越久,但是也会有例外),而用手工的C代码处理此类问题时,总是倾向于将字符流一步步的进行分析,比如分析C语言中的’/’符号,当读入第一个’/’时,并不能确定到底是什么(有二义性),只有继续读接下来的一个字符:如果是一个数字,那’/’就是除法运算符;如果是‘/’那就是一个行注释,可以直接忽略本行余下的内容了;如果是’*’那还要判断什么位置出现了”*/”,然后忽略中间的注释,如果找不到匹配的”*/”,就需要报错。flex允许你对这三种方式定义明确的正则表达式:”/”,“//”,“/*”(最后一种匹配/**/注释的情况还要用到起始状态的知识,这里就不介绍了)。总之记住一点:一次匹配一大段几乎总是比每次匹配一个字符、匹配多次要快,当然也有例外。

  总结:flex作为一种格式化文件处理工具,用它生成的代码不仅可以用于编译器解释器的编程人员的词法分析器开发,还可用于所有需要进行分析的格式化文件,比如快速查找文件中的某一特定格式、自动排版、代码自动缩进、语法着色等等,一切就看你能做什么了!

 


发表评论

电子邮件地址不会被公开。 必填项已用*标注