Почему в dicts defaultdict (int) используется так много памяти? (и другие простые вопросы производительности python)

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

import numpy as num
from collections import defaultdict

topKeys = range(16384)
keys = range(8192)

table = dict((k,defaultdict(int)) for k in topKeys)

dat = num.zeros((16384,8192), dtype="int32")

print "looping begins"
#how much memory should this use? I think it shouldn't use more that a few
#times the memory required to hold (16384*8192) int32 (512 mb), but
#it uses 11 GB!
for k in topKeys:
    for j in keys:
        dat[k,j] = table[k][j]

print "done"

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

topKeys = range(16384)
keys = range(8192)
table = [(j,0) for k in topKeys for j in keys]

Я предполагаю, что для python ints могут быть 64-битные ints, что будет объяснять некоторые из них, но действительно ли эти относительно естественные и простые конструкции действительно создают такие большие накладные расходы? Я предполагаю, что эти сценарии показывают, что они это делают, поэтому мой вопрос: что именно вызывает использование высокой памяти в первом script и длительной продолжительности работы и использовании большой памяти второго script, и есть ли способ избежать этих расходы?

Изменить: Python 2.6.4 на 64-разрядной машине.

Изменить 2: я понимаю, почему в первом приближении моя таблица должна занимать 3 ГБ 16384 * 8192 * (12 + 12) байт  и 6 ГБ с коэффициентом нагрузки по умолчанию, который заставляет его резервировать удвоенное пространство. Тогда неэффективность в распределении памяти съедает еще один фактор в 2 раза.

Итак, вот мои оставшиеся вопросы: Есть ли способ, чтобы я сказал, чтобы использовать 32-битные ints?

И почему мой второй фрагмент кода принимает FOREVER для запуска по сравнению с первым? Первый занимает около минуты, и я убил второй после 80 минут.

Ответ 1

Python ints внутренне представлены как C longs (это на самом деле немного сложнее, чем это), но это не совсем корень вашей проблемы.

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

У дикта может быть намного больше слотов, чем у него есть элементы. Скажем, у вас есть дикт с 3-мя слотами в качестве предметов. Каждому из этих слотов требуется место для указателя на ключ, а указатель служит в качестве конца связанного списка. Это 6x количество точек, как число, плюс все указатели на интересующие вас предметы. Учтите, что каждый из этих указателей имеет 8 байтов в вашей системе и что в этой ситуации у вас есть 16384 defaultdicts. Как грубый, ручной взгляд на это, 16384 occurrences * (8192 items/occurance) * 7 (pointers/item) * 8 (bytes/pointer) = 7 GB. Это до того, как я вернусь к фактическим номерам, которые вы храните (каждый уникальный номер которого сам по себе является питоном Python), внешний dict, этот массив numpy или материал Python, который отслеживает, чтобы попытаться оптимизировать некоторые.

Ваши накладные расходы немного выше, чем я подозреваю, и мне было бы интересно узнать, был ли этот 11GB для всего процесса или вы рассчитали его только для таблицы. В любом случае, я ожидаю, что размер этой структуры данных dict-of-defaultdicts будет на порядок больше, чем представление массива numpy.

Что касается "есть ли способ избежать этих затрат?" ответ "использовать numpy для хранения больших непрерывных числовых массивов фиксированного размера, а не dicts!" Вы должны быть более конкретными и конкретными о том, почему вы нашли такую ​​структуру, необходимую для лучшего совета о том, какое лучшее решение.

Ответ 2

Хорошо, посмотрите, что делает ваш код:

topKeys = range(16384)
table = dict((k,defaultdict(int)) for k in topKeys)

Это создает ячейку 16384 defaultdict(int). У dict есть определенное количество накладных расходов: сам объект dict находится между 60 и 120 байтами (в зависимости от размера указателей и ssize_t в вашей сборке). Это только сам объект; если значение dict меньше пары элементов, данные представляют собой отдельный блок памяти, от 12 до 24 байтов, и он всегда составляет от 1/2 до 2/3rds. И defaultdicts от 4 до 8 байтов больше, потому что у них есть эта дополнительная вещь для хранения. И ints - по 12 байт, и хотя они по возможности используются повторно, этот фрагмент не будет повторно использовать большинство из них. Таким образом, реалистично, в 32-битной сборке этот фрагмент будет занимать 60 + (16384*12) * 1.8 (fill factor) байты для байтов table dict, 16384 * 64 для значений по умолчанию, которые он хранит как значения, и 16384 * 12 байтов для целых чисел. Так что чуть более полутора мегабайт, не сохраняя ничего в ваших defaultdicts. И это в 32-битной сборке; 64-битная сборка будет в два раза больше.

Затем вы создаете массив numpy, который на самом деле довольно консервативен с памятью:

dat = num.zeros((16384,8192), dtype="int32")

Это будет иметь некоторые накладные расходы для самого массива, обычные служебные данные объекта Python, а также размеры и тип массива и т.д., но это будет не более 100 байт и только для одного массива. Однако он сохраняет 16384*8192 int32 в вашем 512 Мб.

И тогда у вас есть довольно своеобразный способ заполнения этого массива numpy:

for k in topKeys:
    for j in keys:
        dat[k,j] = table[k][j]

Две петли сами не используют много памяти, и они повторно используют ее на каждой итерации. Однако table[k][j] создает новое целое число Python для каждого запрашиваемого вами значения и сохраняет его в defaultdict. Созданное целое всегда 0, и бывает так, что это всегда используется повторно, но сохранение ссылки на него по-прежнему использует пробел в defaultdict: вышеупомянутые 12 байт на запись, умноженные на коэффициент заполнения (между 1.66 и 2. ) Это приземляет вас близко к 3Gb фактических данных прямо там и 6Gb в 64-битной сборке.

Кроме того, defaultdicts, потому что вы продолжаете добавлять данные, должны продолжать расти, а это значит, что они должны продолжать перераспределение. Из-за интерфейса Python malloc (obmalloc) и того, как он выделяет меньшие объекты в своих блоках и как работает память процесса в большинстве операционных систем, это означает, что ваш процесс будет выделять больше и не сможет его освободить; он фактически не будет использовать все 11Gb, а Python будет повторно использовать доступную память между большими блоками для defaultdicts, но общее отображаемое адресное пространство будет равным 11Gb.

Ответ 3

Майк Грэм дает хорошее объяснение, почему словари используют больше памяти, но я подумал, что я объясню, почему ваш табличный dict of defaultdicts начинает занимать так много памяти.

Способ настройки defaultdict (DD) прямо сейчас, всякий раз, когда вы извлекаете элемент, который не находится в DD, вы получаете значение по умолчанию для DD (0 для вашего случая), но также и DD хранит ключ, который ранее не был в DD со значением по умолчанию 0. Мне лично это не нравится, но как это происходит. Однако это означает, что для каждой итерации внутреннего цикла выделяется новая память, поэтому она берет навсегда. Если вы измените строки

for k in topKeys:
    for j in keys:
        dat[k,j] = table[k][j]

к

for k in topKeys:
    for j in keys:
        if j in table[k]:
            dat[k,j] = table[k][j]
        else:
            dat[k,j] = 0

то значения по умолчанию не назначаются клавишам в DD, и поэтому память остается около 540 МБ для меня, которая в основном является только памятью, выделенной для dat. DD являются приличными для разреженных матриц, хотя вы, вероятно, должны просто использовать разреженные матрицы в Scipy, если это то, что вы хотите.