[英]Optimizing flex string literal parsing
我開始為我的編程語言編寫詞法分析器。
此語言的字符串文字以"
開始"
,遇到未轉義的"
時結束。 除轉義序列(常用的\\n
s, \\t
s, \\"
s等,外加使用ASCII碼(例如\\097
或\\97
)轉義字符的方式)外,所有內容(包括換行符)都將保留。
這是我到目前為止編寫的代碼:
%{
#include <iostream>
#define YY_DECL extern "C" int yylex()
std::string buffstr;
%}
%x SSTATE
%%
\" {
buffstr.clear();
BEGIN(SSTATE);
}
<SSTATE>\\[0-9]{1,3} {
unsigned code = atoi(yytext + 1);
if (code > 255) {
std::cerr << "SyntaxError: decimal escape sequence larger than 255 (" << code << ')' << std::endl;
exit(1);
}
buffstr += code;
}
<SSTATE>\\a buffstr += '\a';
<SSTATE>\\b buffstr += '\b';
<SSTATE>\\f buffstr += '\f';
<SSTATE>\n buffstr += '\n';
<SSTATE>\r buffstr += '\r';
<SSTATE>\t buffstr += '\t';
<SSTATE>\v buffstr += '\v';
<SSTATE>\\\\ buffstr += '\\';
<SSTATE>\\\" buffstr += '\"';
<SSTATE>\\. {
std::cerr << "SyntaxError: invalid escape sequence (" << yytext << ')' << std::endl;
exit(1);
}
<SSTATE>\" {
std::cout << "Found a string: " << buffstr << std::endl;
BEGIN(INITIAL);
}
<SSTATE>. buffstr += yytext[0];
. ;
%%
int main(int argc, char** argv) {
yylex();
}
它運行完美,但是如您所見,它並沒有特別優化。
對於要解析的字符串文字中的每個字符,都將一個字符附加到std :: string一次,這是不理想的。
我想知道是否有更好的方法,例如存儲指針並增加長度,然后使用std::string(const char* ptr, size_t lenght)
構建字符串的示例。
有一個嗎? 那會是什么
可能是這樣的情況,所提供的代碼對於所有實際目的來說都是足夠快的,並且您不必擔心對其進行優化,直到您真正看到它成為瓶頸為止。 詞法掃描,即使效率低下,也很少對編譯時間有重要貢獻。
但是,有些優化是直接的。
最簡單的方法是觀察大多數字符串不包含轉義序列。 因此,應用通常的優化技術來尋找低窪的果實,我們從一個字符串中處理沒有轉義序列的字符串開始,甚至沒有經過單獨的詞法狀態。 [注1]
\"[^"\\]*\" { yylval.str = new std::string(yytext + 1, yyleng - 2);
return T_STRING;
}
(F)lex提供yyleng
,它是找到的令牌的長度,因此,從沒有真正的理由使用strlen
重新計算長度。 在這種情況下,我們不需要字符串中的雙引號,因此我們選擇yyleng - 2
從第二個字符開始的2個字符。
當然,我們需要處理轉義碼; 我們可以使用類似於您的開始條件。 只有在字符串文字內找到轉義字符時,才輸入此開始條件。 [注2]為了解決這種情況,我們依賴於(f)lex實現的最大munch規則,即匹配時間最長的模式擊敗了其他所有在相同輸入點匹配的模式。 [注3]由於我們已經匹配了所有以“開頭並且在結束”之前不包含反斜杠的標記,因此我們可以添加一個非常相似的模式而沒有結束引號,只有在第一個規則不匹配的情況下才會匹配t,因為與右引號的匹配長了一個字符。
\"[^"\\]* { yylval.str = new std::string(yytext + 1, yyleng - 1);
BEGIN(S_STRING);
/* No return, so the scanner will continue in the new state */
}
在S_STRING
狀態下,我們仍然可以匹配不包含反斜杠的序列(不僅僅是單個字符),從而顯着減少了動作執行和字符串追加的數量:
(開始條件下的帶括號的模式列表是flex擴展名 。)
<S_STRING>{
[^"\\]+ { yylval.str->append(yytext, yyleng); }
\\n { (*yylval.str) += '\n'; }
/* Etc. Handle other escape sequences similarly */
\\. { (*yylval.str) += yytext[1]; }
\\\n { /* A backslash at the end of the line. Do nothing */ }
\" { BEGIN(INITIAL); return T_STRING; }
/* See below */
}
當我們最終找到與最后一個模式匹配的未轉義的雙引號時,我們首先重置詞法狀態,然后返回已完全構造的字符串。
模式\\\\\\n
實際上與該行末尾的反斜杠匹配。 通常,完全忽略此反斜杠和換行符,以便允許長字符串在多個源代碼行上繼續。 如果您不想提供此功能,只需更改\\.
模式為\\(.|\\n)
。
如果我們找不到未轉義的雙引號怎么辦? 也就是說,如果意外省略了雙引號怎么辦? 在這種情況下,我們將以S_STRING
開始條件結束,因為字符串沒有以引號終止,因此后備模式將匹配。 在S_STRING
模式中,我們需要添加另外兩種可能性:
<S_STRING>{
// ... As above
<<EOF>> |
\\ { /* Signal a lexical error */ }
}
這些規則中的第一個捕獲了簡單的未終止的字符串錯誤。 第二種情況是在反斜杠后沒有合法字符的情況下發生的,給定其他規則,只有在反斜杠是程序中最后一個具有未終止字符串的字符時,該規則才會發生。 盡管這不太可能,但是它有可能發生,所以我們應該抓住它。
進一步的優化相對簡單,盡管我不推薦這樣做,因為它主要只是使代碼變得復雜,並且好處是無窮的。 (由於這個原因,我沒有包含任何示例代碼。)
在開始條件下,反斜杠(幾乎)總是導致將單個字符追加到我們要累積的字符串中,這意味着我們可以調整字符串的大小以進行此追加,即使我們只是調整大小以追加非字符也是如此。 -轉義字符。 相反,我們可以在操作中向字符串添加一個與非轉義字符匹配的附加字符。 (由於(f)lex將輸入緩沖區修改為以NUL終止的令牌,因此令牌后的字符將始終為NUL,因此將追加長度增加1將在字符串中插入此NUL而不是反斜杠。但是這並不重要。)
然后,用於處理轉義字符的代碼需要替換字符串中的最后一個字符,而不是將單個字符附加到字符串中,從而避免一個附加調用。 當然,在不希望插入任何內容的情況下,我們需要將字符串的大小減少一個字符,並且如果存在一個轉義序列(例如unicode轉義)會增加一個以上的字節對字符串,我們需要做一些其他的雜技。
簡而言之,我認為這不僅僅是優化,還算是駭客。 但是就其價值而言,我過去曾做過這樣的事情,因此我也必須對過早的優化負責。
您的代碼僅打印出令牌,這使得很難知道將字符串傳遞給解析器的設計。 我在這里假設一種或多或少的標准策略,其中語義值yylval
是一個並集,其成員是std::string*
( 不是 std::string
)。 我沒有解決由此產生的內存管理問題,但是%destruct
聲明會有所幫助。
在此答案的原始版本中,我建議通過使用與反斜杠匹配的模式作為尾隨上下文來捕獲這種情況:
\\"[^"\\\\]*/\\\\ { yylval.str = new std::string(yytext + 1, yyleng - 1); BEGIN(S_STRING); /* No return, so the scanner will continue in the new state */ }
但是使用最大munch規則更簡單,更通用。
如果多個圖案具有相同的最長匹配,則以掃描儀描述中的第一個為准。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.