У меня есть случай использования, где набор строк будет искать определенную строку, s
, Процент совпадений или положительных совпадений для этих поисков будет очень высоким. Скажем, 99% + времени, s
будет в наборе.
я использую boost::unordered_set
прямо сейчас, и даже с его очень быстрый алгоритм хеширования, занимает около 40мс 600 мс на хорошее оборудование ВМ для поиска набора 500 000 раз. Да, это довольно хорошо, но неприемлемо для того, над чем я работаю.
Итак, существует ли какая-либо структура данных, оптимизированная для высокого процента попаданий? Я не могу предварительно вычислить хэши для входящих строк, поэтому я думаю, что я смотрю на сложность \ $ O (средняя длина строки) \ $ для хеш-набора, например boost::unordered_set
, Я посмотрел на Tries, они, вероятно, будут хорошо работать в противоположном случае, когда есть хиты, но на самом деле не лучше, чем хэш-сеты.
редактировать: некоторые другие детали с моим конкретным вариантом использования:
количество строк в наборе составляет около 5000. Самая длинная строка, вероятно, не более 200 символов. Поиск вызывается снова и снова с теми же строками, но они поступают из внешней системы, и я не могу предсказать, какой будет следующая строка. Точный коэффициент совпадения на самом деле составляет 99,975%.
edit2: Я сделал некоторые из моих собственных тестов
Я собрал 5000 строк, которые встречаются в реальной системе. Я создал два сценария.
1) Я перебираю список известных строк и выполняю поиск их в контейнере. Я делаю это для 500 000 поисков («хиты«).
2) Я перебираю набор строк, о которых известно, что их нет в контейнере, для 500 000 запросов («пропускает«).
(Примечание: я заинтересован в хешировании данных в обратном порядке, потому что, глядя на мои данные, я заметил, что существует много общих префиксов и суффиксов, отличающихся друг от друга — по крайней мере, так это выглядит.)
Тесты проводились на виртуальной коробке CentOS 5.6, работающей на хосте macbook.
hits (ms) misses (ms)
boost::unordered_set with default hash and no reserved size: 591.15 441.39
tr1::unordered_set with default hash 191.09 143.80
boost::unordered_set with a reserve size set: 579.31 431.54
boost::unordered_set w/custom hash (hash on the last 15 chars + str size): 357.34 812.13
boost::unordered_set w/custom hash (hash on the last 25 chars + str size): 362.60 795.33
trie: 1809.34 58.11
trie with reversed insertion/search: 2806.26 311.14
В моих тестах, где есть много совпадений, набор tr1 является лучшим. Там, где много промахов, «Три» побеждает.
мой цикл тестирования выглядит следующим образом, где function_set — это тестируемый контейнер, загруженный 5000 строк, а functions — вектор либо всех строк в контейнере, либо набора строк, которых нет в контейнере.
while (searched < kTotalSearches) {
for(std::vector<std::string>::const_iterator i = functions.begin(); i != functions.end(); ++i) {
function_set.count(*i);
searched++;
if (searched == kTotalSearches)
break;
}
}
std::cout << searched << " searches." << std::endl;
Этот вопрос пробудил мое любопытство, поэтому я провел несколько тестов, чтобы удовлетворить себя следующими результатами. Несколько общих замечаний:
Базовое описание испытаний с соответствующими деталями:
std::unordered_map<std::string, int>
std::unordered_set<std::string>
boost::unordered_set<std::string>
, v1.47.0std::unordered_map<const char*, int>
std::unordered_set<const char*>
std::unordered_map<>
используя собственный алгоритм хеширования FNV-1a.std::unordered_set<>
используя собственный алгоритм хеширования FNV-1a.std::binary_search()
на сортированном std::vector<std::string>
,size_t VectorIndex[26][26][26][26][26]
массив для индексации в отсортированный массив.std::unordered_map<>
используя идеальный хеш из Gperf.is_word_set()
функционировать напрямую.Результаты сортируются от самого медленного к быстрому (для большинства хитов ~ 99,975%):
Результаты сортируются от самого медленного до самого быстрого (в случае большинства промахов ~ 0,1% хитов):
Мое первое предположение состояло в том, что три было бы хорошо подходит для такого рода вещей, но на основании результатов, на самом деле, все наоборот. Думая об этом еще немного, это имеет смысл и по тем же причинам не использовать связанный список.
Я полагаю, вы можете быть знакомы с таблица задержек что каждый программист должен знать. В вашем случае у вас есть 500 тысяч просмотров, выполняющихся за 40 мс, или 80 нс / поиск. При таком масштабе вы легко проиграете, если получите доступ к тому, чего еще нет в кеше L1 / L2. Это очень плохо, потому что у вас есть косвенный и, вероятно, нелокальный доступ к памяти для каждого символа. Принимая во внимание размер дерева в этом случае, я не мог придумать, как заставить все дерево поместиться в кэш для повышения производительности (хотя это может быть возможно). Я все еще думаю, что даже если бы вы сделали так, чтобы три полностью помещалось в кэш L2, вы проиграли бы со всей необходимой косвенностью.
std::unordered_
Контейнеры на самом деле очень хорошо справляются со своими задачами. На самом деле, пытаясь ускорить их, я на самом деле сделал их медленнее (в исследованиях под FastMap и FastSet).
То же самое с попыткой переключиться с std::string
в const char *
(примерно в два раза медленнее).
boost::unordered_set<>
был в два раза медленнее, чем std::unordered_set<>
и я не знаю, потому ли это, что я просто использовал встроенную хэш-функцию, использовал немного старую версию boost или что-то еще. Ты пытался std::unordered_set<>
сам?
Используя gperf
Вы можете легко создать идеальную хеш-функцию, если ваш набор строк известен во время компиляции. Вы также можете создать идеальный хеш во время выполнения, в зависимости от того, как часто новые строки добавляются на карту. Это дает вам увеличение скорости на 23% по сравнению со стандартной реализацией карты.
PerfectWordSetThread тесты просто используют идеальный хеш и разбивают работу на 1-32 потока. Эта проблема совершенно параллельна (по крайней мере, тест), поэтому вы получаете почти 5-кратное повышение производительности в случае с 16 потоками. Это дает только 6,3 мс / 500 тыс. Просмотров или 13 нс / поиск … всего 50 циклов на процессоре 4 ГГц.
StringIteration Дело действительно указывает на то, как трудно будет стать намного быстрее. Простая итерация найденной строки занимает 350 мс или 70% времени по сравнению со случаем отображения 500 мс. Даже если бы вы могли точно угадать каждую строку, вам все равно потребовалось бы это 350 мс (для 10 миллионов поисков), чтобы фактически сравнить и проверить соответствие.
Редактировать: Другая вещь, которая иллюстрирует, насколько трудны вещи, — это разница между PerfectWordSetFunc в 4050 мс и PerfectWordSet при 500 мс. Единственное различие между ними состоит в том, что один вызывается в функции, а другой — встроенный. Вызов его как функции снижает скорость в 8 раз. В базовом псевдокоде это просто:
bool IsInPerfectWordSet (string Match)
{
return in_word_set(Match);
}
//Inline benchmark: PerfectWordSet
for i = 1 to 10,000,000
{
if (in_word_set(SomeString)) ++MatchCount;
}
//Function call benchmark: PerfectWordSetFunc
for i = 1 to 10,000,000
{
if (IsInPerfectWordSet(SomeString)) ++MatchCount;
}
Это действительно подчеркивает разницу в производительности, которую может внести встроенный код / функции. Вы также должны быть осторожны, чтобы убедиться, что вы измеряете в тесте. Иногда вы хотели бы включить накладные расходы вызова функции, а иногда нет.
Я научился никогда не говорить «нет» на этот вопрос, но в какой-то момент усилия могут не стоить того. Если вы можете разделить поиски на потоки и использовать идеальную или почти идеальную хеш-функцию, вы сможете достичь 100 миллионов совпадений поиска в секунду (вероятно, больше на машине с несколькими физическими процессорами).
Пара идей, на которые у меня нет знаний:
Найдите минутку, чтобы рассмотреть # 3 …. самый быстрый код — это тот, который никогда не должен запускаться. Если вы можете уменьшить количество поисков или уменьшить потребность в чрезвычайно высокой пропускной способности, вам не придется тратить время на микрооптимизацию функции окончательного поиска.
Я уверен, что Трис это то, что вы ищете. Вы гарантированно не уменьшите количество узлов, превышающих длину вашей строки. Как только вы достигли листа, тогда может быть некоторый линейный поиск, если есть столкновения для этого конкретного узла. Это зависит от того, как вы его построите. Поскольку вы используете набор, я бы предположил, что это не проблема.
Сложность unordered_set будет в худшем случае O (n), но в данном случае n — это количество узлов, которые у вас есть (500 КБ), а не количество символов, которые вы ищете (вероятно, менее 500 КБ).
После редактирования:
Возможно, что вам действительно нужно, это кеш результатов после успешного поиска.
Если набор строк фиксирован во время компиляции (например, это словарь известных человеческих слов), вы можете использовать идеальный хэш алгоритм, и использовать Gperf генератор.
В противном случае вы можете использовать массив из 26 хеш-таблиц, проиндексированных по первой букве слова для хеширования.
Кстати, возможно, использование отсортированного массива этих строк с дихотомическим доступом может быть быстрее (так как log 5000 составляет около 13), или std::map
или std::set
,
Наконец, вы можете определить свою собственную функцию хэширования: возможно, в вашем конкретном случае хэширования будет достаточно только первых 16 байтов!
Если набор строк фиксирован, вы можете подумать о создании дихотомического поиска по нему (например, написать скрипт для генерации функции с 5000 тестами, но только с логом 5000, который выполняется).
Кроме того, даже если набор строк является слегка переменным (например, переход от одного запуска программы к следующему, но остается постоянным в течение одного запуска), вы можете даже подумать о создании функции (путем генерирования кода C ++, а затем его компиляции) в летать и dlopen
это
Вы действительно должны сравнить и попробовать несколько решений! Вероятно, это скорее инженерная проблема, чем алгоритмическая.