Какой самый быстрый алгоритм поиска подстроки? - PullRequest
151 голосов
/ 06 июля 2010

ОК, поэтому я не звучу как идиот, я собираюсь изложить проблему / требования более четко:

  • Игла (шаблон) и стог сена (текст для поиска) - оба Cв стиле строки с нулевым символом в конце.Информация о длине не предоставляется;если необходимо, он должен быть вычислен.
  • Функция должна возвращать указатель на первое совпадение, или NULL, если совпадение не найдено.
  • Случаи сбоев не допускаются.Это означает, что любой алгоритм с непостоянными (или большими постоянными) требованиями к хранилищу должен иметь запасной вариант для сбоя выделения (и, следовательно, производительность в резервном режиме повышает производительность в худшем случае).Реализация должна быть на C, хотя хорошее описание алгоритма (или ссылки на него) без кода тоже подойдет.

... а также то, что я подразумеваю под "быстрым":

  • Детерминистический O(n), где n = длина стога сена.(Но возможно использовать идеи из алгоритмов, которые обычно O(nm) (например, скользящий хеш), если они объединены с более надежным алгоритмом для получения детерминированных O(n) результатов).
  • Никогда не выполняет(измеримо; пара часов для if (!needle[1]) и т. д. в порядке) хуже, чем алгоритм наивной грубой силы, особенно на очень коротких иглах, которые, вероятно, являются наиболее распространенным случаем.(Безусловные тяжелые накладные расходы на предварительную обработку - это плохо, так как пытаются улучшить линейный коэффициент для патологических игл за счет вероятных игл.)
  • При произвольной игле и стоге сена сопоставимые или более высокие показатели (не хуже 50%больше времени поиска) по сравнению с любым другим широко реализованным алгоритмом.
  • Помимо этих условий, я оставляю определение «самого быстрого» открытого типа.Хороший ответ должен объяснить, почему вы считаете подход, который вы предлагаете, «самым быстрым».

Моя текущая реализация работает примерно на 10% медленнее и в 8 раз быстрее (в зависимости от ввода), чем реализация glibcof Two-Way.

Обновление: Мой текущий оптимальный алгоритм выглядит следующим образом:

  • Для игл длины 1 используйте strchr.
  • Для игл длиной 2-4 используйте машинные слова, чтобы сравнить 2-4 байта одновременно следующим образом: предварительно загрузить иглу в 16- или 32-битном целом числе со смещением в битах и ​​зациклить старый байт / новые байты из стога сенана каждой итерации.Каждый байт стога сена читается ровно один раз и проверяется на 0 (конец строки) и одно 16- или 32-битное сравнение.
  • Для игл длиной> 4 используйте двусторонний алгоритм сневерная таблица смещения (например, Бойера-Мура), которая применяется только к последнему байту окна.Чтобы избежать затрат на инициализацию таблицы размером 1 Кб, что может привести к чистым потерям для многих игл средней длины, я сохраняю битовый массив (32 байта), отмечающий, какие записи в таблице сдвига инициализированы.Биты, которые не установлены, соответствуют значениям байтов, которые никогда не появляются в игле, для которых возможен полный сдвиг длины иглы.

Большие вопросы, которые остались в моем уме:

  • Есть ли способ лучше использовать таблицу плохих смен?Бойер-Мур лучше всего использует его, сканируя в обратном направлении (справа налево), но для двухстороннего сканирования требуется сканирование слева направо.
  • Единственные два приемлемых алгоритма-кандидата, которые я нашел для общегорегистр (без условий памяти или квадратичной производительности): Двусторонняя и Строковое соответствие для упорядоченных алфавитов .Но есть ли легко обнаруживаемые случаи, когда разные алгоритмы были бы оптимальными?Конечно, многие из O(m) (где m - длина иглы) в космических алгоритмах могут быть использованы для m<100 или около того.Также было бы возможно использовать алгоритмы, которые являются наихудшими квадратичными, если есть простой тест для игл, которые, как доказано, требуют только линейного времени.

Бонусные баллы за:

  • Можете ли вы улучшить производительность, если предположить, что игла и стог сена хорошо сформированы UTF-8? (С символами различной длины байтов правильная форма накладывает некоторые требования к выравниванию строк между иглой и стогом сена и допускает автоматические сдвиги в 2-4 байта, когда встречается несоответствующий главный байт. Но эти ограничения дают вам многое / что-либо помимо того вычисления максимального суффикса, хорошие сдвиги суффиксов и т. д. уже дают вам различные алгоритмы?)

Примечание: Я хорошо знаю большинство алгоритмов, только не то, насколько хорошо они работают на практике. Вот хороший справочник, чтобы люди не давали мне ссылки на алгоритмы в виде комментариев / ответов: http://www -igm.univ-mlv.fr / ~ lecroq / string / index.html

Ответы [ 17 ]

3 голосов
/ 24 февраля 2012

Просто найдите «самый быстрый strstr», и если вы видите что-то интересное, просто спросите меня.

На мой взгляд, вы накладываете слишком много ограничений на себя (да, мы все хотим сублинейно-линейного в максимальном поисковике), однако для этого требуется настоящий программист, до тех пор, пока я не думаю, что хеш-подход - просто отличный нестабильное решение (хорошо усиленное BNDM для более коротких 2..16 паттернов).

Простой пример:

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) в виде одной строки ... Пропускная способность (чем больше, тем лучше): 3041%, 6801754 пропусков / итераций Railgun_Quadruplet_7Hasherezade_hits / Railgun_Quadruplet_7Hasherezade_clocks: 0/58 Railgun_Quadruplet_7Hasherezade производительность: 3483KB / часы

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) в виде одной строки ... Пропускная способность (чем больше, тем лучше): 1554%, 13307181 пропусков / итераций Boyer_Moore_Flensburg_hits / Boyer_Moore_Flensburg_clocks: 0/83 Boyer_Moore_Flensburg производительность: 2434KB / часы

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) в виде одной строки ... Пропускная способность (чем больше, тем лучше): 129%, 160239051 пропусков / итераций Двухсторонние хиты / Двухсторонние часы: 0/816 Двусторонняя производительность: 247 КБ / часы

Sanmayce
Привет

3 голосов
/ 03 июня 2011

Более быстрый алгоритм «Поиск одного совпадающего символа» (ala strchr).

Важные примечания:

  • Эти функциииспользуйте «число / количество (ведущих | конечных) нулей» gcc intrinsic- __builtin_ctz компилятора.Эти функции могут быть быстрыми только на машинах, на которых есть инструкция (ы), выполняющая эту операцию (например, x86, ppc, arm).

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

  • Эти функции нейтральны по отношению к процессору.Если у целевого ЦП есть векторные инструкции, вы могли бы сделать (намного) лучше.Например, приведенная ниже функция strlen использует SSE3 и может быть тривиально изменена на XOR отсканированных байтов для поиска байта, отличного от 0.Тесты производительности на ноутбуке Core 2 с частотой 2,66 ГГц и Mac OS X 10,6 (x86_64):

    • 843,433 МБ / с для strchr
    • 2656,742 МБ / с для findFirstByte64
    • 13094,479 МБ / с для strlen

... 32-разрядной версии:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}

... и64-битная версия:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}

Редактировать 2011/06/04 В комментариях ОП указывается, что в этом решении есть «непреодолимая ошибка»:

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

Это технически верно, но применимо практически к любому алгоритму, который работает с блоками, размер которых превышает один байт, включая метод , предложенный OP в комментариях:

Типичная реализация strchr не наивна, но несколько более эффективна, чем вы дали.В конце приведен наиболее широко используемый алгоритм: http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

Он также действительно не имеет ничего общего с alignment per se.Да, это может потенциально привести к поведению, обсуждаемому на большинстве используемых архитектур, но это больше связано с деталями реализации микроархитектуры - если не выровненное чтение пересекает границу 4 КБ (опять же, типично), тогда это чтение вызовет программузавершающая ошибка, если следующая граница страницы 4K не отображена.

Но это не «ошибка» в алгоритме, приведенном в ответе, - такое поведение вызвано тем, что такие функции, как strchr и strlen не принимаютlength аргумент для ограничения размера поиска.Поиск char bytes[1] = {0x55};, который для целей нашего обсуждения именно так и происходит в самом конце границы страницы 4K VM, и следующая страница не отображается, с strchr(bytes, 0xAA) (где strchr - это байт ввременная реализация) будет зависать точно так же.То же самое для strchr родственника двоюродного брата strlen.

Без аргумента length невозможно определить, когда следует переключиться с высокоскоростного алгоритма и вернуться к побайтовому алгоритму,Гораздо более вероятной «ошибкой» будет чтение «за пределами размера выделения», что технически приводит к undefined behavior в соответствии с различными стандартами языка C и будет помечено как ошибка чем-то вроде valgrind.

Таким образом, все, что работает с блоками большего размера, будет работать быстрее, поскольку этот код отвечает и код, указанный OP, но должен иметь семантику считывания с точностью до байта, вероятно, будет "глючным", еслиlength аргумент для управления угловым регистром "последнего чтения" отсутствует.

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

ОП также заявляет в комментариях:

Что касается оптимизации ctz, то она имеет значение только для хвостовой операции O (1).Это может улучшить производительность при использовании крошечных строк (например, strchr("abc", 'a');, но, конечно, не применительно к строкам любого большого размера.

То, верно ли это утверждение, во многом зависит от рассматриваемой микроархитектуры. Использованиеканоническая 4-этапная модель конвейера RISC, то это почти наверняка так, но очень трудно сказать, верно ли это для современного суперскалярного суперкадрового ЦП, в котором скорость ядра может сильно затормозить скорость потоковой памяти.В этом случае это не только правдоподобно, но и довольно часто, поскольку существует большой разрыв в «количестве команд, которые могут быть удалены» относительно «количества байтов, которые могут быть переданы», чтобы у вас было «количество»инструкции, которые могут быть удалены для каждого байта, который может быть передан ". Если он достаточно велик, команду ctz + можно выполнить" бесплатно ".

3 голосов
/ 19 марта 2011

Вот Реализация Python для поиска , используемая во всем ядре. Комментарии указывают, что он использует сжатую таблицу дельты Бойера-Мура .

Я провел довольно обширный эксперимент с поиском строк, но это было для нескольких строк поиска. Сборочные реализации Horspool и Bitap часто могут противостоять алгоритмам, таким как Aho-Corasick для низкого количества образцов.

2 голосов
/ 17 ноября 2010

Использовать stdlib strstr:

char *foundit = strstr(haystack, needle);

Это было очень быстро, мне потребовалось около 5 секунд, чтобы напечатать.

2 голосов
/ 07 июля 2010

Возможно, вы захотите использовать различные тесты для нескольких типов строк, так как это может сильно повлиять на производительность.Алгоритмы будут выполнять различие, основанное на поиске естественного языка (и даже здесь все еще могут быть мелкозернистые различия из-за различных морфологий), строк ДНК или случайных строк и т. Д.

Размер алфавита будет играть роль во многих алгоритмах, как будет размер иглы.Например, Хорспул хорошо работает с английским текстом, но плохо с ДНК из-за разного размера алфавита, что усложняет жизнь правилу плохих персонажей.Введение хорошего суффикса значительно облегчает это.

0 голосов
/ 19 февраля 2019

Это не дает прямого ответа на вопрос, но если текст очень большой, как насчет того, чтобы разделить его на перекрывающиеся разделы (перекрывающиеся по длине шаблона), а затем одновременно искать разделы, используя потоки.Что касается самого быстрого алгоритма, Бойер-Мур-Хорспул, я думаю, является одним из самых быстрых, если не самый быстрый среди вариантов Бойер-Мур.Я опубликовал пару вариантов Бойера-Мура (я не знаю их названия) в этой теме Алгоритм быстрее, чем BMH (Бойер-Мур-Хорспул) Поиск .

0 голосов
/ 06 июля 2010

Не знаю, лучший ли это, но у меня был хороший опыт с Бойер-Мур .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...