【发布时间】:2013-08-23 06:09:12
【问题描述】:
上下文:
我有一个代码/文本编辑器,而不是我想要优化的。目前,程序的瓶颈是语言解析器没有扫描所有关键字(不止一个,但它们的写法大致相同)。
在我的计算机上,编辑器延迟了 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"
这使得使用二分搜索算法(甚至是简单的蛮力算法)更加高效。但它只是每个关键字中第一个字符的单词,之后什么都不能被认为是“排序的”。这可能有助于像编程语言这样的小词集,但对于更大的词集(比如整个英语)来说,这还不够。
还有什么可以改进这个算法的吗?
还有其他方法可以提高性能吗?
注意事项:
This question 来自 SO 对我没有帮助。 Boyer-Moore-Horspool 算法(据我了解)是一种用于在字符串中查找子字符串的算法。由于我正在解析 多个 字符串,我认为还有更多的优化空间。
【问题讨论】:
-
如果你想快速完成它,那么你不要循环使用字符串比较和字符串列表,而是基于所有字符串的每个字符构建一个有限状态机,当找到关键字。 Lex 实用程序会执行此操作。
标签: c parsing string-parsing