PHP tokenizing CSS performansı

5 Cevap php

This is a noob question from someone who hasn't written a parser/lexer ever before.

Ben ('OMG, neden PHP?' Ile tekrar etmeyiniz) PHP CSS için tokenizer / ayrıştırıcı yazıyorum. Sözdizimi W3C tarafından aşağı yazılır düzgünce here (CSS2.1) ve here (CSS3, draft).

Tüm (ama iki) statik dizeleri olarak temsil edilemez, 21 olası belirteçleri bir listesi.

Benim şu anki yaklaşım if (preg_match()) yapmak ve maçın tarafından kaynak dize maçı azaltmak, tekrar ve tekrar 21 desenleri içeren bir dizi döngü olduğunu. Prensip olarak bu gerçekten iyi çalışıyor. Ancak, 1000 satır CSS dize için bu benim proje için çok fazla 2 ve 8 saniye arasında bir şey alır.

Şimdi diğer ayrıştırıcılarda and saniye kesirleri CSS ayrıştırmak tokenize nasıl başımı beceriyor ediyorum. Tamam, C is her zaman PHP daha hızlı, ama yine de, herhangi bir bariz D'Oh! s ben içine düştüğü vardır?

Ben ilk kalan dize karakter ve sonra sadece ilgili regexpi uygulamak olarak '@' için kontrol gibi bazı optimizasyonlar, '#' veya '"' yaptı, ancak bu herhangi bir büyük performans artışları getirdi yoktu.

Benim kodu (pasajı) şimdiye kadar:

$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

5 Cevap

Bir lexer generator kullanın.

Ben yapacağını ilk şey preg_match() kurtulmak olacaktır. Böyle strpos() gibi temel string fonksiyonları çok daha hızlı, ama ben onu bile gerek yok. Eğer preg_match() ile bir dize önünde belirli bir simge arıyor ve o zaman sadece bir alt dize olarak bu dizenin ön uzunluğu alıyor gibi görünüyor. Kolayca basit substr() yerine, bu böyle ile bunu başarabilir:

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

Eğer strlen() her zaman aramak zorunda değilsiniz, böylece daha fazla, tüm jeton uzunluğunu önceden hesaplanması ve $TOKENS dizide saklayarak bu optimize olabilir. Eğer $TOKENS uzunluğuna göre gruplara ayrılır ise, sayısını azaltabilir Eğer olmanın geçerli dize bir substr($string) sürebilir gibi substr(), ileri yanı çağırır Her belirteç uzunluğu boyunca sadece bir kez analiz ve jeton sonraki gruba geçmeden önce bu uzunluğu tüm belirteçleri koşuyoruz.

(muhtemelen) daha hızlı (ama daha az hafıza dostu) bir yaklaşım gibi, her bir simge için alternatifleri ile büyük bir regexpi kullanarak, bir kerede tüm akışı tokenize olacaktır

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

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

Karakteri, regexp tarama karakter kullanmayın.

$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 (......) {
    // ......
  }
}

Ancak, bu kod unmaintainable yapar, bu nedenle, bir ayrıştırıcı jeneratör daha iyidir.

It's an old post but still contributing my 2 cents on this. one thing that seriously slows down the original code in the question is the following line :

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

yerine tüm dize üzerinde çalışan, bunun sadece bir kısmını almak tüm olası regexes için yeterli (50 karakter demek). Daha sonra, üzerine kod aynı çizgiyi geçerlidir. Bu dize bir önceden belirlenmiş uzunluğu altında küçülür zaman, ona biraz daha fazla veri yüklenemedi.