【问题标题】:Optimizing flex string literal parsing优化 flex 字符串文字解析
【发布时间】:2017-07-10 07:58:25
【问题描述】:

我开始为我的编程语言编写词法分析器。

此语言中的字符串文字以 " 开头,并在遇到未转义的 " 时结束。除了转义序列(通常的\ns、\ts、\"s 等以及使用其 ASCII 码转义字符的方法外,其中的所有内容(包括换行符)都被保留,例如 \097 或 @987654327 @)。

这是我目前写的代码:

%{
#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) 构建字符串。

有吗?会是什么?

【问题讨论】:

  • 您不需要std::string。只需使用YYMOREYYMORE()yymore() 或其他任何名称。这样会更快。

标签: c++ parsing flex-lexer stdstring string-literals


【解决方案1】:

这可能是因为所提供的代码对于所有实际目的都足够快,并且在您真正观察到它是一个瓶颈之前,您不应该担心对其进行优化。词法扫描,即使是低效的,也很少对编译时间产生重要影响。

但是,一些优化是直截了当的。

最简单的方法是观察大多数字符串不包含转义序列。因此,应用通常的优化技术来寻找低位水果,我们首先在一个模式中处理没有转义序列的字符串,甚至不通过单独的词法状态。 [注1]

\"[^"\\]*\"   { yylval.str = new std::string(yytext + 1, yyleng - 2); 
                return T_STRING;
              }

(F)lex 提供了yyleng,这是它找到的令牌的长度,因此没有任何理由用strlen 重新计算长度。在这种情况下,我们不希望字符串中出现双引号,因此我们选择从第二个字符开始的yyleng - 2 字符。

当然,我们需要处理转义码;我们可以使用类似于您的启动条件来执行此操作。只有当我们在字符串文字中找到转义字符时,我们才会输入此开始条件。 [注 2] 为了捕捉这种情况,我们依赖于 (f)lex 实现的 maximal munch 规则,即最长匹配的模式击败任何其他恰好匹配的模式相同的输入点。 [注 3] 由于我们已经匹配了任何以 " 开头并且在结束 " 之前不包含反斜杠的标记,我们可以添加一个非常相似的模式仅在第一个规则不匹配的情况下才匹配的右引号,因为与右引号匹配的字符长一个字符。

\"[^"\\]*     { 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 extension。)

<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 转义)会添加一个以上的字节到字符串,我们需要做一些其他的杂技。

简而言之,我认为这是一种 hack 而不是优化。但不管怎样,我过去做过这样的事情,所以我也不得不承认过早优化的罪名。


注意事项

  1. 您的代码只打印出令牌,这使得很难知道您的设计是什么将字符串传递给解析器。我在这里假设一种或多或少的标准策略,其中语义值yylval 是一个联合,其成员是std::string*不是std::string)。我没有解决由此产生的内存管理问题,但%destruct 声明会很有帮助。

  2. 在这个答案的原始版本中,我建议通过使用匹配反斜杠作为尾随上下文的模式来捕捉这种情况:

    \"[^"\\]*/\\    { yylval.str = new std::string(yytext + 1, yyleng - 1);
                      BEGIN(S_STRING);
                      /* No return, so the scanner will continue in the new state */
                }
    

    但使用最大咀嚼规则更简单、更通用。

  3. 如果多个模式具有相同的最长匹配,则扫描仪描述中的第一个获胜。

【讨论】:

  • 太棒了。我什至没有开始考虑野牛,现在我只是定义我的终端符号,或多或少遵循this tutorial。但是为什么是 std::string 指针而不是普通的 std::string? std::string 只包含一个指针,而且还有更多。
  • @user6245072:字符串不能是联合的成员。指针可以。
  • 另外,EJP 也有道理,但我不想深入讨论,因为在我看来,您希望能够将 NUL 放入字符串文字中。就个人而言,我会使用 const char* 和 size_t,但 atd::string* 有其魅力。
  • 我有一个疑问:我如何匹配以" 开头、以字符(但没有反斜杠)继续并以 EOF 而不是结束 '"' 结尾的字符串?如果该字符串包含转义码很简单,因为它使扫描仪进入S_STRING状态,我可以简单地写&lt;S_STRING&gt;&lt;&lt;EOF&gt;&gt;。但它不会让我写\"[^"]*&lt;&lt;EOF&gt;&gt;(我认为&lt;&lt;EOF&gt;&gt;只能单独使用在规则内)。
  • 哦,好的。顺便说一句,\"[^"]*/\` in your answer should be \"[^"\]*/\`,或者像"abcd\\efgh\` would be matched as "abcd\\efgh` 这样的字符串,然后是\`, rather than abcd, \`、efgh、`\`。
猜你喜欢
  • 2015-03-31
  • 1970-01-01
  • 1970-01-01
  • 2021-07-26
  • 2012-07-09
  • 1970-01-01
  • 2012-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多