flex学习 - 扫描器性能的考量
flex的主要设计目标是产生高性能的扫描器。它经过优化,可以很好的处理大量规则。除了上面列出的表压缩’-C’选项对扫描器的速度有影响之外,还有许多选项/操作会降低性能。这些是从消耗最高的到最小的:
REJECT
arbitrary trailing context
pattern sets that require backing up
%option yylineno
%array
%option interactive
%option always-interactive
^ beginning-of-line operator
yymore()
前两个耗费资源很多,后两个耗费最小。还要注意的是,unput()是作为一个函数调用实现的,可能会做很多工作,而yyless()是一个相当简单的宏。因此,如果您只是将扫描的一些多余文本放回原处,请使用yyless()。
当性能很重要时,应该不惜一切代价避免REJECT。这是一个性能消耗严重的选择。
有一种情况下,%option yylineno可能很消耗性能。这是当您的模式匹配可能包含换行字符的长标记时。对于不可能匹配换行符的规则没有性能损失,因为flex不需要检查它们是否有换行符。一般来说,您应该避免像[f]+这样的规则,它们匹配非常长的标记,包括换行符,并且可能匹配整个文件。更好的方法是将[f]+分成两个规则:
%option yylineno
%%
[^f\n]+
\n+
上述扫描程序不会导致性能的损失。
清除备份是一件很麻烦的事情,对于一个复杂的扫描器来说,这往往是一项巨大的工作。原则上首先使用’-b’标志来生成lex.backup文件。例如,在输入上:
%%
foo return TOK_KEYWORD;
foobar return TOK_KEYWORD;
文件看起来像下面一样:
State #6 is non-accepting -
associated rule line numbers:
2 3
out-transitions: [ o ]
jam-transitions: EOF [ \001-n p-\177 ]
State #8 is non-accepting -
associated rule line numbers:
3
out-transitions: [ a ]
jam-transitions: EOF [ \001-` b-\177 ]
State #9 is non-accepting -
associated rule line numbers:
3
out-transitions: [ r ]
jam-transitions: EOF [ \001-q s-\177 ]
Compressed tables always back up.
前几行告诉我们有一种扫描器状态,在这种状态下,扫描器可以在’o’上进行转换,但不能在任何其它字符上进行转换,并且在这种状态下,当前扫描的文本不匹配任何规则。当尝试匹配输入文件中的第2行和第3行找到规则时,就会出现这种状态。如果扫描器处于该状态,然后读取除’o’以外的内容,则必须备份以找到匹配的规则。有点挠头,可以看出这一定是它看到’fo’时的状态。发生这种情况时,如果看到的不是’o’,则扫描器将不得不备份以简单的匹配’f’(根据默认规则)。
关于状态#8的注释表明,当’foob’被扫描时存在问题。实际上,对于除’a’以外的任何字符,扫描器都必须备份以接受’foo’。类似的,状态#9的注释关注的是’fooba’被扫描后没有’r’。
最后的注释提醒我们,除非我们使用’-Cf’或’-CF’,否则从规则中删除备份是没有意义的,因为这样做对压缩扫描器没有性能提高。
删除备份的方法是添加’error’规则:
%%
foo return TOK_KEYWORD;
foobar return TOK_KEYWORD;
fooba |
foob |
fo {
/* false alarm, not really a keyword */
return TOK_ID;
}
消除关键字列表中的备份也可以使用’catch-all’规则来完成:
%%
foo return TOK_KEYWORD;
foobar return TOK_KEYWORD;
[a-z]+ return TOK_ID;
在适当的情况下,这通常是最好的解决方案。
备份消息往往是级联的。由于一套复杂的规则,收到数百条消息并不罕见。但是,如果可以破译它们,通常只需要十几个规则就可以消除备份(尽管很容易犯错误,并且错误规则意外的匹配有效的标记。未来可能的flex特性是自动添加规则以消除备份)。
重要的是,只有在消除所有备份实例的情况下,才能获得消除备份的好处。只留下一个意味着你什么也得不到。
可变尾随上下文(前导和尾随部分都没有固定长度)几乎会导致与REJECT相同的性能损失(即实质性损失)。因此当可能的规则像:
%%
mouse|rat/(cat|dog) run();
更好的写法是:
%%
mouse/cat|dog run();
rat/cat|dog run();
或者可以是:
%%
mouse|rat/cat run();
mouse|rat/dog run();
请注意,这里的特殊’|’操作不会提供任何节省,甚至会使情况变得更糟(请查阅限制)。
用户可以提高扫描器性能的另一个方面(也是更容易实现的一个方面)源于这样一个事实,即匹配的标记越长,扫描器运行得越快。这是因为对于长标记,大多数输入字符的处理都是在(短)内部扫描循环中进行的,并且通常不需要为操作设置扫面环境(如,yytext)的额外工作。回想一下C注释的扫描器:
%x comment
%%
int line_num = 1;
"/*" BEGIN(comment);
<comment>[^*\n]*
<comment>"*"+[^*/\n]*
<comment>\n ++line_num;
<comment>"*"+"/" BEGIN(INITIAL);
这可以通过这样写来加速:
%x comment
%%
int line_num = 1;
"/*" BEGIN(comment);
<comment>[^*\n]*
<comment>[^*\n]*\n ++line_num;
<comment>"*"+[^*/\n]*
<comment>"*"+[^*/\n]*\n ++line_num;
<comment>"*"+"/" BEGIN(INITIAL);
现在,每个换行符不再需要处理另一个操作,而是将识别换行符分布在其它规则上,以尽可能长的保留匹配的文本。请注意,添加规则并不会减慢扫描程序的速度!扫描器的速度独立于规则的数量或(对本节开始时给出的考量进行模型计算)与’’和’|’等操作符相关的规则的复杂程序。
关于加速扫描器的最后一个例子:假设您希望扫描一个包含标识符和关键字的文件,每行一个,不包括其它无关的字符,并识别所有关键字。自然的第一种方法是:
%%
asm |
auto |
break |
... etc ...
volatile |
while / it's a keyword */
.|\n /* it's not a keyword */
为了消除回溯,引入一个包罗万象的规则:
%%
asm |
auto |
break |
... etc ...
volatile |
while /* it's a keyword */
[a-z]+ |
.|\n /* it's not a keyword */
现在,如果保证每行只有一个单词,那么我们可以通过合并换行符和其它标记的识别来减少总匹配次数的一半:
%%
asm\n |
auto\n |
break\n |
... etc ...
volatile\n |
while\n /* it's a keyword */
[a-z]+\n |
.|\n /* it's not a keyword */
这里必须要小心,因为我们现在重新引入了备份到扫描器中。特别是,虽然我们知道输入流中除了字母或换行符之外不会有任何字符,但是flex无法弄清楚这一点,并且当它扫描了像’auto’这样的标记,然后下一个字符不是换行符或字母时,它将计划可能需要备份。以前,它只需要匹配’auto’规则就可以了,但现在它没有’auto’规则,只有一个’auto\n’规则。为了消除备份的可能性,我们可以复制所有规则,但不包括最后的换行符,或者,由于我们从未期望遇到这样的输入,因此不知道它时如何分类的,我们可以引入一个更多的包罗万象的规则,这个规则不包括换行符:
%%
asm\n |
auto\n |
break\n |
... etc ...
volatile\n |
while\n /* it's a keyword */
[a-z]+\n |
[a-z]+ |
.|\n /* it's not a keyword */
使用’-Cf’编译,这是最快的一个flex扫描器可以去这个特定的问题。
最后注意:flex在匹配NULs时很慢,特别是当一个标记包含多个NULs时。如果预知文本经常包含NULs,那么最好编写匹配短文本的规则。
关于性能的另一个最后注意事项:正如在匹配中提到的,动态调整yytext的大小以适应巨大的标记是一个缓慢的过程,因为它目前需要从一开始重新扫描(巨大的)标记。因此,如果性能是至关重要的,您应该尝试匹配’大’数量的文本,而不是’巨大’数量的文本,其中两者之间的界限是每个标记大约8K个字符。
浙公网安备 33010602011771号