Почему 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)