Почему mb_strpos() так значительно медленнее, чем strpos()?

Я критиковал ответ, который предложил preg_match over === при поиске смещений подстроки, чтобы избежать несоответствия типов.

Однако позже автор ответа обнаружил, что preg_match на самом деле значительно быстрее, чем многобайтовый режим работы mb_strpos. Обычный strpos быстрее, чем обе функции, но, конечно, не может иметь дело с многобайтовыми строками.

Я понимаю, что mb_strpos нужно сделать что-то большее, чем strpos. Однако, если регулярное выражение может сделать это почти так же быстро, как strpos, что это значит, что mb_strpos занимает так много времени?

У меня есть сильное подозрение, что это ошибка оптимизации. Могут ли, например, расширения PHP быть медленнее, чем его собственные функции?

mb_strpos($str, "颜色", 0 ,"GBK"): 15.988190889 (89%)
preg_match("/颜色/", $str): 1.022506952 (6%)
strpos($str, "dh"): 0.934401989 (5%)

Функции выполнялись 10 6 раз. Абсолютное время учитывает сумму времени 10 6 пробегов функции, а не среднее значение для одного.

Тестовая строка $str = "代码dhgd颜色代码";. Тест можно увидеть здесь (прокрутите вниз, чтобы пропустить класс тестирования).

Примечание.. По словам одного из комментаторов (и здравого смысла), preg_match также не использует многобайтов при сравнении, подвергая себя тому же риску ошибок, что и strpos.

Ответы

Ответ 1

Чтобы понять, почему функции имеют разное время выполнения, вам нужно понять, что они на самом деле делают. Потому что суммировать их как "они ищут иглу в стоге сена, этого недостаточно".

strpos

Если вы посмотрите реализацию strpos, она использует zend_memstr внутренне, что реализует красивый наивный алгоритм поиска иглы в стоге сена: в основном используется memchr для поиска первого байта иглы в стоге сена, а затем использует memcmp для проверки начинается ли вся игла в этом положении. Если нет, он повторяет поиск первого байта иглы с позиции предыдущего совпадения первого байта.

Зная это, можно сказать, что strpos выполняет поиск байтовой последовательности в байтовой последовательности с использованием алгоритма наивного поиска.

mb_strpos

Эта функция является многобайтовой копией strpos. Это делает поиск немного более сложным, поскольку вы не можете просто взглянуть на байты, не зная, к какому символу они принадлежат.

mb_strpos использует mbfl_strpos, что делает намного больше по сравнению с простым алгоритмом zend_memstr, его как 200 строк сложного кода (mbfl_strpos) по сравнению с 30 строками гладкого кода (zend_memstr).

Мы можем пропустить часть где игла и стога сена преобразуются в UTF-8, если необходимо, и перейдите к основной фрагмент кода.

Сначала у нас есть две петли установки, а затем есть цикл который переводит указатель в соответствии с данным смещением, где вы можете видеть, что они знают фактические символы и как они пропускают целые кодированные символы UTF-8: поскольку UTF-8 представляет собой кодировку символов переменной ширины, где первый байт каждого кодированного символа обозначает всю длину кодированного символа. Эта информация хранится в массиве u8_tbl.

Наконец, цикл где происходит фактический поиск. И здесь у нас есть что-то интересное, потому что тест на иглу в определенном положении в стоге сена рассматривается в обратном порядке. И если один байт не совпал, таблица перехода jtbl используется, чтобы найти следующую возможную позицию для иглы в стоге сена. На самом деле это реализация алгоритма строкового поиска Boyer-Moore .

Итак, теперь мы знаем, что mb_strpos...

  • преобразует строки в UTF-8, если необходимо
  • знает фактические символы
  • использует алгоритм поиска Boyer-Moore

preg_match

Что касается preg_match, он использует библиотеку PCRE. В стандартном алгоритме сопоставления используется недетерминированный конечный автомат (NFA), чтобы найти совпадение, ведущее поиск глубины в дереве шаблонов. Это, по сути, наивный подход к поиску.

Ответ 2

Я оставляю preg_match, чтобы сделать анализ более акцентированным.

Взял ваше наблюдение, что mb_strpos относительно медленнее по сравнению с strpos, это приводит к предположению, что из-за потребленного времени mb_strpos делает больше, чем strpos.

Я думаю, что это наблюдение правильное.

Затем вы спросили, что это "больше", что вызывает разницу во времени.

Я пытаюсь дать простой ответ: это "больше", потому что strpos работает с двоичными строками (один символ = 8 бит = 1 октет = 1 байт). mb_strpos работает с закодированными последовательностями символов (как почти все функции mb_*), которые могут быть X бит, возможно, даже переменной длины для каждого символа.

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

Это работа перевода и - в зависимости от кодирования - также требует определенного алгоритма поиска.

Рядом с тем, что расширение mb также должно иметь некоторые структуры в памяти для организации различных кодировок символов, будь то таблицы перевода и/или конкретные алгоритмы. См. Дополнительный параметр, который вы вводите, - например, имя кодировки.

Это намного больше, чем просто простые побайтовые сравнения.

Например, кодировка символов GBK довольно интересна, когда вам нужно кодировать или декодировать определенный символ. Функция строки mb в этом случае должна учитывать все эти особенности, чтобы выяснить, есть ли и в какой позиции символ. Поскольку у PHP есть только двоичные строки в пользовательской области, из которой вы бы назвали эту функцию, вся работа должна выполняться при каждом вызове функции.

Чтобы проиллюстрировать это еще больше, если вы просмотрите список поддерживаемых кодировок (mb_list_encodings), вы также можете найти такие, как BASE64, UUENCODE, HTML- ENTITIES и Quoted-Printable. Как вы можете себе представить, все они обрабатываются по-разному.

Например, одиночный числовой объект HTML может иметь длину до 1024 байтов, если даже не больше. Крайним примером, который я знаю и люблю, является этот. Однако для этой кодировки он должен обрабатываться алгоритмом mb_strpos.

Ответ 3

Причина медленности

Взглянув на исходные исходные файлы 5.5.6 PHP, кажется, что задержка, по-видимому, возникает большей частью в mbfilter.c, где - as hakre surmised - как стог сена, так и игла должны быть проверены и преобразованы, каждый раз mb_strpos (или, я думаю, большая часть семейства mb_*) вызывается:

Если стопка haystack не используется в формате по умолчанию, закодируйте ее по умолчанию:

if (haystack->no_encoding != mbfl_no_encoding_utf8) {
        mbfl_string_init(&_haystack_u8);
        haystack_u8 = mbfl_convert_encoding(haystack, &_haystack_u8, mbfl_no_encoding_utf8);
        if (haystack_u8 == NULL) {
                result = -4;
                goto out;
        }
} else {
        haystack_u8 = haystack;
}

Если игла не используется в формате по умолчанию, закодируйте ее по умолчанию:

if (needle->no_encoding != mbfl_no_encoding_utf8) {
        mbfl_string_init(&_needle_u8);
        needle_u8 = mbfl_convert_encoding(needle, &_needle_u8, mbfl_no_encoding_utf8);
        if (needle_u8 == NULL) {
                result = -4;
                goto out;
        }
} else {
        needle_u8 = needle;
}

Согласно быстрой проверке с valgrind, преобразование кодирования составляет огромную часть времени выполнения mb_strpos, около 84% от общего числа или пять шестой:

218,552,085  ext/mbstring/libmbfl/mbfl/mbfilter.c:mbfl_strpos [/usr/src/php-5.5.6/sapi/cli/php]
183,812,085  ext/mbstring/libmbfl/mbfl/mbfilter.c:mbfl_convert_encoding [/usr/src/php-5.5.6/sapi/cli/php]

который, по-видимому, согласуется с таймингами OP mb_strpos по сравнению с strpos.

Кодирование не учитывается, mb_strpos 'строка точно совпадает с strpos с немного длинной строкой. Хорошо, строка в четыре раза длиннее, если у вас действительно неуклюжие строки, но даже тогда вы получите задержку в четыре раза, а не в два раза. Дополнительное 5-6X замедление возникает из-за времени кодирования.

Ускорение mb_strpos...

Так что вы можете сделать? Вы можете пропустить эти два шага, убедившись, что у вас есть внутренне строки уже в "базовом" формате, в котором mbfl* выполняется преобразование и сравнение, то есть mbfl_no_encoding_utf8 (UTF-8):

  • Сохраняйте данные в UTF-8.
  • Преобразуйте пользовательский ввод в UTF-8 как можно скорее.
  • При необходимости преобразуйте обратно в клиентскую кодировку.

Затем ваш псевдокод:

$haystack = "...";
$needle   = "...";

$res = mb_strpos($haystack, $needle, 0, $Encoding);

становится:

$haystack = "...";
$needle   = "...";

mb_internal_encoding('UTF-8') or die("Cannot set encoding");
$haystack   = mb_convert_encoding($haystack, 'UTF-8' [, $SourceEncoding]);
$needle     = mb_convert_encoding($needle, 'UTF-8', [, $SourceEncoding]);

$res = mb_strpos($haystack, $needle, 0);

... когда это стоит

Конечно, это удобно только в том случае, если "время установки" и обслуживание всей базы UTF-8 заметно меньше, чем "время выполнения", неявно выполняющее преобразования в каждой функции mb_*.

Ответ 4

Проблемы с производительностью mb_ могут быть вызваны запутанной установкой пакета php-mbstring (в Linux). Установка его явно для точной версии установки php помогла мне.

sudo apt-get install php7.1-mbstring

...

Before: Time: 16.17 seconds, Memory: 36.00MB OK (3093 tests, 40272 assertions)
After:  Time:  1.81 seconds, Memory: 36.00MB OK (3093 tests, 40272 assertions)