Оптимизированная версия strstr (поиск имеет постоянную длину)

В моей программе C было много вызовов функций strstr. Стандартная библиотека strstr уже быстрая, но в моем случае строка поиска всегда имеет длину 5 символов. Я заменил его специальной версией, чтобы получить некоторую скорость:

int strstr5(const char *cs, const char *ct)
{
    while (cs[4]) {

        if (cs[0] == ct[0] && cs[1] == ct[1] && cs[2] == ct[2] && cs[3] == ct[3] && cs[4] == ct[4])
            return 1;

        cs++;
    }

    return 0;
}

Функция возвращает целое число, потому что достаточно знать, имеет ли ct cs. Моя функция проста и быстрее, чем стандартная strstr в этом специальном случае, но мне интересно узнать, есть ли у кого-нибудь улучшения производительности, которые могут быть применены. Даже небольшие улучшения приветствуются.

Резюме:

  • cs имеет длину >= 10, но в остальном он может меняться. Длина известна ранее (не используется в моей функции). Длина cs обычно составляет от 100 до 200.
  • ct имеет длину 5
  • Содержимое строк может быть любым

Изменить: Спасибо за все ответы и комментарии. Я должен изучать и тестировать идеи, чтобы увидеть, что лучше всего работает. Я начну с идеи MAK о суффиксе trie.

Ответы

Ответ 1

Существует несколько быстрых строковых алгоритмов поиска. Попробуйте посмотреть Boyer-Moore (как уже было предложено Грегом Хьюджиллом), Rabin -Karp и KMP.

Если вам нужно найти множество маленьких шаблонов в одном большом тексте, вы также можете попробовать создать дерево суффиксов массив суффикса. Но это IMHO несколько сложнее понять и реализовать правильно.

Но будьте осторожны, эти методы очень быстрые, но только дают вам заметное ускорение, если задействованные строки очень большие. Возможно, вы не увидите заметного ускорения для строк меньше, чем 1000 символов.

EDIT:

Если вы просматриваете один и тот же текст снова и снова (т.е. значение cs всегда/часто одно и то же для всех вызовов), вы получите большое ускорение с помощью суффикса trie (в основном a trie суффиксов). Так как ваш текст не превышает 100 или 200 символов, вы можете использовать более простой метод O (n ^ 2) для построения trie, а затем выполнить несколько быстрых поисков. Для каждого поиска требуется только 5 сравнений вместо обычных 5 * 200.

Изменить 2:

Как уже упоминалось в комментарии caf, алгоритм C strstr зависит от реализации. glibc использует алгоритм линейного времени, который должен быть более или менее столь же быстрым на практике, как и любой из методов, которые я упомянул. В то время как метод OP асимптотически медленнее (O (N * m) вместо O (n)), скорее всего это связано с тем, что как n, так и m (длины рисунка и текста) очень малы, и это не нужно выполнять какую-либо длительную предварительную обработку в версии glibc.

Ответ 2

Уменьшение количества сравнений приведет к увеличению скорости поиска. Сохраните запущенную строку int и сравните ее с фиксированным int для поискового запроса. Если он сопоставляет последний символ.

uint32_t term = ct[0] << 24 | ct[1] << 16 | ct[2] << 8 | ct[3];
uint32_t walk = cs[0] << 24 | cs[1] << 16 | cs[2] << 8 | cs[3];
int i = 0;

do {
  if ( term == walk && ct[4] == cs[4] ) { return i; } // or return cs or 1
  walk = ( walk << 8 ) | cs[4];
  cs += 1;
  i += 1;
} while ( cs[4] ); // assumes original cs was longer than ct
// return failure

Добавьте проверки для коротких cs.

Edit:

Добавлены исправления из комментариев. Спасибо.

Это можно легко использовать для использования 64-битных значений. Вы можете хранить cs [4] и ct [4] в локальных переменных вместо того, чтобы предполагать, что компилятор сделает это за вас. Вы можете добавить 4 к cs и ct перед циклом и использовать cs [0] и ct [0] в цикле.

Ответ 3

Интерфейс strstr накладывает некоторые ограничения, которые могут быть избиты. Он принимает строки с нулевым завершением, и любой конкурент, который сначала выполняет "strlen" своей цели, теряет. Он не принимает аргумент "state", поэтому затраты на установку не могут быть амортизированы во многих вызовах (скажем) одной и той же целью или шаблоном. Ожидается, что он будет работать с широким спектром входных данных, включая очень короткие цели/шаблоны и патологические данные (рассмотрите поиск "ABABAC" в строке "ABABABABAB... C" ). libc также теперь зависит от платформы. В мире x86-64 SSE2 составляет семь лет, а libc strlen и strchr с использованием SSE2 на 6-8 раз быстрее, чем наивные алгоритмы. На платформах Intel, поддерживающих SSE4.2, strstr использует инструкцию PCMPESTRI. Но вы тоже можете победить.

Boyer-Moore (и Turbo B-M и Backward Oracle Matching и др.) имеют время настройки, которое в значительной степени выбивает их из работы, даже не считая проблему с нулевой конечной строкой. Horspool - это ограниченный B-M, который хорошо работает на практике, но не очень хорошо справляется с краевыми случаями. Лучшее, что я нашел в этом поле, - это BNDM ( "Обратное недетерминированное соответствие Direct-Ациклическое-Word-Graph Matching" ), реализация которого меньше его имени: -)

Вот несколько фрагментов кода, которые могут представлять интерес. Интеллектуальный SSE2 превосходит наивный SSE4.2 и обрабатывает проблему с нулевым завершением. Реализация BNDM показывает один из способов сохранения затрат на установку. Если вы знакомы с Horspool, вы заметите сходство, за исключением того, что BNDM использует битмаски вместо скипов. Я собираюсь опубликовать, как решить проблему с нулевым терминатором (эффективно) для алгоритмов суффикса, таких как Horspool и BNDM.

Общий атрибут всех хороших решений разбивается на разные алгоритмы для разных длин аргументов. Примером является функция Sanmayce "Railgun" .

Ответ 4

Ваш код может получить доступ к cs за пределами его выделения, если cs короче 4 символов.

Общей оптимизацией для поиска строк является использование алгоритма Boyer-Moore, где вы начинаете искать в cs с конца того, что будет ct. См. Связанную страницу для полного описания алгоритма.

Ответ 5

Вы не будете бить хорошую реализацию на современном компьютере x86.

Новые процессоры Intel имеют инструкцию, которая берет 16 байтов исследуемой строки, до 16 байтов строки поиска, а в одной команде возвращает, которая является первой позицией байта, в которой может быть строка поиска (или здесь ничего нет). Например, если вы ищете "Hello" в строке "abcdefghijklmnHexyz", первая инструкция сообщит вам, что строка "Hello" может начинаться со смещения 14 (поскольку при чтении 16 байтов процессор имеет байты H, e, неизвестные, которые могут это местоположение "Hello" . Следующая инструкция, начинающаяся со смещения 14, сообщает, что строки там нет. И да, она знает о завершении нулевых байтов.

Это две инструкции, чтобы найти, что строка с пятью символами отсутствует в 19-символьной строке. Попробуйте побить это с помощью любого специального кода случая. (Очевидно, что это построено специально для strstr, strcmp и подобных инструкций).