Чому кортежі займають менше місця в пам'яті, ніж списки?


105

A tupleзаймає менше пам’яті в Python:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

тоді як lists займає більше місця в пам'яті:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

Що відбувається всередині управління пам'яттю Python?


1
Я не впевнений, як це працює внутрішньо, але об'єкт списку має принаймні більше функцій, як, наприклад, додати, яких кортеж не має. Отже, кортеж як більш простий тип об'єкта має сенс бути меншим
Metareven

Я думаю, що це також залежить від машини до машини .... для мене, коли я перевіряю a = (1,2,3) займає 72, а b = [1,2,3] займає 88.
Амріт

6
Кортежі Python незмінні. Змінні об'єкти мають додаткові накладні витрати для вирішення змін часу виконання.
Лі Даніел Крокер

@Metareven кількість методів, які має тип, не впливають на простір пам'яті, який займають екземпляри. Список методів та їх код обробляються об'єктом прототипу, але екземпляри зберігають лише дані та внутрішні змінні.
jjmontes

Відповіді:


144

Я припускаю, що ви використовуєте CPython та 64-бітові (я отримав однакові результати на своєму 64-бітному CPython 2.7). Можуть бути відмінності в інших реалізаціях Python або якщо у вас 32-бітний Python.

Незалежно від реалізації, lists мають змінний розмір, а tuples мають фіксований розмір.

Так tuples може зберігати елементи безпосередньо всередині структури, списки з іншого боку потребують шару непрямості (він зберігає вказівник на елементи). Цей шар непрямості є вказівником для 64-бітових систем, які мають 64 біт, отже, 8 байт.

Але є ще одна річ, яка listробиться: вони перерозподіляють. В іншому випадку list.appendбуло б в O(n)операції завжди - щоб зробити його амортизуєтьсяO(1) (набагато швидше !!!) це по-Розподіляє. Але тепер він повинен відслідковувати виділений розмір і заповнений розмір ( tuples потрібно зберігати лише один розмір, оскільки розміщений і заповнений розмір завжди однаковий). Це означає, що кожен список повинен зберігати ще один "розмір", який у 64-бітових системах - це 64-бітове ціле число, знову ж 8 байт.

Так list s потрібно як мінімум на 16 байт більше пам'яті, ніж tuples. Чому я сказав «принаймні»? Через перевитрату. Перевищення коштів означає, що воно виділяє більше місця, ніж потрібно. Однак сума перевитрати залежить від того, як ви створюєте список та історію додавання / видалення:

>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4)  # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96

>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1)  # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2)  # no re-alloc
>>> l.append(3)  # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4)  # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72

Зображення

Я вирішив створити кілька зображень, які супроводжують пояснення вище. Можливо, вони корисні

Ось як це (схематично) зберігається в пам'яті у вашому прикладі. Я виділив відмінності з червоними (вільними) циклами:

введіть тут опис зображення

Це насправді лише наближення, оскільки intоб’єкти є також об’єктами Python, а CPython навіть повторно використовує невеликі цілі числа, тому, ймовірно, більш точне подання (хоча і не як читабельне) об'єктів у пам'яті було б:

введіть тут опис зображення

Корисні посилання:

Зауважте, що __sizeof__насправді не повертається "правильний" розмір! Він повертає лише розмір збережених значень. Однак при використанні sys.getsizeofрезультат відрізняється:

>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72

Є 24 "зайвих" байти. Це реально , це накладні витрати на збирач сміття, які не враховуються в __sizeof__методі. Це тому, що ви, як правило, не повинні використовувати магічні методи безпосередньо - в цьому випадку використовуйте функції, які вміють поводитися з ними: sys.getsizeof(що фактично додає накладні витрати GC до значення, поверненого з __sizeof__).


Re « Так списками потрібні як мінімум 16 байт більше пам'яті , ніж кортежі. », Хіба що бути 8? Один розмір для кортежів і два розміри для списків означає один додатковий розмір для списків.
ikegami

1
Так, список має один додатковий "розмір" (8 байт), але також зберігає вказівник (8 байт) на "масив PyObject", а не зберігати їх у структурі (що робить кортеж). 8 + 8 = 16.
MSeifert

2
Інша корисна посилання про listвиділення пам'яті stackoverflow.com/questions/40018398 / ...
vishes_shell

@vishes_shell Це насправді не пов’язано з питанням, оскільки код у питанні взагалі не перерозподіляє . Але так, це корисно в тому випадку, якщо ви хочете дізнатися більше про суму перевищення при використанні list()або розумінні списку.
MSeifert

1
@ user3349993 Кортежі незмінні, тому ви не можете додавати кортеж або видаляти предмет з кортежу.
MSeifert

31

Я заглиблююсь у кодову базу CPython, щоб ми могли побачити, як насправді розраховуються розміри. Чи не в вашому конкретному прикладі , не більше-розподілу були виконані, тому я не буду торкатися що .

Тут я буду використовувати 64-бітні значення, як і ви.


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

static PyObject *
list_sizeof(PyListObject *self)
{
    Py_ssize_t res;

    res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
    return PyInt_FromSsize_t(res);
}

Ось Py_TYPE(self)макрос , який захоплює ob_typeз self(повернення PyList_Type) в той час як _PyObject_SIZEще один макрос , який захоплює tp_basicsizeвід цього типу. tp_basicsizeобчислюється так, sizeof(PyListObject)де PyListObjectструктура екземпляра.

PyListObjectСтруктура має три поля:

PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes

у них є коментарі (які я обрізав), що пояснюють, що вони є, перейдіть за посиланням вище, щоб прочитати їх. PyObject_VAR_HEADрозширюється на три байта 8 полів ( ob_refcount, ob_typeі ob_size) , тому 24байт вкладу.

Тож resпоки що:

sizeof(PyListObject) + self->allocated * sizeof(void*)

або:

40 + self->allocated * sizeof(void*)

Якщо екземпляр списку має виділені елементи. друга частина розраховує їх внесок. self->allocated, як випливає з назви, містить кількість виділених елементів.

Без будь-яких елементів розмір списків обчислюється таким:

>>> [].__sizeof__()
40

тобто розмір структури екземпляра.


tuple об'єкти не визначають a tuple_sizeof функцію. Натомість вони використовують object_sizeofдля обчислення їх розміру:

static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
    Py_ssize_t res, isize;

    res = 0;
    isize = self->ob_type->tp_itemsize;
    if (isize > 0)
        res = Py_SIZE(self) * isize;
    res += self->ob_type->tp_basicsize;

    return PyInt_FromSsize_t(res);
}

Це, як і для lists, захоплює tp_basicsizeі, якщо об'єкт не має нуляtp_itemsize (тобто має екземпляри змінної довжини), він помножує кількість елементів у кортежі (через який він отримує Py_SIZE) зtp_itemsize .

tp_basicsizeзнову використовує sizeof(PyTupleObject)де PyTupleObject структура містить :

PyObject_VAR_HEAD       # 24 bytes 
PyObject *ob_item[1];   # 8  bytes

Отже, без будь-яких елементів (тобто Py_SIZEповертається0 ) розмір порожніх кортежів дорівнює sizeof(PyTupleObject):

>>> ().__sizeof__()
24

так? Ну ось ось дивацтво, якому я не знайшов поясненняtp_basicsize з tupleх фактично розраховується наступним чином :

sizeof(PyTupleObject) - sizeof(PyObject *)

чому додатковий 8 видаляється байт - tp_basicsizeце те, чого я не зміг з’ясувати. (Дивіться коментар MSeifert для можливого пояснення)


Але, це в основному різниця у вашому конкретному прикладі .lists також тримати навколо декількох виділених елементів, що допомагає визначити, коли знову перерозподілити.

Тепер, коли додаються додаткові елементи, списки справді виконують це перерозподіл, щоб отримати додатки O (1). Це призводить до більших розмірів, оскільки MSeifert чудово висвітлює свою відповідь.


Я вважаю, що ob_item[1]здебільшого це заповнювач (тому є сенс, що його відняти від основної величини). tupleВиділяється використанням PyObject_NewVar. Я не з'ясував деталей, тому це лише здогадка про здогадку ...
MSeifert

@MSeifert Вибачте за це, виправлено :-). Я насправді не знаю, я пам’ятаю, що знаходив це в минулому в якийсь момент, але я ніколи не приділяв йому надто великої уваги, можливо, я просто поставлю питання в якийсь момент майбутнього :-)
Димитріс Фасаракіс Хілліард

29

Відповідь MSeifert широко висвітлює її; щоб це було просто, ви можете придумати:

tupleнезмінна. Після встановлення його ви не можете змінити. Тож ви заздалегідь знаєте, скільки пам’яті потрібно виділити для цього об’єкта.

listє змінним. Ви можете додавати або видаляти елементи з нього. Він повинен знати його розмір (для внутрішніх імп.). Він змінює розмір за потребою.

Безкоштовного харчування немає - ці можливості поставляються із вартістю. Звідси накладні витрати на пам'ять для списків.


3

Розмір кортежу є префіксом, тобто при ініціалізації кортежу інтерпретатор виділяє достатньо місця для вміщених даних, і це є кінцем, даючи це незмінне (неможливо змінити), тоді як список є об'єктом, що змінюється, що означає динаміку Виділення пам'яті, щоб уникнути виділення місця кожного разу, коли ви додаєте або змінюєте список (виділіть достатньо місця для вмісту змінених даних та копіювання даних до нього), він виділяє додатковий простір для майбутнього додавання, модифікацій, ... що дуже багато резюмує це

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