Чому два однакових списки мають різний слід пам’яті?


155

Я створив два списки l1і l2, але кожен з них має інший метод створення:

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

Але результат мене здивував:

Size of l1 = 144
Size of l2 = 192

Список, створений із розумінням списку, має більший розмір пам’яті, але два списки однакові у Python інакше.

Чому так? Це якась внутрішня річ CPython чи якесь інше пояснення?


2
Можливо, оператор повторення буде викликати якусь функцію, яка точно розміряє базовий масив. Зауважимо, що 144 == sys.getsizeof([]) + 8*10)де 8 - розмір вказівника.
juanpa.arrivillaga

1
Зауважте, що якщо ви перейдете 10на 11, [None] * 11список має розмір 152, але розуміння списку все-таки має розмір 192. Попередньо пов'язане запитання не є точним дублікатом, але воно доречне для розуміння того, чому це відбувається.
Патрік Хоф

Відповіді:


162

Коли пишеш [None] * 10 , Python знає, що йому знадобиться список з точно 10 об’єктів, тому він виділяє саме це.

Коли ви використовуєте розуміння списку, Python не знає, скільки йому знадобиться. Таким чином, поступово список збільшується у міру додавання елементів. Для кожного перерозподілу він виділяє більше місця, ніж потрібно відразу, так що не потрібно перерозподіляти кожен елемент. Отриманий список, ймовірно, буде дещо більшим, ніж потрібно.

Ви можете бачити таку поведінку, порівнюючи списки, створені з подібними розмірами:

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

Видно, що перший метод виділяє саме те, що потрібно, а другий періодично зростає. У цьому прикладі він виділяє достатньо для 16 елементів і повинен був перерозподілитись, коли досяг 17-го.


1
Так, це має сенс. Можливо, краще створити списки, *коли я знаю розмір спереду.
Андрій Кеселі

27
@AndrejKesely Використовуйте лише [x] * nз незмінним xу вашому списку. Отриманий список міститиме посилання на ідентичний об'єкт.
schwobaseggl

5
@schwobaseggl добре, це може бути те, що ви хочете, але це добре розуміти.
juanpa.arrivillaga

19
@ juanpa.arrivillaga Щоправда, можливо. Але зазвичай це не так, і особливо ТАК заповнений плакатами, цікавлячись, чому всі їх дані змінювалися одночасно: D
schwobaseggl

50

Як зазначається в цьому питанні, використовується розуміння спискуlist.append під кришкою, тому воно називатиме метод зміни розміру списку, який перевизначає.

Щоб продемонструвати це самому, ви можете реально скористатися disрозбірником:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

Зауважте LIST_APPENDопкод при розбиранні <listcomp>об'єкта коду. З документів :

LIST_APPEND (i)

Дзвінки list.append(TOS[-i], TOS). Використовується для реалізації розуміння списку.

Тепер для операції з повторенням списку маємо підказку про те, що відбувається, якщо врахувати:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

Отже, схоже, можна точно розподілити розмір. Дивлячись на вихідний код , ми бачимо, що саме це відбувається:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);

А саме тут : size = Py_SIZE(a) * n;. Решта функцій просто заповнює масив.


"Як зазначено в цьому питанні, для розуміння списку використовується list.append під кришкою" Я думаю, що точніше сказати, що він використовує .extend().
Накопичення

@ Накопичення, чому ти так віриш?
juanpa.arrivillaga

Тому що це не додавання елементів по одному. Коли ви додаєте елементи до списку, ви дійсно створюєте новий список, з новим розподілом пам'яті, і вносите його в цей новий розподіл пам'яті. З іншого боку, перелічення розумінь укладає більшість нових елементів у пам'ять, яка вже була виділена, і коли у них закінчується виділена пам'ять, вони виділяють ще один патрон пам'яті, не достатній для нового елемента.
Накопичення

7
@ Накопичення Це неправильно. list.append- це амортизована операція постійного часу, оскільки, коли список змінюється, він переоцінює. Не кожна операція додавання, отже, призводить до щойно виділеного масиву. У будь-якому випадку питання, до якого я пов'язував, показує вам у вихідному коді, що насправді розуміння списку справді використовується list.append,. Я повернуся на моєму ноутбуці в даний момент , і я можу показати вам розібраному байткод для списку розуміння і відповідного LIST_APPENDопкод
juanpa.arrivillaga

3

Жоден не є блоком пам'яті, але це не заздалегідь заданий розмір. На додаток до цього, у масиві між елементами масиву є додатковий інтервал. Ви можете самі це побачити, запустивши:

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

Який не сумарний розмір l2, а навпаки, менший.

print(sys.getsizeof([None]))
72

А це набагато більше однієї десятої частини розміру l1 .

Ваші номери повинні відрізнятися залежно від деталей вашої операційної системи та деталей поточного використання пам'яті у вашій операційній системі. Розмір [None] ніколи не може бути більшим, ніж наявна сусідня пам'ять, де змінна встановлена ​​для зберігання, і зміну, можливо, доведеться перенести, якщо вона згодом динамічно розподіляється, щоб бути більшою.


1
Noneнасправді не зберігається в базовому масиві, єдине, що зберігається - це PyObjectвказівник (8 байт). Всі об’єкти Python виділяються на купі. Noneє одиночним, тому наявність списку з багатьма нінами просто створить масив вказівників PyObject на той самий Noneоб’єкт на купі (і не використовуватиме додаткову пам'ять у процесі на додаткову None). Я не впевнений, що ви маєте на увазі під "Жоден не має заздалегідь заданого розміру", але це не звучить правильно. Нарешті, ваша петля з getsizeofкожним елементом не демонструє те, що, здається, вважає, що це демонструє.
juanpa.arrivillaga

Якщо, як ви говорите, це правда, розмір [None] * 10 повинен бути таким же, як розмір [None]. Але очевидно, що це не так - додано кілька додаткових сховищ. Насправді розмір [None], повторений десять разів (160), також менший, ніж розмір [None], помножений на десять. Як ви зазначаєте, чітко розмір вказівника на [None] менший, ніж розмір самого [None] (16 байт, а не 72 байти). Однак 160 + 32 - це 192. Я не думаю, що попередня відповідь також не вирішує проблему. Зрозуміло, що виділяється трохи зайвий об'єм пам'яті (можливо, залежно від стану машини).
StevenJD

"Якщо, як ви говорите, це правда, розмір [None] * 10 повинен бути таким же, як розмір [None]", що я кажу, що це може означати це? Знову ви, здається, зосереджуєтесь на тому, що базовий буфер перерозподілений, або що розмір списку включає більше, ніж розмір основного буфера (це, звичайно, так), але це не суть це питання. Знову ж таки, ваше використання gestsizeofкожного eleз l2них вводить в оману, оскільки getsizeof(l2) не враховує розмір елементів всередині контейнера .
juanpa.arrivillaga

Щоб довести собі останню претензію, зробіть l1 = [None]; l2 = [None]*100; l3 = [l2]тоді print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). ви отримаєте результат , як: 72 864 72. Тобто, відповідно, 64 + 1*8, 64 + 100*8, і 64 + 1*8, знову ж , припускаючи систему 64 - бітної з 8 байт розміру покажчика.
juanpa.arrivillaga

1
Як я вже зазначив, sys.getsizeof* не враховує розмір елементів у контейнері. З документів : " Обліковується лише споживання пам'яті, безпосередньо приписане об'єкту, а не споживання пам'яті об'єктів, на які він посилається ... Див. Рекурсивний розмір рецепту для прикладу використання getizeof () рекурсивно для пошуку розміру контейнерів і весь їхній зміст ".
juanpa.arrivillaga
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.