上下文
我有一个代码/文本编辑器,而不是我想要优化。目前,该程序的瓶颈是语言解析器,而不是扫描所有关键字(不止一个,但它们的编写方式基本相同)。
在我的计算机上,编辑器会延迟1,000,000
行代码周围的文件。在像Raspberry Pi这样的低端计算机上,延迟开始的时间要快得多(我不记得确切,但我认为是10,000
行代码)。虽然我从来没有看到大于1,000,000
行代码的文档,但我确信它们在那里,我希望我的程序能够编辑它们。
问题:
这引出了一个问题:在大型动态字符串中扫描单词列表的最快方法是什么?
以下是可能影响算法设计的一些信息:
瓶颈溶液
这是(大致)我正在使用的解析字符串的方法:
// this is just an example, not an excerpt
// I haven't compiled this, I'm just writing it to
// illustrate how I'm currently parsing strings
struct tokens * scantokens (char * string, char ** tokens, int tcount){
int result = 0;
struct tokens * tks = tokens_init ();
for (int i = 0; string[i]; i++){
// qualifiers for C are: a-z, A-Z, 0-9, and underscore
// if it isn't a qualifier, skip it
while (isnotqualifier (string[i])) i++;
for (int j = 0; j < tcount; j++){
// returns 0 for no match
// returns the length of the keyword if they match
result = string_compare (&string[i], tokens[j]);
if (result > 0){ // if the string matches
token_push (tks, i, i + result); // add the token
// token_push (data_struct, where_it_begins, where_it_ends)
break;
}
}
if (result > 0){
i += result;
} else {
// skip to the next non-qualifier
// then skip to the beginning of the next qualifier
/* ie, go from:
'some_id + sizeof (int)'
^
to here:
'some_id + sizeof (int)'
^
*/
}
}
if (!tks->len){
free (tks);
return 0;
} else return tks;
}
可能的解决方案:
上下文解决方案:
我正在考虑以下事项:
扫描一次大字符串,并在每次有用户输入时添加一个评估/调整标记标记的功能(而不是一遍又一遍地重新扫描整个文档)。我希望这将解决瓶颈,因为涉及的更少解析。但是,它并没有完全修复程序,因为初始扫描可能仍然需要
优化令牌扫描算法(见下文)
我也考虑过,但拒绝了这些优化:
架构解决方案:
使用汇编语言,解析这些字符串的更快捷方法是将字符加载到寄存器中,并一次比较它们4
或8
个字节。还有一些必须考虑的额外措施和预防措施,例如:
s
,其中s % word-size == 0
为防止阅读违规但是这些问题看起来很容易修复。唯一的问题(除了通常用汇编语言编写的问题)是它不是一个算法解决方案,而是一个硬件解决方案。
算法解决方案:
到目前为止,我已经考虑让程序重新排列关键字列表,使二进制搜索算法更加可能。
我考虑过重新安排它们的一种方法是切换关键字列表的尺寸。以下是C
中的示例:
// some keywords for the C language
auto // keywords[0]
break // keywords[1]
case char const continue // keywords[2], keywords[3], keywords[4]
default do double
else enum extern
float for
goto
if int
long
register return
short signed sizeof static struct switch
typedef
union unsigned
void volatile
while
/* keywords[i] refers to the i-th keyword in the list
*
*/
切换二维数组的尺寸会使它看起来像这样:
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
-----------------------------------------------------------------
1 | a b c c c c d d d e e e f f g i i l r r s s s s s s t u u v v w
2 | u r a h o o e o o l n x l o o f n o e e h i i t t w y n n o o h
3 | t e s a n n f u s u t o r t t n g t o g z a r i p i s i l i
4 | o a e r s t a b e m e a o g i u r n e t u t e o i d a l
5 | k i u l r t s r t e o i c c d n g t e
6 | n l e n t n d f c t h e n i
7 | u t e f e l
8 | e r d e
// note that, now, keywords[0] refers to the string "abccccdddeeefffiilrr"
这使得使用二进制搜索算法(甚至是普通的暴力算法)更有效。但它只是每个关键字中第一个字符的单词,之后什么都不能被视为“排序”。这可能对像编程语言这样的小词组有帮助,但对于更大的单词集(例如整个英语语言)来说这是不够的。
是否还有很多工作可以改进这个算法?
是否有其他方法可以提高性能?
备注:
来自SO的This question对我没有帮助。 Boyer-Moore-Horspool算法(据我所知)是一种在字符串中查找子字符串的算法。由于我正在解析多个字符串,我认为还有更多的优化空间。
答案 0 :(得分:3)
Aho-Corasick是一个非常酷的算法,但它不适合关键字匹配,因为关键字匹配是对齐;您不能重叠匹配,因为您只匹配完整的标识符。
对于基本标识符查找,您只需要在关键字中构建trie(请参阅下面的注释)。
您的基本算法很好:找到标识符的开头,然后查看它是否是关键字。改善这两个部分非常重要。除非您需要处理多字节字符,否则查找关键字开头的最快方法是使用256条目表,每个可能字符都有一个条目。有三种可能性:
该字符不能出现在标识符中。 (继续扫描)
字符可以出现在标识符中,但没有关键字以字符开头。 (略过标识符)
该角色可以启动关键字。 (开始行走特里;如果行走不能继续,则跳过标识符。如果行走找到关键字并且下一个字符不能在标识符中,则跳过标识符的其余部分;如果它可以在标识符中,请尝试继续如果可能的话,走路。)
实际上,第2步和第3步非常接近,你真的不需要特殊的逻辑。
上述算法存在一些不精确性,因为在很多情况下你会发现看起来像标识符但在语法上不可能的东西。最常见的情况是注释和引用的字符串,但大多数语言都有其他可能性。例如,在C中,您可以使用十六进制浮点数;虽然只能从[a-f]
构建C关键字,但用户提供的单词可能是:
0x1.deadbeef
另一方面,C ++允许用户定义的数字后缀,如果用户将它们添加到列表中,您可能希望将其识别为关键字:
274_myType
除了上述所有内容之外,每次用户在编辑器中键入字符时解析一百万行代码实际上是不切实际的。您需要开发一些缓存标记化的方法,最简单和最常见的方法是按输入行缓存。将输入行保持在链接列表中,并且每个输入行还在行的开头记录tokenizer状态(即,您是否使用多行引用字符串;多行注释或其他特殊注释词汇状态)。除了一些非常奇怪的语言之外,编辑不会影响编辑前行的标记结构,因此对于任何编辑,您只需要重新标记已编辑的行以及其标记化器状态已更改的任何后续行。 (请注意在多行字符串的情况下工作太猛烈:因为用户输入未终止的引号,它会产生大量的视觉噪音来翻转整个显示。)
注意:对于少量(数百)个关键字,完整的trie并没有真正占用那么多空间,但在某些时候你需要处理膨胀的分支。一个非常合理的数据结构,如果你对数据布局要小心,可以很好地执行,是一个ternary search tree(尽管我称之为三元搜索系列。)
答案 1 :(得分:2)
很难打败这段代码。
假设您的关键字是“a”,“ax”和“foo”。
获取关键字列表,对其进行排序,并将其输入到打印出如下代码的程序中:
switch(pc[0]){
break; case 'a':{
if (0){
} else if (strcmp(pc, "a")==0 && !alphanum(pc[1])){
// push "a"
pc += 1;
} else if (strcmp(pc, "ax")==0 && !alphanum(pc[2])){
// push "ax"
pc += 2;
}
}
break; case 'f':{
if (0){
} else if (strcmp(pc, "foo")==0 && !alphanum(pc[3])){
// push "foo"
pc += 3;
}
// etc. etc.
}
// etc. etc.
}
然后,如果您没有看到关键字,只需递增pc
然后重试。
关键是,通过调度第一个字符,您可以快速进入以该字符开头的关键字子集。
您甚至可能想要进行两级调度。
当然,和往常一样,拿一些堆栈样本来查看正在使用的时间。 无论如何,如果你有数据结构类,你会发现你花费了大部分时间,所以要把它保持在最低限度(把风投入风中)。
答案 2 :(得分:1)
最快的方法是为单词集构建一个有限状态机。使用Lex构建FSM。
答案 3 :(得分:0)
这个问题的最佳算法可能是Aho-Corasick。已经存在C实现,例如,
http://sourceforge.net/projects/multifast/