Python: сокращение использования памяти словаря

Пытаюсь загрузить в память пару файлов. Файлы имеют любой из следующих 3-х форматов:

  • строка TAB целое
  • строка TAB с плавающей запятой
  • int TAB с плавающей запятой.

Действительно, это файлы статики ngram, на случай, если это поможет с решением. Например:

i_love TAB 10
love_you TAB 12

В настоящее время псевдокод, который я делаю прямо сейчас,

loadData(file):
     data = {}
     for line in file:
        first, second = line.split('\t')
        data[first] = int(second) #or float(second)

     return data

К моему большому удивлению, при общем размере файлов на диске около 21 мб, при загрузке в память процесс занимает 120 - 180 мб памяти! (все приложение Python не загружает никаких других данных в память).

Файлов меньше 10, большинство из них останутся стабильными на уровне 50-80 тыс. строк, за исключением одного файла, который в настоящее время содержит миллионы строк.

Поэтому я хотел бы попросить технику/структуру данных для уменьшения потребления памяти:

  • Любые советы по методам сжатия?
  • Если я все еще использую dict, есть ли способ уменьшить память? Можно ли установить «коэффициент нагрузки», как в Java для Python dict?
  • Если у вас есть какие-то другие структуры данных, я также готов обменять часть скорости на уменьшение памяти. Тем не менее, это чувствительное ко времени приложение, поэтому после того, как пользователи введут свои запросы, я думаю, что было бы не совсем разумно возвращать результат более чем на несколько секунд. Что касается этого, я все еще поражен тем, как Google удается так быстро выполнять Google Translate: они, должно быть, используют много методов + много мощности серверов?

Большое Вам спасибо. Я с нетерпением жду вашего совета.


person Paul Hoang    schedule 22.04.2012    source источник
comment
Повторяются ли какие-либо данные (ключи или значения)? Если это так, вы можете использовать ссылки на одни и те же объекты для экономии места.   -  person Gabe    schedule 22.04.2012
comment
@Gabe: я думаю, что ключи не повторяются (или, если они повторяются, я просто позволю последнему переопределить предыдущий). Спасибо.   -  person Paul Hoang    schedule 22.04.2012
comment
Сколько из этих 120-180 МБ памяти занимает сам интерпретатор?   -  person Platinum Azure    schedule 22.04.2012
comment
@PlatinumAzure Любые предложения, как я могу это узнать? Спасибо.   -  person Paul Hoang    schedule 22.04.2012
comment
@Gabe: словарь уже заботится о повторяющихся ключах, поскольку ключи хэшируются.   -  person Joel Cornett    schedule 22.04.2012
comment
@hunghuuhoang: Какой метод вы используете для определения размера словаря Python? Это не то, что легко определить.   -  person Joel Cornett    schedule 22.04.2012
comment
К сожалению, Python изначально не предназначен для такой поддержки. Способ сделать это — написать его как модуль C (например, NumPy).   -  person Gabe    schedule 22.04.2012
comment
@JoelCornett: псевдокод OP подразумевает, что каждый файл будет загружаться в отдельный словарь, что позволит им обмениваться ключами.   -  person Gabe    schedule 22.04.2012
comment
Вы можете использовать Guppy/Heapy, чтобы найти разбивку памяти модуля Python. (guppy-pe.sourceforge.net)   -  person Adam Lewis    schedule 22.04.2012
comment
@JoelCornett Думаю, я создал неправильное впечатление. 120-180 МБ - это весь процесс pthon. Я не пытался измерить размер Python dict. Но поскольку все приложение загружает только файлы статистики, я думаю, разумно угадать размер диктов из этого (за вычетом накладных расходов интерпретатора python)   -  person Paul Hoang    schedule 22.04.2012
comment
@hunghuuhoang: Понятно. Мне любопытно, что происходит, когда вы меняете размер входного файла. Использование памяти увеличивается или уменьшается линейно? экспоненциально?   -  person Joel Cornett    schedule 22.04.2012
comment
@PlatinumAzure Я никогда раньше не видел, чтобы интерпретатор Python превышал 10 МБ или Jython превышал 30 МБ, поэтому я сомневаюсь, что это важно.   -  person javawizard    schedule 27.11.2012


Ответы (6)


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

Если вы посмотрите на реализация Python словаря (которая является относительно простой реализацией хеш-таблицы), а также на реализацию встроенных строковых и целочисленных типов данных, например здесь (в частности, object.h, intobject.h, stringobject.h и dictobject .h, а также соответствующие файлы *.c в ../Objects), можно с некоторой точностью рассчитать ожидаемые требования к пространству:

  1. Целое число — это объект фиксированного размера, т. е. он содержит счетчик ссылок, указатель типа и фактическое целое число, обычно не менее 12 байтов в 32-битной системе и 24 байта в 64-битной системе, без учета лишнего пространства, которое может быть потеряно из-за выравнивания.

  2. Объект string имеет переменный размер, что означает, что он содержит

  • счетчик ссылок

  • указатель типа

  • информация о размере

  • место для лениво вычисляемого хэш-кода

  • информация о состоянии (например, используется для интернированных строк)

  • указатель на динамическое содержимое

    всего не менее 24 байт для 32-разрядной версии или 60 байт для 64-разрядной версии, не включая место для самой строки.

  1. Сам словарь состоит из нескольких сегментов, каждый из которых содержит
  • хэш-код хранящегося в данный момент объекта (который нельзя предсказать по положению ведра из-за используемой стратегии разрешения коллизий)

  • указатель на ключевой объект

  • указатель на объект значения

    всего не менее 12 байт для 32-разрядной версии и 24 байта для 64-разрядной версии.

  1. Словарь начинается с 8 пустых сегментов, и его размер изменяется за счет удвоения количества записей при достижении его емкости.

Я провел тест со списком из 46 461 уникальной строки (размер объединенной строки 337 670 байт), каждая из которых связана с целым числом — аналогично вашей настройке на 32-разрядной машине. Согласно приведенному выше расчету, я ожидаю, что минимальный объем памяти

  • 46 461 * (24+12) байт = 1,6 МБ для комбинаций строки/целого числа.
  • 337 670 = 0,3 МБ для содержимого строки
  • 65 536 * 12 байт = 1,6 МБ для сегментов хеширования (после 13-кратного изменения размера)

всего 2,65 МБ. (Для 64-битной системы соответствующий расчет дает 5,5 МБ.)

При работе интерпретатора Python в режиме ожидания его размер согласно ps-инструменту составляет 4,6 МБ. Таким образом, общее ожидаемое потребление памяти после создания словаря составляет примерно 4,6 + 2,65 = 7,25 МБ. Реальный объем памяти (согласно ps) в моем тесте составил 7,6. МБ. Я предполагаю, что дополнительно ок. 0,35 МБ были израсходованы накладными расходами, созданными с помощью стратегии распределения памяти Python (для арен памяти и т. д.).

Конечно, многие люди теперь укажут, что мое использование ps для измерения объема памяти является неточным, и мои предположения о размерах типов указателей и целых чисел в 32-битных и 64-битных системах могут быть неверными во многих конкретных системах. Предоставленный.

Но, тем не менее, ключевые выводы, я считаю, таковы:

  • Реализация словаря в Python потребляет на удивление небольшой объем памяти.
  • Но пространство, занимаемое многими объектами int и (в частности) строковыми объектами, для подсчета ссылок, предварительно рассчитанных хэш-кодов и т. д., больше, чем вы думаете. первый
  • Существует едва ли способ избежать накладных расходов на память, если вы используете Python и хотите, чтобы строки и целые числа представлялись как отдельные объекты — по крайней мере, я не понимаю, как это можно сделать.
  • Возможно, стоит поискать (или реализовать самостоятельно) расширение Python-C, реализующее хэш, хранящий ключи и значения в виде C-указателей (а не объектов Python). Я не знаю, существует ли это; но я считаю, что это можно сделать и уменьшить объем памяти более чем наполовину.
person jogojapan    schedule 22.04.2012
comment
Действительно, очень полезный анализ! И спасибо за конкретные цифры. У меня один вопрос: в ваших 65 536 * 12 байт = 1,6 Мб как получить число 65 536? Спасибо. - person Paul Hoang; 22.04.2012
comment
@Paul Hoang Я должен был объяснить это лучше: начиная с 8 записей и изменяя размер (удваивая) 13 раз, мы получаем 8 * (2 ^ 13) = 65 536. Изменение размера всего 12 раз дает 32 768, поэтому я предполагаю, что для 46 461 записи размер будет изменяться как минимум 13 раз как минимум. - person jogojapan; 22.04.2012
comment
Целые числа выделяются из пула, распределенного блоками по 1000 байт, поэтому средние накладные расходы на целое число очень малы. - person Gabe; 23.04.2012
comment
@Gabe: Но они интернированы только до 256 человек; после этого они распределяются так же, как и любой другой объект. - person javawizard; 27.11.2012
comment
@javawizard: Да; Я говорил о неинтернированных целых числах. - person Gabe; 27.11.2012
comment
Что бы это ни стоило, Pypy оптимизирует списки целых чисел или строк машинного размера, чтобы не было накладных расходов на память. - person Antimony; 25.08.2013
comment
Хороший анализ. Я использую 64-битную версию и обнаружил, что sys.getsizeof('') == 37, а не 60 байт. Откуда вы взяли 60 байт? - person RussellStewart; 15.03.2014
comment
@user2237635 user2237635 Тестирование на sys.getsizeof() — хорошая идея — я этого не делал. Возможно, я действительно использовал Python 2.5, когда писал это, где getsizeof не существовало. В любом случае, мои цифры основаны на чтении исходного кода и предположениях о размере базовых типов, таких как int, short и т. д., что, конечно, зависит от платформы. Кроме того, реализация типов Python со временем изменилась. Тестирование с getsizeof моей текущей 64-битной системы показывает разницу в 5 байтов для размера "" и b"" соответственно между Python 2.7.5 и Python 3.3. - person jogojapan; 15.03.2014
comment
Таким образом, для текущих версий Python и, конечно же, в зависимости от платформы и системы вы получите разные числа. Но 60 действительно кажется довольно большим — возможно, я неправильно рассчитал размер некоторых элементов данных. - person jogojapan; 15.03.2014
comment
NUMPY. Если вы читаете набор целых чисел или чисел с плавающей запятой и сохраняете их в массиве или многомерном массиве, вы можете использовать возможности Numpy, чтобы сделать это компактно. Это позволяет использовать специализированные типы данных, реализованные на C, которые хранят необработанные данные без дополнительных затрат. - person Kevin J. Rice; 01.12.2014
comment
ссылка здесь мертва - person Brambor; 09.08.2020
comment
@Brambor Спасибо, что заметили это. Я исправил ссылку. - person jogojapan; 09.08.2020

1) SQLite в памяти звучит как отличное решение, это позволит вам легче запрашивать ваши данные после их загрузки, что доставляет удовольствие

sqlite3.connect(':память:')

2) вам, вероятно, нужен именованный кортеж - я почти уверен, что они легче, чем словари, и вы можете получить доступ к свойствам, используя точечную нотацию (к чему у меня все равно есть эстетическое предпочтение).

http://docs.python.org/dev/library/collections

3) вы можете взглянуть на Redis: https://github.com/andymccurdy/redis-py

Это БЫСТРО и позволит вам легко сохранять вещи, а это означает, что вам не нужно загружать весь набор каждый раз, когда вы хотите его использовать.

4) попытка звучит как хорошая идея, но добавляет некоторую теоретическую сложность вашей работе. Однако вы можете использовать Redis для его реализации и хранения, что еще больше повысит вашу скорость.

Но в целом именованные кортежи, вероятно, являются хитростью.

person Moshe Bildner    schedule 22.08.2013

На диске у вас есть только строки, при загрузке в Python интерпретатор должен создать целую структуру для каждой строки и для каждого словаря, помимо самой строки.

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

person Pedro Werneck    schedule 22.04.2012
comment
Согласованный. Если он слишком велик для размещения в памяти, используйте базу данных. - person Francis Avila; 22.04.2012
comment
@Pedro Werneck Спасибо, что предложили SQLite. Я пробовал PostGres, и он действительно намного медленнее (чем хотелось бы). Знаете ли вы, что использование SQLite быстрее, чем использование PostGres или MySQL? - person Paul Hoang; 22.04.2012
comment
Это будет работать только в том случае, если вы можете хранить всю БД в памяти. Если вы укажете SQLite использовать память вместо файла, вы сможете получить необходимую скорость. Однако неясно, улучшится ли использование памяти. - person Gabe; 22.04.2012
comment
SQLite в несколько раз быстрее, чем PostgreSQL и MySQL - person Pedro Werneck; 22.04.2012

Похоже на Trie ( http://en.m.wikipedia.org/wiki/Trie) структура данных может лучше соответствовать вашему стремлению к эффективности памяти.

Обновление: эффективность использования памяти python dict была поднята как проблема, хотя она была отклонена из стандартной библиотеки из-за доступности сторонних библиотек. См.: http://bugs.python.org/issue9520.

person Garrett    schedule 22.04.2012
comment
Хорошая идея! Кроме того, поиск стал быстрее. Как насчет использования DAWG? Я думаю, что это будет стоить меньше памяти. - person Ray; 15.08.2012

Вы можете заменить dict на blist.sorteddict для доступа к log(n) без накладных расходов на память. Он удобен тем, что ведет себя точно так же, как словарь, т.е. реализует все свои методы, так что вам нужно изменить только исходный тип.

person ealfonso    schedule 29.10.2013

Если вы пытаетесь компактно хранить числовые данные в python в памяти, лучшим решением, вероятно, будет Numpy.

Numpy (http://numpy.org) размещает структуры данных, используя собственные структуры C. Большинство его структур данных предполагают, что вы храните один тип данных, так что это не для всех ситуаций (вам может понадобиться хранить null и т. д.), но это может быть очень, очень, очень быстро и примерно так же компактно, как вы могли бы попросить. С его помощью делается много научных работ (см. также: SciPy).

Конечно, есть и другой вариант: zlib, если у вас:

  • Достаточные циклы ЦП и
  • МНОГО данных, которые не помещаются в память

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

Затем перебирайте страницы, разархивируйте, преобразуйте обратно в массив и выполняйте необходимые операции. Например:

def arrayToBlob(self, inArray):
    a = array.array('f', inArray)
    return a.tostring()

def blobToArray(self, blob, suppressWarning=False):
    try:
        out = array.array('f', [])
        out.fromstring(blob)
    except Exception, e:
        if not suppressWarning:
            msg = "Exception: blob2array, err: %s, in: %s" % (e, blob)
            self.log.warning(msg)
            raise Exception, msg
    return out

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

Конечно, это медленнее, чем хранить все это в несжатом виде, но если вы не можете уместить его в памяти, ваш выбор с самого начала ограничен.

Даже при сжатии все это может не поместиться в памяти, и в этот момент вам, возможно, придется записать сжатые страницы в виде строк или солений и т. д.

Удачи!

person Kevin J. Rice    schedule 01.12.2014