Эффективность Preg_replace

Управляющее резюме:

preg_replace() работает быстрее, чем сравнение строк. Зачем? Разве регулярные выражения не должны замедляться?


В недавнем вопросе об обнаружении любого из массива недопустимых подстрок внутри данного ввода я предложил сравнить результат preg_replace() с исходным вводом, поскольку preg_replace() может принимать массив шаблонов в качестве входных данных. Таким образом, мой метод для этого может быть единым, if для других решений требуется одна (или много) петлей.

Мне не интересно обсуждать мой ответ, потому что на самом деле он менее читабельный / поддерживаемый, чем циклы. Мой ответ там по-прежнему имеет значение -1, и я соглашусь с тем, что для удобства чтения или простоты обслуживания, но самой большой ошибкой, отмеченной моим методом, было отсутствие эффективности. Мне было любопытно, и я провела несколько испытаний. Мои результаты были для меня несколько удивительными: при прочих равных условиях preg_replace() был быстрее любого из других методов.

Можете ли вы объяснить, почему так было?

Мой код для этих тестов можно найти ниже, а также результаты:

 $input = "In a recent question about detecting any of an array of disallowed substrings within a given input, I suggested comparing the result of a `preg_replace()` call to the original input, since `preg_replace()` can take an array of patterns as input. Thus my method for this could be a single `if` whereas the other solutions required one (or many) loops. I'm not interested in debating my answer, because really it is less readable/maintainable than the loops. However, the biggest fault pointed out with my method was a lack of efficiency. That got me curious, and led me to do some testing. My results were a bit surprising to me: with all other factors held equal, `preg_replace()` was **faster** than any of the other methods. Can you explain why this was the case?"; $input2 = "Short sentence - no matches"; $input3 = "Word"; $input4 = "Short sentence - matches loop"; $start1 = microtime(true); $rejectedStrs = array("loop", "efficiency", "explain"); $p_matches = 0; for ($i = 0; $i < 10000; $i++) { if (str_check($rejectedStrs, $input)) $p_matches++; if (str_check($rejectedStrs, $input2)) $p_matches++; if (str_check($rejectedStrs, $input3)) $p_matches++; if (str_check($rejectedStrs, $input4)) $p_matches++; } $start2 = microtime(true); $rejectedStrs = array("loop", "efficiency", "explain"); $l_matches = 0; for ($i = 0; $i < 10000; $i++) { if (loop_check($rejectedStrs, $input)) $l_matches++; if (loop_check($rejectedStrs, $input2)) $l_matches++; if (loop_check($rejectedStrs, $input3)) $l_matches++; if (loop_check($rejectedStrs, $input4)) $l_matches++; } $start3 = microtime(true); $rejectedStrs = array("/loop/", "/efficiency/", "/explain/"); $s_matches = 0; for ($i = 0; $i < 10000; $i++) { if (preg_check($rejectedStrs, $input)) $s_matches++; if (preg_check($rejectedStrs, $input2)) $s_matches++; if (preg_check($rejectedStrs, $input3)) $s_matches++; if (preg_check($rejectedStrs, $input4)) $s_matches++; } $end = microtime(true); echo $p_matches." ".$l_matches." ".$s_matches."\n"; echo "str_match: ".$start1." ".$start2."= ".($start2-$start1)."\nloop_match: ".$start2." ".$start3."=".($start3-$start2)."\npreg_match: ".$start3." ".$end."=".($end-$start3); function preg_check($rejectedStrs, $input) { if($input == preg_replace($rejectedStrs, "", $input)) return true; return false; } function loop_check($badwords, $string) { foreach (str_word_count($string, 1) as $word) { foreach ($badwords as $bw) { if (stripos($word, $bw) === 0) { return false; } } } return true; } function str_check($badwords, $str) { foreach ($badwords as $word) { if (stripos(" $str ", " $word ") !== false) { return false; } } return true; } 

Результаты

20000 20000 20000

str_match: 1282270516.6934 1282270518.5881 = 1.894730091095

loop_match: 1282270518.5881 1282270523.0943 = 4.5061857700348

preg_match: 1282270523.0943 1282270523.6191 = 0.52475500106812

Давайте сначала посмотрим на preg_check и loop_check . Оба из них должны будут пересечь всю строку, и им придется проверять каждое из отдельных слов в каждом обходе. Таким образом, их поведение будет по крайней мере равно O(n*m) , где n – длина строки, а m – количество плохих слов. Вы можете проверить это, запустив алгоритм с возрастающими значениями n и m и построив 3D-графики (однако вы можете или не должны запускать его с очень высокими значениями n и m чтобы увидеть это поведение).

loop_check здесь (асимптотически) эффективнее. Причина в том, что количество слов, которые имеет строка, не пропорционально их длине – я, кажется, вспоминаю, как правило, следует за логарифмической функцией. Вероятно, он использует хеш-таблицу для хранения слов, которые она находит в пути, который выполняется в среднем постоянном времени (если мы игнорируем, что нам, возможно, придется время от времени перестраивать хеш-таблицу для размещения большего количества элементов).

Поэтому loop_check будет иметь асимптотическое поведение, которое следует за чем-то вроде n + m * log(n) , которое лучше n*m .

Теперь это относится к асимптотическому поведению алгоритмов, т. Е. Когда m и n растут очень (и может потребоваться очень «очень»). При малых значениях m и n константы играют большую роль. В частности, выполнение кодов операций PHP и вызовов функций PHP является более дорогостоящим, чем одна и та же задача, реализованная на C, только один вызов функции. Это не ускоряет алгоритм регулярного выражения, а просто уменьшает значения m и n .

Можете ли вы объяснить, почему так было?

Легко. preg_match реализуется в C. Другие решения реализованы в PHP. Теперь это не означает, что регулярное выражение всегда будет быстрее, чем эквивалентный PHP, но в большинстве случаев это, вероятно, будет.

В последнее время у меня была аналогичная ситуация, когда у меня была функция (конвертер CamelCase), которую называли десятки тысяч раз, и занимая достаточное количество CPU (я профилировал). Я пробовал каждую повторную реализацию PHP, о которой я мог мечтать. preg_replace всегда был быстрее. В конце концов, я оставил функцию так, как она была, и запомнила ее, что и сделало трюк.

Во многих случаях, чем меньше выполняются инструкции PHP, тем лучше. Если вы можете заменить цикл на один вызов функции, реализованной в C под капотом, это может быть вашим лучшим выбором.

на самом деле он менее читабельным / поддерживаемым, чем циклы

Я не согласен. Однострочники так же просто, как и все. Хотя я, вероятно, поеду с чем-то более похожим

 function preg_check($rejectedStrs, $input) { return preg_match($rejectedStrs, "", $input); }