Пространственно-эффективная структура в памяти для сортированного текста, поддерживающего поиск префикса
У меня проблема: мне нужен пространственно-эффективный поиск данных файловой системы на основе префикса пути к файлу. Другими словами, поиск в отсортированном тексте. Вы говорите, что используете три, и я думал то же самое. Проблема в том, что попытки не достаточно эффективны в пространстве, не без других трюков.
У меня есть достаточное количество данных:
- около 450M в текстовом формате Unix-формата на диске
- около 8 миллионов строк
- gzip по умолчанию сжимает до 31M
- bzip2 по умолчанию сжимает до 21M
Я не хочу есть где-нибудь около 450M в памяти. На этом этапе я был бы счастлив использовать где-то около 100 М, так как там много избыточности в виде префиксов.
Я использую С# для этого задания, и для простой реализации trie по-прежнему потребуется один лист node для каждой строки в файле. Учитывая, что для каждого листа node потребуется некоторая ссылка на последний фрагмент текста (32 бита, скажем, индекс в массив строковых данных для минимизации дублирования строк), а накладные расходы CLR - 8 байтов (проверено с использованием windbg/SOS), Я буду тратить > 96 000 000 байт на структурные издержки без текстового хранения.
Посмотрим на некоторые статистические атрибуты данных. Когда набивается в trie:
- всего уникальных "кусков" текста около 1,1 миллиона.
- общее количество уникальных кусков около 16 М на диске в текстовом файле
- средняя длина блока составляет 5,5 символов, max 136
- если не учитывать дубликаты, около 52 миллионов символов в кусках
- Внутренние три-узлы в среднем составляют около 6,5 детей с макс. 44
- около 1,8 м внутренних узлов.
Превышение скорости создания листьев составляет около 15%, избыточное внутреннее создание node составляет 22% - избыточное создание, я имею в виду листья и внутренние узлы, созданные во время построения trie, но не в финальном trie как доля от конечного числа узлов каждого типа.
Здесь представлен анализ кучи из 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, похоже, не удаляет его (было сделано несколько явных коллекций до этого дамп) - !gcroot
в SOS не указывает никаких корней, но я ожидаю, что позднее GC освободит его.
MiniList<T>
является заменой для List<T>
с использованием точности (то есть линейного роста, производительности O(n^2)
) T[]
, чтобы избежать потери пространства; это тип значения и используется InteriorNode
для отслеживания дочерних элементов. Этот T[]
добавляется в кучу System.Object[]
.
Итак, если я суммирую "интересные" элементы (помеченные *
), я получаю около 270 М, что лучше, чем сырой текст на диске, но все еще недостаточно близко к моей цели. Я понял, что накладные расходы .NET слишком много, и создал новое "тонкое" trie, используя только массивы типа значений для хранения данных:
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
// ...
}
Эта структура уменьшила объем данных до 139M и по-прежнему является эффективно проходящим trie для операций только для чтения. И поскольку это так просто, я могу тривиально сохранить его на диск и восстановить его, чтобы избежать затрат на воссоздание trie каждый раз.
Итак, любые предложения для более эффективных структур для поиска префикса, чем trie? Альтернативные подходы, которые я должен рассмотреть?
Ответы
Ответ 1
Поскольку всего 1,1 миллиона кусков, вы можете индексировать кусок, используя 24 бита вместо 32 бит и экономя там место.
Вы также можете сжать куски. Возможно, кодирование Хаффмана - хороший выбор. Я также хотел бы попробовать следующую стратегию: вместо использования символа в качестве символа для кодирования вы должны кодировать переходы символов. Поэтому вместо того, чтобы смотреть на вероятность появления символа, посмотрите на вероятность перехода в цепочке Markov, где состояние является текущим символом.
Ответ 2
Вы можете найти научную статью, связанную с вашей проблемой здесь (цитирование авторов: "Эксперименты показывают, что наш индекс поддерживает быстрые запросы в пределах занимаемая площадь, близкая к той, которая достигается сжатием строкового словаря через gzip, bzip или ppmdi". - но, к сожалению, бумага является только оплатой). Я не уверен, насколько сложно реализовать эти идеи. Авторы этой статьи веб-сайт, где вы можете найти также реализации (в разделе "Коллекция индексов" ) различных сжатых алгоритмов индекса.
Если вы хотите продолжить свой подход, не забудьте проверить веб-сайты о деревья бит-бит и Дерево Radix.
Ответ 3
Нестандартная идея: вместо трюма хеш-таблицы. У вас в памяти будет только хэш и строковые данные, возможно сжатые.
Или вы можете позволить себе читать одну страницу? Только хеш и позиция файла в памяти, извлекают "страницу" с линиями, соответствующими этому хешу, предположительно небольшому числу упорядоченных строк, поэтому очень быстро можно искать в случае столкновений.