Эффективно проверьте строку на один из нескольких сотен возможных суффиксов

Я должен записать функцию C/C++, которая быстро проверила бы, заканчивается ли строка одним из ~1000 предопределенных суффиксов. Конкретно строка является именем хоста, и я должен проверить, принадлежит ли она одному из нескольких сотен предопределенных доменов второго уровня.

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

Набор суффиксов предопределяется во время компиляции и не изменяется.

Я думаю или реализую изменение Rabin-Karp или пишу инструмент, который генерировал бы функцию с вложенной IFS и переключателями, которые будут пользовательские адаптированный в соответствии с определенным набором суффиксов. Так как рассматриваемое приложение является 64-разрядным для ускорения сравнений, я мог сохранить суффиксы до 8 байтов в длине как сортированный массив константы и сделать двоичный поиск в нем.

Есть ли какие-либо другие разумные опции?

7
задан Ghostrider 4 April 2010 в 17:09
поделиться

7 ответов

После некоторых исследований и размышлений Я решил пойти с подходом к 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 байтов данных за раз, но это не повлияло общая производительность в любом случае.

Спасибо всем, кто поделился своими идеями!

0
ответ дан 6 December 2019 в 09:18
поделиться

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

Тогда просто используйте хеш-таблицу. Измерьте его таким образом, чтобы не было столкновений, поэтому вам не нужны ведра; поиск будет точно O (1). Для небольших типов хэшей (например, 32 бита) вам нужно проверить, действительно ли совпадают строки. Для 64-битного хэша вероятность столкновения другого домена с одним из хешей в вашей таблице уже настолько мала (порядка 10 ^ -17), что вы, вероятно, сможете с этим жить.

4
ответ дан 6 December 2019 в 09:18
поделиться

Просто создайте массив 26x26 из набора доменов. например thisArray [0] [0] - это домены, заканчивающиеся на «aa», thisArray [0] [1] - это все домены, которые заканчиваются на «ab» и так далее ...

Как только у вас есть это, просто найдите в своем массиве thisArray [2-й последний символ имени хоста] [последний символ имени хоста], чтобы получить возможные домены. Если на этом этапе их несколько, просто переберите остальные.

0
ответ дан 6 December 2019 в 09:18
поделиться

Я думаю, что решение должно сильно отличаться в зависимости от типа входных строк. Если строки представляют собой какой-то класс строк, который может повторяться с конца (например, строки stl), это намного проще, чем если бы они были C-строками с завершающим NULL.

String Class

Итерация строки в обратном направлении (не делайте обратную копию - используйте какой-нибудь обратный итератор). Создайте Trie, в котором каждый узел состоит из двух 64-битных слов, одного шаблона и одной битовой маски. Затем проверяйте 8 символов за раз на каждом уровне. Маска используется, если вы хотите сопоставить менее 8 символов - например, deny "* .org" даст маску с 32 битами. Маска также используется в качестве критерия завершения.

C-строки

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

0
ответ дан 6 December 2019 в 09:18
поделиться

Я бы перевернул все строки суффиксов, построил из них префиксное дерево, а затем протестировал бы обратную строку вашего IP на это.

3
ответ дан 6 December 2019 в 09:18
поделиться

Я думаю, что создание собственных автоматов было бы наиболее эффективным способом ... это своего рода ваше второе решение, согласно которому, исходя из конечного набора суффиксов, он генерирует автомат, подходящий для этих суффиксов.

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

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

  • взять строку
  • взять суффикс
  • вычислить хэш суффикса
  • проверить, есть ли суффикс в таблице
2
ответ дан 6 December 2019 в 09:18
поделиться

Если суффиксы не содержат никаких расширений / правил (например, регулярное выражение), вы можете построить 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
11
ответ дан 6 December 2019 в 09:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: