Потребление памяти Python: dict VS список кортежей

Есть много вопросов и обсуждений о потреблении памяти разных типов данных python. Однако немногие из них (если таковые имеются) приходят к очень конкретному сценарию. Если вы хотите сохранить LOTS данных ключа в памяти, какая структура данных более эффективна с точки зрения памяти, dict или список кортежей?

В начале я думал, что dict более мощный, чем список кортежей, и что власть должна приходить с некоторой ценой, а на самом деле пустой dict DOES занимают больше памяти, чем пустой список или кортеж (см. Размер памяти в структуре Python), поэтому я подумал, что использование [(key1, value1), (key2, value2), ...] будет более эффективным с точки зрения памяти, чем {key1: value1, key2: value2, ...}.

Похоже, я ошибся. Просто запустите следующий фрагмент кода и просмотрите потребление памяти, указанное вашей ОС. Я использую Windows XP, чтобы диспетчер задач подсказывал мне, что большой диктат использует только "только" 40MB Ram и 40MB VIRTURAL Ram, но список кортежей питается 60MB Ram и 60MB Virtual ram.

Как это могло быть?

from sys import getsizeof as g
raw_input('ready, press ENTER')
i = 1000000
#p = [(x, x) for x in xrange(i)] # Will print 4,348,736 40,348,736
p = dict((x, x) for x in xrange(i)) # Will print 25,165,964 37,165,964
print g(p), g(p) + sum(g(x) for x in p)
raw_input("Check your process memory consumption now, press ENTER to exit")

Update:

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

Ответы

Ответ 1

В list tuple добавлен дополнительный слой. У вас есть 3 слоя предметов:

  • Внешний список длиной 1 миллион, поэтому 1 млн указателей
    • 1 миллион 2-х слотовых кортежей, поэтому 2 миллиона указателей
      • 2 миллиона ссылок на 1 миллион целых значений

в то время как ваш dict выполняется только:

  • The dict (включая 1 миллион кэшированных хэшей) с 2 миллионами указателей + дополнительное пространство для роста таблицы
    • 2 миллиона ссылок на 1 миллион целых значений

Он содержит 1 миллион кортежей плюс список для хранения ссылок на них, которые занимают больше памяти, чем 1 миллион кэшированных хэшей. Здесь есть около 50% указателей, что позволяет легко использовать на 50% больше использования памяти.

Существует еще один недостаток вашего списка кортежей: время поиска. Чтобы найти подходящий ключ в dict, существует стоимость сложности O (1). Чтобы сделать то же самое в списке кортежей, вы должны потенциально просмотреть весь список для стоимости O (n). Не используйте список кортежей, если вам нужно сопоставить ключи со значениями.

Ответ 2

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

i:  2  list size:  296  dict size:  328  difference:  -32
i:  3  list size:  392  dict size:  352  difference:  40
i:  4  list size:  488  dict size:  376  difference:  112
i:  5  list size:  616  dict size:  400  difference:  216
i:  7  list size:  808  dict size:  1216  difference:  -408
i:  10  list size:  1160  dict size:  1288  difference:  -128
i:  13  list size:  1448  dict size:  1360  difference:  88
i:  17  list size:  1904  dict size:  1456  difference:  448
i:  23  list size:  2480  dict size:  3904  difference:  -1424
i:  31  list size:  3328  dict size:  4096  difference:  -768
i:  42  list size:  4472  dict size:  4360  difference:  112
i:  56  list size:  5912  dict size:  4696  difference:  1216
i:  74  list size:  7880  dict size:  5128  difference:  2752
i:  100  list size:  10520  dict size:  14968  difference:  -4448
i:  133  list size:  14024  dict size:  15760  difference:  -1736
i:  177  list size:  18672  dict size:  16816  difference:  1856

Этот шаблон продолжается по мере роста i. (Вы можете проверить это с помощью своего метода - попробуйте установить i рядом с 2636744. Размер словаря больше в этой точке, по крайней мере для меня.) Martijn правильно, что кортежи в списке кортежей добавляют к издержкам памяти, отменяя преимущество памяти в списках над словарями. Но результат, в среднем, не в том, что словарь лучше; это то, что словарь примерно одинаковый. Поэтому в ответ на ваш оригинальный вопрос:

Если вы хотите сохранить LOTS данных о значении ключа в памяти, какая структура данных более эффективна с точки зрения памяти, dict или список кортежей?

На самом деле неважно, все ли вы обеспокоены памятью.

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


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

import sys, collections, itertools, math

def totalsize(x):
    seen = set()
    return ts_rec(x, seen)

def ts_rec(x, seen):
    if id(x) in seen:
        return 0
    else:
        seen.add(id(x))

    x_size = sys.getsizeof(x)
    if isinstance(x, collections.Mapping):
        kv_chain = itertools.chain.from_iterable(x.iteritems())
        return x_size + sum(ts_rec(i, seen) for i in kv_chain)
    elif isinstance(x, collections.Sequence):
        return x_size + sum(ts_rec(i, seen) for i in x)
    else:
        return x_size

for i in (10 ** (e / 8.0) for e in range(3, 19)):
    i = int(i)
    lsize = totalsize([(x, x) for x in xrange(i)])
    dsize = totalsize(dict((x, x) for x in xrange(i)))

    print "i: ", i,
    print " list size: ", lsize, " dict size: ", dsize,
    print " difference: ", lsize - dsize