2013-08-21 49 views
5

語境:優化一句話解析器

我有一個代碼/文本編輯器比我試圖優化。目前,該程序的瓶頸是語言解析器,而不是掃描所有關鍵字(不止一個,但它們通常寫成相同)。

在我的電腦上,編輯器延遲了1,000,000代碼行的文件。在像Raspberry Pi這樣的低端計算機上,延遲開始的時間要早​​得多(我不記得確實如此,但我想到的代碼是10,000)。雖然我從來沒有見過比1,000,000代碼行更大的文件,但我確定它們在那裏,我希望我的程序能夠編輯它們。

問:

這使我的問題是:什麼是掃描大,動態字符串中的單詞的列表,最快的方法?

這裏的一些信息可能會影響算法的設計:

  1. 關鍵字
  2. 預選賽字符允許是關鍵字的一部分,(我稱他們預選賽)
  3. 大串

瓶頸溶液:

這是(大約)我目前使用解析字符串的方法:

// 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; 
} 

可能的解決方案:


語境解決方案:

我正在考慮以下內容:

  • 掃描大字符串一次,並添加一個函數來評估/調整標記標記,每次有用戶輸入時(而不是一遍又一遍地重新掃描整個文檔)。我期望這將解決瓶頸問題,因爲有遠遠少於解析涉及。但是,它並沒有完全修復程序,因爲初始掃描可能仍然需要很長時間。

  • 優化令牌掃描算法(見下文)

我也考慮過,但是已經拒絕了,這些優化:

  • 掃描,這只是在屏幕上的代碼。雖然這可以解決瓶頸問題,但它會限制查找比屏幕開始位置更早出現的用戶定義標記(即變量名稱,函數名稱,宏)的能力。
  • 將文本切換到鏈接列表(每行一個節點),而不是單片陣列。這並不能真正幫助瓶頸。雖然插入/刪除操作會更快,但索引訪問的丟失會降低解析器的速度。我認爲,單一數組更容易被緩存,而不是分解列表。
  • 對每種語言的掃描標記功能進行硬編碼。雖然這可能是性能的最佳優化,但從軟件開發的角度來看,這似乎並不實際。

建築溶液:

彙編語言,一個更快的方式來解析這些字符串將字符加載到寄存器和在一個時間對它們進行比較48或字節。有一些額外的措施和預防措施必須加以考慮,例如:

  • 該體系結構是否支持未對齊的內存訪問?
  • 所有字符串必須是大小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算法(據我所知)是一種在字符串中查找子字符串的算法。由於我解析爲多個字符串,我認爲還有更多優化空間。

+1

如果你想快速做到這一點您不會使用字符串比較和字符串列表進行循環,但會根據發現關鍵字時觸發的所有字符串的每個字符構建一個有限狀態機。 Lex實用程序執行此操作。 – Jiminion

回答

3

阿霍Corasick是一個非常酷的算法,但它不是理想的關鍵字匹配,因爲關鍵字匹配是對準;你不能有重疊的匹配,因爲你只匹配一個完整的標識符。

對於基本的標識符查找,您只需要從關鍵字中創建一個trie(請參閱下面的註釋)。

您的基本算法很好:找到標識符的開頭,然後查看它是否是關鍵字。改進這兩個部分是很重要的。除非需要處理多字節字符,否則查找關鍵字開頭的最快方法是使用256條目表,每個可能的字符都有一個條目。有三種可能性:

  1. 該字符不能出現在標識符中。 (繼續掃描)

  2. 該字符可以出現在標識符中,但沒有關鍵字以字符開頭。 (跳過標識符)

  3. 該字符可以啓動一個關鍵字。 (開始行走樹;如果行走不能繼續,跳過標識符;如果行走找到關鍵字並且下一個字符不能在標識符中,則跳過標識符的其餘部分;如果它可以在標識符中,請嘗試繼續如果可能的話,走路。)

其實第2步和第3步足夠接近,你不需要特殊的邏輯。

上述算法存在一些不準確的地方,因爲在很多情況下,您會發現看起來像標識符但語法上不可能的東西。最常見的情況是評論和引用字符串,但大多數語言都有其他可能性。例如,在C中,可以使用十六進制浮點數;雖然沒有C關鍵字可以只從[a-f]構造,用戶提供的文字可能是:

0x1.deadbeef 

在另一方面,C++允許用戶自定義的數字後綴,您很可能希望識別爲關鍵字,如果用戶將它們添加到列表:

274_myType 

除了上述所有的,這真的不現實解析每次一百萬行代碼的用戶類型在編輯器中的一個字符。您需要開發一些緩存標記化的方法,最簡單也是最常用的方法是通過輸入行進行緩存。將輸入行保持在一個鏈表中,並且每個輸入行還在行的開頭記錄標記器狀態(即,是否在多行引用字符串中;多行註釋或其他特殊行詞彙狀態)。除了一些非常奇怪的語言,編輯不會影響編輯之前的行的標記結構,因此對於任何編輯,只需要重新編輯已編輯的行以及標記符狀態已更改的後續行。(請注意在多行字符串的情況下也能吃苦耐勞的:它可以創造大量的視覺噪聲的翻轉整個顯示,因爲用戶類型的未終結的報價。)


注意:對於短小(數百)關鍵字的數量,完整的特里沒有真正佔用這麼多的空間,但在某些時候,你需要處理臃腫的分支。一個非常合理的數據結構,如果你仔細查看數據佈局,可以做得非常好,是一個ternary search tree(儘管我稱之爲三元搜索特里)。

+0

特里看起來像一個非常有前途的解決方案,謝謝。關於你的第二部分,我打算實現一個評估函數來調整令牌(參見問題:'ctrl + f掃描')而不用重新掃描。另外,輸入'* /'可能會影響到之前的行,但我明白你的觀點。 – tay10r

+0

@TaylorFlores:'* /'如何影響以前的行?它會終止在可能的上一行開始的註釋,但它不會使上一行突然變成註釋或不註釋。 (避免將未發表的評論或引用檢測爲錯誤並對它們進行處理,也會導致不必要的視覺噪音; 99.99%的時間,用戶即將終止它們。我採用的一種策略是避免未編輯後的再着色即使lex狀態發生變化,直到用戶停止輸入一段時間,希望它不會被需要。) – rici

+0

「關鍵字匹配是一致的;您不能重疊匹配,因爲您只匹配完整標識符「 - 問題包括提及搜索英語單詞,這當然可以重疊。 –

1

做到這一點的最快方法是構建到單詞集的有限狀態機。使用Lex構建FSM。

+0

沒錯,但是有很多細節在那裏丟失,很多方法可以做到*不會最快。這些問題已被大牌算法解決。編輯:lex是一個很好的建議,但flex是可取的。 –

+0

我想唯一的bugaboo是動態字符串(就像它正在被編輯?)。在這種情況下,請注意編輯區域並保留舊的標記流,直到距離更改區域有一段距離並重新標記爲止。 – Jiminion

0

這個問題的最佳算法可能是Aho-Corasick。目前已經存在的C實現,例如,

http://sourceforge.net/projects/multifast/ 
+0

感謝您的鏈接。我仍在讀它。它看起來像我不能動態地添加關鍵字到列表中,這是一種繪畫,因爲那樣我就不能添加用戶定義的關鍵字(參見問題:'ctrl + f user-defined tokens')。這是真的? – tay10r

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,然後再試一次。 關鍵是,通過分派第一個字符,您可以快速進入以該字符開始的關鍵字子集。 你甚至可能想要進入兩級調度。

當然,像往常一樣,採取一些堆棧樣本,看看是什麼時間被用於。 無論如何,如果你有數據結構類,你會發現消耗大部分時間,所以保持在最低限度(拋棄宗教:)

+0

我明白你的意思了。我正在研究一種哈希方法,我還沒有發佈,並且它也可能非常高效。 – tay10r

+0

@Taylor:哈希編碼將是一個有趣的練習,但就每個輸入字符的指令週期而言,除非您有數百萬個關鍵字,否則這裏的代碼將很難被擊敗。當您生成輸入單詞的哈希碼時,您已經花費了更多的週期。散列編碼在字符串中獲勝的地方在於它們是否存儲在慢速媒體上,如數據庫。 –

+0

在使用了trie(這很有趣)之後,我實際上最終使用了這種方法。儘管寫了一段時間(到目前爲止有1000行),但是使用最後一種方法(包括trie)的速度要快很多。再次感謝! – tay10r