Эффективное для памяти отображение строки в строку в Python (или C) - PullRequest
3 голосов
/ 26 октября 2010

Мне нужна структура данных с эффективным использованием памяти для хранения около миллиона пар ключ-значение, где ключи - это строки размером около 80 байтов, а значения - строки размером около 200 байтов, общий размер ключа и значения составляет около 280 МБ.,Мне также нужен эффективный поиск значения по ключу, предпочтительно хэш-карта.Объем служебной памяти должен быть как можно меньше, например, для 280 МБ полезных данных структура данных не должна использовать более 300 МБ виртуальной памяти (включая malloc() служебные данные и все остальное).Шаблон использования следующий: мы начинаем с пустой структуры данных и заполняем ее постепенно, никогда не меняя ключей и никогда не меняя длины значений.Кроме того, структура данных может поддерживать изменение длины значений за счет 100% -ной служебной информации (это означает, что для байтов значения x, х байтов могут быть потрачены впустую временно в неиспользуемом буферном пространстве).

Мне нужен чистый модуль Python, или встроенный модуль Python, или реализация C, предпочтительно с привязками (C) Python.Я бы предпочел, чтобы можно было сериализовать всю структуру данных на диск и быстро прочитать ее обратно.

Просто чтобы доказать, что такие небольшие издержки возможны, я создал простой дизайн с открытая адресация , хеш-таблица из 1,25 миллионов элементов, содержащая 4-байтовые указатели на блоки данных 1 МБ, блоки данных, содержащие длину ключа и значения в виде base-128 varints .Эта конструкция имеет важное ограничение: она не позволяет удалять или менять пары без потери их памяти.Согласно моим расчетам с 1 миллионом пар ключ-значение по 280 байт каждая, служебные данные составляют менее 3,6% (10 080 000 байт).Приведенные выше ограничения более щедры, они допускают 20 000 000 байтов служебных данных.

Я только что нашел http://www.pytables.org/, который обеспечивает быстрый доступ и эффективную упаковку данных.Я должен изучить его более внимательно, чтобы проверить, соответствует ли он моим потребностям.

Ответы [ 10 ]

7 голосов
/ 26 октября 2010

Хорошо, простой подход.

Используйте словарь Python для структуры данных. Я заполнил словарь Python 1 миллионом случайных пар ключ-значение, где ключ был 80 символов, а значение 200 символов. На моем компьютере потребовалось 360 844 Кбайт, что не соответствует вашим спецификациям и составляет не более 300 МБ, но я все равно предлагаю его в качестве решения, поскольку оно все еще довольно эффективно использует память.

Это также не соответствует вашему требованию иметь C API. Я не уверен, зачем вам нужен C, но так как вопрос помечен как Python и не имеет тега C, я предложу чистый Python, чтобы посмотреть, может ли он соответствовать всем требованиям.

Относительно настойчивости. Используйте модуль cPickle. Это очень быстро и, опять же, очень просто. Чтобы сохранить ваш словарь:

cPickle.dump(mydict, "myfile.pkl")

Чтобы обновить словарь:

mydict = cPickle.load("myfile.pkl")

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

6 голосов
/ 26 октября 2010

Если вы не планируете делать большое количество удалений, то это не так сложно.Удаление приводит к фрагментации.

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

Итак, вот что вы делаете.

Вы создаете массив:

struct {
    char value[80];
    char *data;
} key;

И вы сохраняете этот массив отсортированным.

Если ключи могут дублироваться, вам необходимо:

struct link {
    char *data;
    link *next;
}

struct {
    char value[80];
    link *data;
} key;

(My C ржавый, но это суть). У каждого ключа есть ключ, указывающий на связанный список значений.

Тогда поиск - это простой бинарный поиск.«Боль» заключается в поддержании этого массива и вставке / удалении ключей.Это не так больно, как кажется, но экономит много памяти, особенно в 64-битных системах.

То, что вы хотите уменьшить, это количество указателей.Указатели дороги, когда у вас много структур, заполненных указателями.В 64-битной системе указатель составляет 8 байтов.Таким образом, для одного указателя у вас уходит 8 МБ бюджета памяти.

Таким образом, затраты заключаются в создании массива, копировании и сжатии памяти (если вы «знаете», у вас будет миллион строк и вы сможете зафиксироватьзатем malloc (1000000 * sizeof (key)) сразу же, это сэкономит вам некоторое копирование во время расширения).

Но не бойтесь, как только он запущен, производительность довольно хорошая,Современные процессоры на самом деле неплохо копируют 100М блоков памяти.

Просто в стороне, я просто сделал что-то похожее на Java.На 64-битной JVM карта с 25M записями - это 2G оперативной памяти.Мое решение (с использованием методов, подобных этому) имеет около 600 млн.).Java использует больше указателей, чем C, но предпосылка та же.

6 голосов
/ 26 октября 2010

Мартийн упомянул об этом в комментарии (не уверен, почему люди комментируют с ответами), но я согласен: используйте SQLite. Вы должны попробовать и посмотреть, будет ли он соответствовать вашим потребностям.

5 голосов
/ 26 октября 2010

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

2 голосов
/ 27 октября 2010

Использование SQLite - хорошая идея. Быстрая реализация может определить, достаточно ли вы достаточно быстрые без особых усилий.


Если вы решите, что вы должны бросить свой собственный, я бы порекомендовал следующее:

Насколько хорошо вы можете предсказать количество пар или верхний предел для этого?
Насколько хорошо вы можете предсказать общий размер данных или верхний предел для этого?

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

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

Однако, если вам нужно выполнить какие-либо операции cmp / copy и т. Д. С этими строками, помните, что со следующими гарантиями вы можете выжать немного или много из этих строковых операций:

  • все элементы выровнены по процессору
  • все байты пэда (например, 0
  • вы можете безопасно читать «за» концом строки, если вы не пересекаете границу процессора

Хеш-таблица для индекса. Словарь также будет работать, но это имеет смысл, только если потенциальная деградация / перефразировка будет серьезной проблемой. Я не знаю какой-либо «стандартной» реализации хеш-таблицы для C, но она должна быть, верно? право? Просто замените распределение на вызовы распределителю арены.


Местонахождение памяти

Если вы можете гарантировать, что поиск никогда не запросит строку, которой нет на карте, вам следует хранить ключи на отдельной арене, так как они нужны только при коллизиях хешей. Это может значительно улучшить локальность памяти. (В этом случае, если у вас когда-либо будет «финальная» таблица, вы можете даже скопировать сталкивающиеся ключи на новую арену и выбросить все остальные. Однако преимущества этого, вероятно, незначительны.)

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


Если вам нужно поддерживать «забавные символы» / Unicode, нормализуйте ваши строки перед их сохранением.

2 голосов
/ 26 октября 2010

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

from random import choice
from string import letters
from hashlib import sha1

def keygen(length):
    return "".join(choice(letters) for _ in xrange(length))

def gentestdata(n=1000*1000):
    # return dict((sha1(keygen(80)).digest(), keygen(200)) for _ in xrange(n))
    d = {}
    for _ in xrange(n):
        key = sha1(keygen(80)).digest()
        assert key not in d
        value = keygen(200)
        d[key] = value
    return d

if __name__ == '__main__':
    d = gentestdata()

На моем компьютере с Ubuntu это занимает максимум 304 МБ памяти:

2010-10-26 14:26:02 hbrown@hbrown-ubuntu-wks:~$ ps aux | grep python
[...]
hbrown   12082 78.2  7.5 307420 303128 pts/1   S+   14:20   4:47 python

Достаточно близко?Это Python, а не C.


Позже: также, если ваши данные несколько избыточны, вы можете gzip значения.Это время против космического компромисса.

1 голос
/ 05 февраля 2013

Джуди должна эффективно использовать память: http://judy.sourceforge.net/
(Контрольные показатели: http://www.nothings.org/computer/judy/, см. «Размер структуры данных»).
Смотрите также: http://www.dalkescientific.com/Python/PyJudy.html

Кроме того,

Для ключей фиксированного размера в C ++ есть http://panthema.net/2007/stx-btree/ (я уверен, что с пользовательскими обертками C это можно использовать из CPython). Если набор данных позволяет это, вы можете сохранить ключи переменной длины в значении и использовать хеш или префикс ключа переменной длины в качестве ключа фиксированной длины.

Та же логика применима к http://google -opensource.blogspot.ru / 2013/01 / c-container-that-save-memory-and-time.html и http://code.google.com/p/sparsehash/ - вместо использования тяжелого std :: string в качестве ключа используйте 32-битный или 64-битный целочисленный ключ, что делает его каким-то образом из реального ключа переменной длины.

1 голос
/ 26 октября 2010

Apache Portable Runtime (aka APR) имеет хеш-таблицу на основе c.Вы можете посмотреть документацию на http://apr.apache.org/docs/apr/0.9/group_apr_hash.html

С apr_hash_t все, что вы храните, является недействительным *.Так что это дает вам полный контроль над ценностями.Поэтому, если вы хотите, вы можете сохранить указатель на 100-байтовый блок вместо фактической длины строки.

1 голос
/ 26 октября 2010

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

0 голосов
/ 13 ноября 2010

Поскольку я не мог найти какие-либо существующие решения, которые будут плотно упаковывать память, я решил реализовать это на C для себя.См. Мой дизайн с открытой адресацией в вопросе.

...