Strstr быстрее, чем алгоритмы?

У меня есть файл, содержащий 21056 байтов.

Я написал программу на C, которая читает весь файл в буфер, а затем использует несколько алгоритмов поиска для поиска файла для токена, который содержит 82 символа.

Я использовал все реализации алгоритмов из "Exact String Matching Algorithms" . Я использовал: KMP, BM, TBM и Horspool. И затем я использовал strstr и сравнивал каждый из них.

Мне интересно, каждый раз, когда strstr превосходит все остальные алгоритмы. Единственное, что быстрее, это BM.

Не должен ли strstr быть самым медленным?

Здесь мой контрольный код с примером бенчмаркинга BM:

double get_time()
{
    LARGE_INTEGER t, f;
    QueryPerformanceCounter(&t);
    QueryPerformanceFrequency(&f);
    return (double)t.QuadPart/(double)f.QuadPart;
}
before = get_time();
BM(token, strlen(token), buffer, len);
after = get_time();
printf("Time: %f\n\n", after - before);

Может ли кто-нибудь объяснить мне, почему strstr превосходит другие поисковые алгоритмы? При необходимости я отправлю больше кода по запросу.

Ответы

Ответ 1

Почему, по-вашему, strstr должен быть медленнее всех остальных? Знаете ли вы, что использует алгоритм strstr? Я думаю, вполне вероятно, что strstr использует тонко настроенный, специфичный для процессора алгоритм с кодировкой сборки типа KMP или лучше. В этом случае у вас нет шансов выйти из него в C для таких небольших эталонных тестов.

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

Ответ 2

Horspool, KMP и др. оптимальны для минимизации количества байтовых сравнений.

Однако это не узкое место на современном процессоре. На процессоре x86/64 ваша строка загружается в кеш L1 в кусках ширины строки (обычно 64 байта). Независимо от того, насколько умен ваш алгоритм, если только он не дает вам шагов, которые больше, чем вы, вы ничего не получаете; и более сложный код Horspool (по крайней мере один поиск в таблице) не может конкурировать.

Кроме того, вы застряли со строковым ограничением "C" с нулевым завершением: SOMEWHERE код должен проверять каждый байт.

strstr(), как ожидается, будет оптимальным для широкого круга случаев; например поиск крошечных строк, таких как "\r\n" в короткой строке, а также гораздо более длинные, где у некоторых более разумных алгоритмов может быть надежда. Основной цикл strchr/memcmp довольно сложно превзойти весь диапазон вероятных входов.

Практически все x86-совместимые процессоры с 2003 года поддерживают SSE2. Если вы разобрали strlen()/x86 для glibc, возможно, вы заметили, что он использует некоторые операции SSE2 PCMPEQ и MOVMASK для поиска нулевого терминатора 16 байтов за раз. Решение настолько эффективно, что оно превосходит очевидный суперпростой цикл, для чего-либо длиннее пустой строки.

Я принял эту идею и придумал strstr(), который превосходит glibc strstr() для всех случаев, превышающих 1 байт, где относительная разница в значительной степени спорна. Если вам интересно, проверьте:

  • Конвергенция SSE2 и strstr()

  • Лучше strstr() без ASM-кода

    Если вы хотите увидеть решение, отличное от SSE2, которое доминирует над strstr() для целевых строк более 15 байт, проверьте:

    который использует многобайтовые сравнения, а не strchr(), чтобы найти точку, в которой нужно выполнить memcmp.

Кстати, вы, вероятно, уже выяснили, что операторы x86 REP SCASB/REP CMPSB попадают на их задницу на что-то большее, чем 32 байта, и не сильно улучшают короткие строки. Желание Intel уделяло этому немного больше внимания, чем добавлению операционных систем SSE4.2 "string".

Для струн, достаточно больших для материи, мои перфекционные тесты показывают, что BNDM лучше, чем Horspool, по всем направлениям. BNDM более терпимо относится к "патологическим" случаям, таким как цели, которые сильно повторяют последний байт рисунка. BNDM также может использовать SSE2 (128-битные регистры) таким образом, чтобы конкурировать с 32-разрядными регистрами по эффективности и затратам на запуск. Исходный код здесь.

Ответ 3

Не видя своего кода, трудно точно сказать. strstr сильно оптимизирован и обычно написан на языке ассемблера. Он делает такие вещи, как чтение данных по 4 байта за раз и сравнение их (при необходимости, при выравнивании по битам), чтобы минимизировать задержку памяти. Он также может использовать такие вещи, как SSE, для загрузки 16 байтов за раз. Если ваш код загружает только один байт за раз, он, вероятно, убивается латентностью памяти.

Используйте ваш отладчик и выполните разборку strstr - вы, вероятно, найдете там что-то интересное.

Ответ 4

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

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