【问题标题】:Performance of tokenizing CSS in PHP在 PHP 中标记 CSS 的性能
【发布时间】:2010-04-09 18:53:26
【问题描述】:

这是一个从未编写过解析器/词法分析器的人提出的菜鸟问题。

我正在为 PHP 中的 CSS 编写标记器/解析器(请不要重复“OMG,为什么在 PHP 中?”)。语法由 W3C 整齐地写下来here (CSS2.1)here (CSS3, draft)

这是一个包含 21 个可能标记的列表,所有(除了两个)都不能表示为静态字符串。

我目前的方法是一遍又一遍地遍历包含 21 个模式的数组,执行if (preg_match()) 并通过匹配减少源字符串匹配。原则上,这真的很好。然而,对于一个 1000 行的 CSS 字符串,这需要 2 到 8 秒,这对我的项目来说太长了。

现在我正在研究其他解析器如何在几分之一秒内标记 解析 CSS。好的,C 总是比 PHP 快,但是,有没有明显的 D'Oh! 让我陷入困境?

我做了一些优化,比如检查 '@'、'#' 或 '"' 作为剩余字符串的第一个字符,然后只应用相关的正则表达式,但这并没有带来任何显着的性能提升。

到目前为止我的代码(sn-p):

$TOKENS = array(
  'IDENT' => '...regexp...',
  'ATKEYWORD' => '@...regexp...',
  'String' => '"...regexp..."|\'...regexp...\'',
  //...
);

$string = '...CSS source string...';
$stream = array();

// we reduce $string token by token
while ($string != '') {
    $string = ltrim($string, " \t\r\n\f"); // unconsumed whitespace at the
        // start is insignificant but doing a trim reduces exec time by 25%
    $matches = array();
    // loop through all possible tokens
    foreach ($TOKENS as $t => $p) {
        // The '&' is used as delimiter, because it isn't used anywhere in
        // the token regexps
        if (preg_match('&^'.$p.'&Su', $string, $matches)) {
            $stream[] = array($t, $matches[0]);
            $string = substr($string, strlen($matches[0]));
            // Yay! We found one that matches!
            continue 2;
        }
    }
    // if we come here, we have a syntax error and handle it somehow
}

// result: an array $stream consisting of arrays with
// 0 => type of token
// 1 => token content

【问题讨论】:

  • 个人资料。使用 XDebug 生成分析数据并将其加载到 KCacheGrind 中。如果可能,请避免在源字符串上一遍又一遍地运行 substr() ——一遍又一遍地重新分配字符串并不是免费的。并找到一些方法来减少您评估的正则表达式的数量。或者,更好的是,停止使用正则表达式。
  • 显而易见的 D'oh!不是在阅读有关词法分析器如何真正工作的信息。关键思想是它们将 set 模式匹配(您称它们为正则表达式)组合成一个匹配器,就像它一次应用所有匹配器一样。再多的“优化”你的尝试模式方案,一次一个,在性能上永远不会接近。

标签: php performance parsing token lexer


【解决方案1】:

使用lexer generator

【讨论】:

  • 感谢您的指点。我会看看它(尤其是生成的代码)。
  • 按照埃里克的建议去做。在您了解词法分析器生成器为您提供什么以及它是如何工作的之前,您将无法理解为什么它可以如此快速地对输入流进行 lex。
  • 仅链接的答案不是很好。但在其中,那些链接断开的完全没用。
【解决方案2】:

我要做的第一件事就是删除preg_match()strpos() 之类的基本字符串函数要快得多,但我认为您甚至不需要它。看起来您正在寻找带有preg_match() 的字符串前面的特定标记,然后简单地将该字符串的前面长度作为子字符串。您可以使用简单的substr() 轻松完成此操作,如下所示:

foreach ($TOKENS as $t => $p)
{
    $front = substr($string,0,strlen($p));
    $len = strlen($p);  //this could be pre-stored in $TOKENS
    if ($front == $p) {
        $stream[] = array($t, $string);
        $string = substr($string, $len);
        // Yay! We found one that matches!
        continue 2;
    }
}

您可以通过预先计算所有令牌的长度并将它们存储在$TOKENS 数组中来进一步优化它,这样您就不必一直调用strlen()。如果您按长度将$TOKENS 分组,您还可以进一步减少substr() 调用的数量,因为您可以对每个令牌长度只分析一次当前字符串的substr($string),然后遍历所有在移动到下一组标记之前,该长度的标记。

【讨论】:

  • 问题是,我事先不知道令牌长度。例如,@-token 可以是“@charset”、“@namespace”或“@import”,但也可以是“@-moz-document”之类的任意东西。它被定义为 '@' 后跟 1 个或多个 [a-zA-Z0-9_-] 转义序列(如\10FFFF任何非ASCII Unicode 字符。我可以放弃preg_match,只处理'@'后面的字符,但是如果它是非ASCII或允许ASCII或ASCII作为转义序列的一部分,我必须逐个字符地测试字符,我想,这就是正则表达式引擎的优化目标。
【解决方案3】:

(可能)更快(但对内存不太友好)的方法是一次标记整个流,使用一个大的正则表达式和每个标记的替代项,例如

 preg_match_all('/
       (...string...)
       |
       (@ident)
       |
       (#ident)
       ...etc
   /x', $stream, $tokens);

 foreach($tokens as $token)...parse

【讨论】:

  • 原则上,这是可行的(只要内存限制不会崩溃)。但是,之后我必须遍历每场比赛并找出它是什么类型的令牌。它可能有效(在许多情况下,查看第一个字符串就足够了),但我怀疑它会快得多。
【解决方案4】:

不要使用正则表达式,逐个字符扫描。

$tokens = array();
$string = "...code...";
$length = strlen($string);
$i = 0;
while ($i < $length) {
  $buf = '';
  $char = $string[$i];
  if ($char <= ord('Z') && $char >= ord('A') || $char >= ord('a') && $char <= ord('z') || $char == ord('_') || $char == ord('-')) {
    while ($char <= ord('Z') && $char >= ord('A') || $char >= ord('a') && $char <= ord('z') || $char == ord('_') || $char == ord('-')) {
      // identifier
      $buf .= $char;
      $char = $string[$i]; $i ++;
    }
    $tokens[] = array('IDENT', $buf);
  } else if (......) {
    // ......
  }
}

但是,这会使代码无法维护,因此,解析器生成器会更好。

【讨论】:

    【解决方案5】:

    这是一篇旧帖子,但仍然为此贡献了我的 2 美分。 严重减慢问题中原始代码的一件事是以下行:

    $string = substr($string, strlen($matches[0]));
    

    而不是处理整个字符串,只取其中的一部分(比如 50 个字符),这对于所有可能的正则表达式来说已经足够了。然后,在其上应用相同的代码行。当这个字符串收缩到预设长度以下时,向它加载更多数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-18
      • 2011-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-28
      相关资源
      最近更新 更多