Экономичная в памяти структура для сортированного текста с поддержкой поиска по префиксу - PullRequest
14 голосов
/ 31 августа 2009

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

У меня достаточно данных:

  • около 450M в простом текстовом листинге Unix-формата на диске
  • около 8 миллионов строк
  • GZIP по умолчанию сжимает до 31M
  • bzip2 по умолчанию сжимает до 21M

Я не хочу есть где-то близко к 450M в памяти. На этом этапе я был бы рад использовать где-то около 100M, поскольку существует множество избыточностей в виде префиксов.

Я использую C # для этой работы, и для простой реализации дерева все равно потребуется один листовой узел для каждой строки в файле. Учитывая, что каждому листовому узлу потребуется какая-то ссылка на последний фрагмент текста (32 бита, скажем, индекс в массив строковых данных, чтобы минимизировать дублирование строки), а служебные данные объекта CLR составляют 8 байтов (проверено с помощью windbg / SOS) , Я буду тратить> 96 000 000 байтов на структурные издержки без сохранения текста вообще.

Давайте посмотрим на некоторые статистические атрибуты данных. При наполнении деревом:

  • всего уникальных "кусков" текста около 1,1 млн.
  • Всего уникальных фрагментов около 16M на диске в текстовом файле
  • средняя длина фрагмента составляет 5,5 символов, макс. 136
  • без учета дубликатов, всего около 52 миллионов символов в кусках
  • Внутренние узлы Trie в среднем около 6,5 детей с макс. 44
  • около 1,8 м внутренних узлов.

Превышение скорости создания листа составляет около 15%, избыточное создание внутреннего узла составляет 22% - под избыточным созданием я подразумеваю листья и внутренние узлы, созданные во время построения дерева, но не в конечном дереве как пропорцию от конечного числа узлов каждого типа.

Вот анализ кучи из SOS, показывающий, где больше всего памяти используется:

 [MT    ]--[Count]----[   Size]-[Class                                          ]
 03563150       11         1584 System.Collections.Hashtable+bucket[]
 03561630       24         4636 System.Char[]
 03563470        8         6000 System.Byte[]
 00193558      425        74788      Free
 00984ac8    14457       462624 MiniList`1+<GetEnumerator>d__0[[StringTrie+Node]]
 03562b9c        6     11573372 System.Int32[]
*009835a0  1456066     23297056 StringTrie+InteriorNode
 035576dc        1     46292000 Dictionary`2+Entry[[String],[Int32]][]
*035341d0  1456085     69730164 System.Object[]
*03560a00  1747257     80435032 System.String
*00983a54  8052746     96632952 StringTrie+LeafNode

Dictionary<string,int> используется для сопоставления кусочков строк с индексами в List<string>, и его можно отбросить после построения trie, хотя GC, похоже, не удаляет его (пара явных коллекций была сделана до этого dump) - !gcroot в SOS не указывает никаких корней, но я ожидаю, что более поздний GC освободит его.

MiniList<T> является заменой List<T> с использованием точного размера (то есть линейного роста, O(n^2) производительности сложения) T[] во избежание потери пространства; это тип значения и используется InteriorNode для отслеживания детей. Это T[] добавляется в стопку System.Object[].

Итак, если я суммирую «интересные» элементы (помеченные *), я получу около 270M, что лучше, чем необработанный текст на диске, но все еще не достаточно близко к моей цели. Я подумал, что издержки объекта .NET были слишком большими, и создал новый «тонкий» три, используя только массивы типа значения для хранения данных:

class SlimTrie
{
    byte[] _stringData; // UTF8-encoded, 7-bit-encoded-length prefixed string data

    // indexed by _interiorChildIndex[n].._interiorChildIndex[n]+_interiorChildCount[n]
    // Indexes interior_node_index if negative (bitwise complement),
    // leaf_node_group if positive.
    int[] _interiorChildren;

    // The interior_node_index group - all arrays use same index.
    byte[] _interiorChildCount;
    int[] _interiorChildIndex; // indexes _interiorChildren
    int[] _interiorChunk; // indexes _stringData

    // The leaf_node_index group.
    int[] _leafNodes; // indexes _stringData

    // ...
}

Эта структура сократила объем данных до 139 МБ и до сих пор является эффективно проходимым механизмом для операций только для чтения. А поскольку это так просто, я могу легко сохранить его на диск и восстановить, чтобы избежать затрат на повторное создание дерева каждый раз.

Итак, есть ли предложения по более эффективным структурам для поиска префиксов, чем trie? Альтернативные подходы, которые я должен рассмотреть?

Ответы [ 3 ]

2 голосов
/ 31 августа 2009

Поскольку существует только 1,1 миллиона чанков, вы можете индексировать чанк, используя 24 бита вместо 32-х бит, и сэкономить там место.

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

1 голос
/ 31 августа 2009

Вы можете найти научную статью, связанную с вашей проблемой здесь (цитата авторов: «Эксперименты показывают, что наш индекс поддерживает быстрые запросы в пространстве, близком к тому, который достигается при сжатии строки словарь через gzip, bzip или ppmdi. "- но, к сожалению, статья только оплата). Я не уверен, насколько сложно реализовать эти идеи. У авторов этой статьи есть веб-сайт , где вы также можете найти реализации (в разделе «Коллекция индексов») различных алгоритмов сжатых индексов .

Если вы хотите продолжить свой подход, обязательно посетите веб-сайты о Критических битах и Корень дерева .

0 голосов
/ 31 августа 2009

Идея вне стен: вместо трех хеш-таблиц. Вы бы имели в памяти только хеш и строковые данные, возможно сжатые.

Или вы можете позволить себе прочитать одну страницу? Только хэш и положение файла в памяти, извлекают «страницу» со строками, совпадающими с этим хешем, предположительно небольшое количество упорядоченных строк, следовательно, очень быстро искать в случае коллизий.

...