list () використовує трохи більше пам'яті, ніж розуміння списку


79

Тож я грав з listпредметами і виявив трохи дивної речі, яка, якщо listстворена з list()нею, використовує більше пам'яті, ніж розуміння списку? Я використовую Python 3.5.2

In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008

З документів :

Списки можуть бути побудовані кількома способами:

  • Використання пари квадратних дужок для позначення порожнього списку: []
  • Використовуючи квадратні дужки, розділяючи елементи комами: [a],[a, b, c]
  • Використання розуміння списку: [x for x in iterable]
  • Використання конструктора типу: list()абоlist(iterable)

Але, здається, при list()його використанні використовується більше пам'яті.

І listчим більше, тим більше розрив.

Різниця в пам’яті

Чому так трапляється?

ОНОВЛЕННЯ №1

Тест з Python 3.6.0b2:

Python 3.6.0b2 (default, Oct 11 2016, 11:52:53) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912

ОНОВЛЕННЯ No2

Тест з Python 2.7.12:

Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920

3
Це дуже цікаве питання. Я можу відтворити явище в Python 3.4.3. Ще цікавіше: на Python 2.7.5 sys.getsizeof(list(range(100)))дорівнює 1016, getsizeof(range(100))дорівнює 872 і getsizeof([i for i in range(100)])дорівнює 920. Усі мають тип list.
Свен Фестерсен

Цікаво, що ця різниця є і в Python 2.7.10 (хоча фактичні цифри відрізняються від Python 3). Також там у 3.5 та 3.6b.
cdarke

Я отримую ті самі номери для Python 2.7.6, що і @SvenFestersen, також під час використання xrange.
RemcoGerlich

2
Тут є можливе пояснення: stackoverflow.com/questions/7247298/size-of-list-in-memory . Якщо один із методів створює список за допомогою append(), можливо, надмірне виділення пам'яті. Думаю, єдиний спосіб реально це пояснити - це поглянути на джерела Python.
Свен Фестерсен

Тільки на 10% більше (ви ніде цього не говорите). Я переформулював би заголовок "трохи більше".
smci

Відповіді:


61

Я думаю, ви бачите моделі перерозподілу, це зразок із джерела :


Друкуючи розміри списку довжинами 0-88, ви можете побачити збіги шаблонів:

# create comprehensions for sizes 0-88
comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)]

# only take those that resulted in growth compared to previous length
steps = zip(comprehensions, comprehensions[1:])
growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]]

# print the results:
for growth in growths:
    print(growth)

Результати (формат (list length, (old total size, new total size))):

(0, (64, 96)) 
(4, (96, 128))
(8, (128, 192))
(16, (192, 264))
(25, (264, 344))
(35, (344, 432))
(46, (432, 528))
(58, (528, 640))
(72, (640, 768))
(88, (768, 912))

Надмірне розподіл здійснюється з міркувань продуктивності, дозволяючи спискам рости, не виділяючи більше пам’яті з кожним зростанням (краща амортизована продуктивність).

Ймовірною причиною різниці з використанням розуміння списку є те, що розуміння списку не може детерміновано обчислити розмір створеного списку, але list()може. Це означає, що розуміння будуть постійно нарощувати список, оскільки він заповнює його, використовуючи надмірне розподіл, до остаточного заповнення.

Можливо, це не призведе до зростання буфера надмірного розподілу з невикористаними виділеними вузлами, як тільки це буде зроблено (насправді, в більшості випадків це не буває, що перемогло б ціль перерозподілу).

list(), однак, він може додати деякий буфер незалежно від розміру списку, оскільки він знає остаточний розмір списку заздалегідь.


Ще одним підтвердженням, також із джерела, є те, що ми бачимо, як застосовується розуміння спискуLIST_APPEND , що вказує на використання list.resize, що, в свою чергу, вказує на споживання буфера попереднього розподілу, не знаючи, скільки його буде заповнено. Це відповідає поведінці, яку ви спостерігаєте.


На закінчення list()буде попередньо виділено більше вузлів як функція від розміру списку

>>> sys.getsizeof(list([1,2,3]))
60
>>> sys.getsizeof(list([1,2,3,4]))
64

Розуміння списку не знає розміру списку, тому він використовує операції додавання по мірі зростання, вичерпуючи буфер попереднього розподілу:

# one item before filling pre-allocation buffer completely
>>> sys.getsizeof([i for i in [1,2,3]]) 
52
# fills pre-allocation buffer completely
# note that size did not change, we still have buffered unused nodes
>>> sys.getsizeof([i for i in [1,2,3,4]]) 
52
# grows pre-allocation buffer
>>> sys.getsizeof([i for i in [1,2,3,4,5]])
68

4
Але чому надмірний розподіл трапляється з одним, але не з іншим?
cdarke

Це конкретно від list.resize. Я не фахівець у навігації за джерелом, але якщо один вимагає зміни розміру, а інший - ні, це може пояснити різницю.
Реут Шарабані

6
Python 3.5.2 тут. Спробуйте друкувати розміри списків від 0 до 35 у циклі. Для списку, який я бачу, 64, 96, 104, 112, 120, 128, 136, 144, 160, 192, 200, 208, 216, 224, 232, 240, 256, 264, 272, 280, 288, 296, 304, 312, 328, 336, 344, 352, 360, 368, 376, 384, 400, 408, 416і для розуміння 64, 96, 96, 96, 96, 128, 128, 128, 128, 192, 192, 192, 192, 192, 192, 192, 192, 264, 264, 264, 264, 264, 264, 264, 264, 264, 344, 344, 344, 344, 344, 344, 344, 344, 344. Я б виключив, що розуміння є тим, хто, здається, попередньо розподіляє пам'ять як алгоритм, який використовує більше оперативної пам'яті для певних розмірів.
tavo

Я би очікував того ж. Невдовзі я можу заглянути далі. Хороші коментарі.
Реут Шарабані

4
насправді list()детерміновано визначає розмір списку, чого розуміння списку не може зробити. Це наводить на думку, що розуміння списку не завжди "викликає" "останнє" зростання списку. Може мати сенс.
Реут Шарабані

30

Дякую усім за допомогу мені зрозуміти цей чудовий Python.

Я не хочу робити таке масове запитання (саме тому я публікую відповідь), просто хочу показати та поділитися своїми думками.

Як правильно зазначив @ReutSharabani : "list () детерміновано визначає розмір списку". Ви можете бачити це на цьому графіку.

графік розмірів

Коли ви appendабо використовуєте розуміння списку, ви завжди маєте певні межі, які розширюються, коли ви досягаєте певної точки. І у list()вас майже однакові межі, але вони плаваючі.

ОНОВЛЕННЯ

Тож завдяки @ReutSharabani , @tavo , @SvenFestersen

Підводячи підсумок: list()попереднє розподіл пам'яті залежить від розміру списку, розуміння списку не може цього зробити (він вимагає більше пам'яті, коли це потрібно, наприклад .append()). Тому list()зберігайте більше пам'яті.

Ще один графік, який показує list()попередній розподіл пам'яті. Отже, зелена лінія показує list(range(830))додавання елемента за елементом і на деякий час пам’ять не змінюється.

list () попередньо розподіляє пам'ять

ОНОВЛЕННЯ 2

Як зазначав @Barmar у коментарях нижче, list()мені потрібно швидше, ніж розуміння списку, тому я побіг timeit()із number=1000довжиною listвід 4**0до 4**10і результати

вимірювання часу


1
Відповідь, чому червона лінія знаходиться над синьою, полягає в тому, що коли listконструктор може визначити розмір нового списку за своїм аргументом, він все одно буде попередньо розподіляти ту саму кількість місця, як це було б, якби останній елемент щойно потрапив туди, і для нього недостатньо місця. Принаймні, це те, що для мене має сенс.
tavo

@tavo мені здається однаковим, через якийсь момент я хочу показати це на графіку.
vishes_shell

2
Отже, хоча для розуміння списків використовується менше пам'яті, вони, ймовірно, значно повільніші через усі зміни розміру. Їм часто доводиться копіювати основу списку в нову область пам'яті.
Бармар

@Barmar насправді я можу запустити вимірювання часу з rangeоб'єктом (це може бути цікаво).
vishes_shell

І це зробить ваші графіки ще гарнішими. :)
Бармар
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.