Я должен записать функцию C/C++, которая быстро проверила бы, заканчивается ли строка одним из ~1000 предопределенных суффиксов. Конкретно строка является именем хоста, и я должен проверить, принадлежит ли она одному из нескольких сотен предопределенных доменов второго уровня.
Эта функция будет вызвана много, таким образом, она должна будет быть записана максимально эффективно. Поразрядные взломы и т.д., что-либо идет, пока это оказывается быстрым.
Набор суффиксов предопределяется во время компиляции и не изменяется.
Я думаю или реализую изменение Rabin-Karp или пишу инструмент, который генерировал бы функцию с вложенной IFS и переключателями, которые будут пользовательские адаптированный в соответствии с определенным набором суффиксов. Так как рассматриваемое приложение является 64-разрядным для ускорения сравнений, я мог сохранить суффиксы до 8 байтов в длине как сортированный массив константы и сделать двоичный поиск в нем.
Есть ли какие-либо другие разумные опции?
После некоторых исследований и размышлений Я решил пойти с подходом к trie / конечному автомату.
Строка анализируется, начиная с последнего символа, идущего в обратном направлении, с использованием TRIE до тех пор, пока часть суффикса, которая была проанализирована до сих пор, может соответствовать нескольким суффиксам. В какой-то момент мы либо попадаем в первый символ одного из возможных суффиксов, что означает, что у нас есть совпадение, заходим в тупик, что означает, что больше нет возможных совпадений, либо попадаем в ситуацию, когда есть только один кандидат на суффикс. В этом случае мы просто сравниваем остаток суффикса.
Поскольку поиск в дереве выполняется постоянно, сложность в худшем случае равна o (максимальная длина суффикса). Функция оказалась довольно быстрой. На процессоре Core i5 с тактовой частотой 2,8 ГГц он может проверять 33 000 000 строк в секунду на предмет возможных суффиксов 2K. Суффиксы 2K общим объемом 18 килобайт, расширенные до 320kb trie / table machine.Я предполагаю, что мог бы сохранить его более эффективно, но на данный момент это решение, похоже, работает достаточно хорошо.
Поскольку список суффиксов был таким большим, я не хотел кодировать все вручную, поэтому в итоге я написал приложение на C #, которое генерировало C-код для функции проверки суффиксов:
public static uint GetFourBytes(string s, int index)
{
byte[] bytes = new byte[4] { 0, 0, 0, 0};
int len = Math.Min(s.Length - index, 4);
Encoding.ASCII.GetBytes(s, index, len, bytes, 0);
return BitConverter.ToUInt32(bytes, 0);
}
public static string ReverseString(string s)
{
char[] chars = s.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
static StringBuilder trieArray = new StringBuilder();
static int trieArraySize = 0;
static void Main(string[] args)
{
// read all non-empty lines from input file
var suffixes = File
.ReadAllLines(@"suffixes.txt")
.Where(l => !string.IsNullOrEmpty(l));
var reversedSuffixes = suffixes
.Select(s => ReverseString(s));
int start = CreateTrieNode(reversedSuffixes, "");
string outFName = @"checkStringSuffix.debug.h";
if (args.Length != 0 && args[0] == "--release")
{
outFName = @"checkStringSuffix.h";
}
using (StreamWriter wrt = new StreamWriter(outFName))
{
wrt.WriteLine(
"#pragma once\n\n" +
"#define TRIE_NONE -1000000\n"+
"#define TRIE_DONE -2000000\n\n"
);
wrt.WriteLine("const int trieArray[] = {{{0}\n}};", trieArray);
wrt.WriteLine(
"inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {\n"+
" int len = trie[0];\n"+
" if (curr - str < len) return false;\n"+
" const char* cmp = (const char*)(trie + 1);\n"+
" while (len-- > 0) {\n"+
" if (*--curr != *cmp++) return false;\n"+
" }\n"+
" return true;\n"+
"}\n\n"+
"bool checkStringSuffix(const char* str, int len) {\n" +
" if (len < " + suffixes.Select(s => s.Length).Min().ToString() + ") return false;\n" +
" const char* curr = (str + len - 1);\n"+
" int currTrie = " + start.ToString() + ";\n"+
" while (curr >= str) {\n" +
" assert(*curr >= 0x20 && *curr <= 0x7f);\n" +
" currTrie = trieArray[currTrie + *curr - 0x20];\n" +
" if (currTrie < 0) {\n" +
" if (currTrie == TRIE_NONE) return false;\n" +
" if (currTrie == TRIE_DONE) return true;\n" +
" return checkSingleSuffix(str, curr, trieArray - currTrie - 1);\n" +
" }\n"+
" --curr;\n"+
" }\n" +
" return false;\n"+
"}\n"
);
}
}
private static int CreateTrieNode(IEnumerable<string> suffixes, string prefix)
{
int retVal = trieArraySize;
if (suffixes.Count() == 1)
{
string theSuffix = suffixes.Single();
trieArray.AppendFormat("\n\t/* {1} - {2} */ {0}, ", theSuffix.Length, trieArraySize, prefix);
++trieArraySize;
for (int i = 0; i < theSuffix.Length; i += 4)
{
trieArray.AppendFormat("0x{0:X}, ", GetFourBytes(theSuffix, i));
++trieArraySize;
}
retVal = -(retVal + 1);
}
else
{
var groupByFirstChar =
from s in suffixes
let first = s[0]
let remainder = s.Substring(1)
group remainder by first;
string[] trieIndexes = new string[0x60];
for (int i = 0; i < trieIndexes.Length; ++i)
{
trieIndexes[i] = "TRIE_NONE";
}
foreach (var g in groupByFirstChar)
{
if (g.Any(s => s == string.Empty))
{
trieIndexes[g.Key - 0x20] = "TRIE_DONE";
continue;
}
trieIndexes[g.Key - 0x20] = CreateTrieNode(g, g.Key + prefix).ToString();
}
trieArray.AppendFormat("\n\t/* {1} - {2} */ {0},", string.Join(", ", trieIndexes), trieArraySize, prefix);
retVal = trieArraySize;
trieArraySize += 0x60;
}
return retVal;
}
Таким образом, он генерирует такой код:
inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {
int len = trie[0];
if (curr - str < len) return false;
const char* cmp = (const char*)(trie + 1);
while (len-- > 0) {
if (*--curr != *cmp++) return false;
}
return true;
}
bool checkStringSuffix(const char* str, int len) {
if (len < 5) return false;
const char* curr = (str + len - 1);
int currTrie = 81921;
while (curr >= str) {
assert(*curr >= 0x20 && *curr <= 0x7f);
currTrie = trieArray[currTrie + *curr - 0x20];
if (currTrie < 0) {
if (currTrie == TRIE_NONE) return false;
if (currTrie == TRIE_DONE) return true;
return checkSingleSuffix(str, curr, trieArray - currTrie - 1);
}
--curr;
}
return false;
}
Поскольку для моего конкретного набора данных len в checkSingleSuffix никогда не было больше 9, я попытался заменить цикл сравнения переключателем (len) и жестко запрограммированными процедурами сравнения, которые сравнивали до 8 байтов данных за раз, но это не повлияло общая производительность в любом случае.
Спасибо всем, кто поделился своими идеями!
Вы упомянули, что смотрите только на доменные имена второго уровня, поэтому, даже не зная точного набора совпадающих доменов, вы можете извлечь соответствующую часть входной строки.
Тогда просто используйте хеш-таблицу. Измерьте его таким образом, чтобы не было столкновений, поэтому вам не нужны ведра; поиск будет точно O (1). Для небольших типов хэшей (например, 32 бита) вам нужно проверить, действительно ли совпадают строки. Для 64-битного хэша вероятность столкновения другого домена с одним из хешей в вашей таблице уже настолько мала (порядка 10 ^ -17), что вы, вероятно, сможете с этим жить.
Просто создайте массив 26x26 из набора доменов. например thisArray [0] [0] - это домены, заканчивающиеся на «aa», thisArray [0] [1] - это все домены, которые заканчиваются на «ab» и так далее ...
Как только у вас есть это, просто найдите в своем массиве thisArray [2-й последний символ имени хоста] [последний символ имени хоста], чтобы получить возможные домены. Если на этом этапе их несколько, просто переберите остальные.
Я думаю, что решение должно сильно отличаться в зависимости от типа входных строк. Если строки представляют собой какой-то класс строк, который может повторяться с конца (например, строки stl), это намного проще, чем если бы они были C-строками с завершающим NULL.
Итерация строки в обратном направлении (не делайте обратную копию - используйте какой-нибудь обратный итератор). Создайте Trie, в котором каждый узел состоит из двух 64-битных слов, одного шаблона и одной битовой маски. Затем проверяйте 8 символов за раз на каждом уровне. Маска используется, если вы хотите сопоставить менее 8 символов - например, deny "* .org" даст маску с 32 битами. Маска также используется в качестве критерия завершения.
Создайте NDFA, который сопоставляет строки за один проход по ним. Таким образом, вам не нужно сначала выполнять итерацию до конца, а вместо этого можно использовать ее за один проход. NDFA можно преобразовать в DFA, что, вероятно, сделает реализацию более эффективной. Как создание NDFA, так и преобразование в DFA, вероятно, будут настолько сложными, что вам придется писать инструменты для этого.
Я бы перевернул все строки суффиксов, построил из них префиксное дерево, а затем протестировал бы обратную строку вашего IP на это.
Я думаю, что создание собственных автоматов было бы наиболее эффективным способом ... это своего рода ваше второе решение, согласно которому, исходя из конечного набора суффиксов, он генерирует автомат, подходящий для этих суффиксов.
Я думаю, вы можете легко использовать flex , чтобы сделать это, позаботившись об изменении ввода или обработки особым образом, поскольку вы ищете только суффиксы (просто для повышения эффективности) ..
Кстати, использование подхода Рабина-Карпа также будет эффективным, поскольку ваши суффиксы будут короткими. Вы можете установить хэш-набор со всеми необходимыми суффиксами, а затем
Если суффиксы не содержат никаких расширений / правил (например, регулярное выражение), вы можете построить Trie суффиксов в обратном порядке, а затем сопоставить строку на основе этого. Например,
suffixes:
foo
bar
bao
reverse order suffix trie:
o
-a-b (matches bao)
-o-f (matches foo)
r-a-b (matches bar)
Затем их можно использовать для сопоставления вашей строки:
"mystringfoo" -> reverse -> "oofgnirtsym" -> trie match -> foo suffix