Mengapa tupel mengambil lebih sedikit ruang dalam memori daripada daftar?

105

A tuplemembutuhkan lebih sedikit ruang memori dengan Python:

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

sedangkan lists membutuhkan lebih banyak ruang memori:

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

Apa yang terjadi secara internal pada manajemen memori Python?

JON
sumber
1
Saya tidak yakin bagaimana ini bekerja secara internal, tetapi objek daftar setidaknya memiliki lebih banyak fungsi seperti misalnya append yang tidak dimiliki tupel. Oleh karena itu, masuk akal jika tupel sebagai jenis objek yang lebih sederhana menjadi lebih kecil
Metareven
Saya pikir itu juga tergantung pada mesin ke mesin .... bagi saya ketika saya memeriksa a = (1,2,3) membutuhkan 72 dan b = [1,2,3] membutuhkan 88.
Amrit
6
Tupel Python tidak dapat diubah. Objek yang dapat berubah memiliki biaya tambahan untuk menangani perubahan waktu proses.
Lee Daniel Crocker
@Metareven jumlah metode yang dimiliki suatu jenis tidak memengaruhi ruang memori yang digunakan instance. Daftar metode dan kodenya ditangani oleh prototipe objek, tetapi instance hanya menyimpan data dan variabel internal.
jjmontes

Jawaban:

144

Saya berasumsi Anda menggunakan CPython dan dengan 64bits (Saya mendapatkan hasil yang sama pada CPython 2.7 64-bit saya). Mungkin ada perbedaan dalam implementasi Python lainnya atau jika Anda memiliki Python 32-bit.

Terlepas dari implementasinya, lists berukuran variabel sedangkan tuples berukuran tetap.

Jadi tuples dapat menyimpan elemen secara langsung di dalam struct, list di sisi lain memerlukan lapisan tipuan (ini menyimpan pointer ke elemen). Lapisan tipuan ini adalah pointer, pada sistem 64bit yang 64bit, karenanya 8bytes.

Tapi ada hal lain yang bisa listdilakukan: Mereka mengalokasikan terlalu banyak. Jika tidak list.appendakan menjadi O(n)operasi selalu - untuk membuatnya diamortisasi O(1)(jauh lebih cepat !!!) itu over-mengalokasikan. Tapi sekarang harus melacak ukuran yang dialokasikan dan ukuran yang terisi ( tuplehanya perlu menyimpan satu ukuran, karena ukuran yang dialokasikan dan diisi selalu identik). Itu berarti setiap daftar harus menyimpan "ukuran" lain yang pada sistem 64bit adalah integer 64bit, lagi-lagi 8 byte.

Jadi lists membutuhkan setidaknya 16 byte lebih banyak memori daripada tuples. Mengapa saya mengatakan "setidaknya"? Karena alokasi berlebih. Alokasi berlebih berarti mengalokasikan lebih banyak ruang daripada yang dibutuhkan. Namun, jumlah alokasi berlebih bergantung pada "cara" Anda membuat daftar dan riwayat penambahan / penghapusan:

>>> 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

Gambar-gambar

Saya memutuskan untuk membuat beberapa gambar untuk melengkapi penjelasan di atas. Mungkin ini membantu

Beginilah cara (secara skematis) disimpan dalam memori dalam contoh Anda. Saya menyoroti perbedaan dengan siklus merah (tangan bebas):

masukkan deskripsi gambar di sini

Itu sebenarnya hanya perkiraan karena intobjek juga merupakan objek Python dan CPython bahkan menggunakan kembali bilangan bulat kecil, jadi representasi yang mungkin lebih akurat (meskipun tidak dapat dibaca) dari objek dalam memori adalah:

masukkan deskripsi gambar di sini

Link yang berguna:

Perhatikan bahwa __sizeof__tidak benar-benar mengembalikan ukuran yang "benar"! Ini hanya mengembalikan ukuran nilai yang disimpan. Namun ketika Anda menggunakan sys.getsizeofhasilnya berbeda:

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

Ada 24 byte "ekstra". Ini nyata , itulah overhead pengumpul sampah yang tidak diperhitungkan dalam __sizeof__metode. Itu karena Anda pada umumnya tidak seharusnya menggunakan metode ajaib secara langsung - gunakan fungsi yang mengetahui cara menanganinya, dalam hal ini: sys.getsizeof(yang sebenarnya menambahkan overhead GC ke nilai yang dikembalikan dari __sizeof__).

MSeifert
sumber
Re " Jadi daftar membutuhkan setidaknya 16 byte lebih banyak memori daripada tupel. ", Bukankah itu 8? Satu ukuran untuk tupel dan dua ukuran untuk daftar berarti satu ukuran ekstra untuk daftar.
ikegami
1
Ya, list memiliki satu ekstra "size" (8 byte) tetapi juga menyimpan pointer (8byte) ke "array of PyObject" daripada menyimpannya di struct secara langsung (apa yang dilakukan tupel). 8 + 8 = 16.
MSeifert
2
Tautan berguna lainnya tentang listalokasi memori stackoverflow.com/questions/40018398/…
vishes_shell
@vishes_shell Itu tidak benar-benar terkait dengan pertanyaan karena kode dalam pertanyaan tidak mengalokasikan secara berlebihan sama sekali . Tapi ya itu berguna jika Anda ingin tahu lebih banyak tentang jumlah alokasi berlebih saat menggunakan list()atau pemahaman daftar.
MSeifert
1
@ user3349993 Tupel tidak dapat diubah, jadi Anda tidak dapat menambahkan ke tupel atau menghapus item dari tupel.
MSeifert
31

Saya akan mempelajari lebih dalam basis kode CPython sehingga kita dapat melihat bagaimana ukuran sebenarnya dihitung. Dalam contoh spesifik Anda , tidak ada alokasi berlebih yang dilakukan, jadi saya tidak akan menyentuhnya .

Saya akan menggunakan nilai 64-bit di sini, seperti Anda.


Ukuran untuk lists dihitung dari fungsi berikut, 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);
}

Berikut Py_TYPE(self)adalah makro yang mengambil ob_typedari self(kembali PyList_Type) sementara _PyObject_SIZEadalah makro lain yang mengambil tp_basicsizedari jenis itu. tp_basicsizedihitung sebagai di sizeof(PyListObject)mana PyListObjectstruct instance.

The PyListObjectStruktur memiliki tiga bidang:

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

ini memiliki komentar (yang saya pangkas) menjelaskan apa itu, ikuti tautan di atas untuk membacanya. PyObject_VAR_HEADberkembang menjadi tiga bidang 8 byte ( ob_refcount, ob_typedan ob_size) jadi 24kontribusi byte.

Jadi untuk saat resini adalah:

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

atau:

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

Jika contoh daftar memiliki elemen yang dialokasikan. bagian kedua menghitung kontribusinya. self->allocated, seperti yang tersirat dari namanya, menampung jumlah elemen yang dialokasikan.

Tanpa elemen apa pun, ukuran daftar dihitung menjadi:

>>> [].__sizeof__()
40

yaitu ukuran dari instance struct.


tupleobjek tidak mendefinisikan tuple_sizeoffungsi. Sebaliknya, mereka menggunakan object_sizeofuntuk menghitung ukurannya:

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);
}

Ini, seperti untuk lists, mengambil tp_basicsizedan, jika objek memiliki non-nol tp_itemsize(artinya memiliki instance dengan panjang variabel), ia mengalikan jumlah item dalam tupel (yang diterimanya Py_SIZE) dengan tp_itemsize.

tp_basicsizelagi menggunakan di sizeof(PyTupleObject)mana PyTupleObjectstruct berisi :

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

Jadi, tanpa elemen apa pun (yaitu, Py_SIZEmengembalikan 0) ukuran tupel kosong sama dengan sizeof(PyTupleObject):

>>> ().__sizeof__()
24

Hah? Nah, inilah keanehan yang belum saya temukan penjelasannya, tp_basicsizedari tuples sebenarnya dihitung sebagai berikut:

sizeof(PyTupleObject) - sizeof(PyObject *)

mengapa 8byte tambahan dihapus dari tp_basicsizeadalah sesuatu yang saya belum bisa temukan. (Lihat komentar MSeifert untuk penjelasan yang mungkin)


Tapi, pada dasarnya ini adalah perbedaan dalam contoh spesifik Anda . lists juga menyimpan sejumlah elemen yang dialokasikan yang membantu menentukan kapan harus mengalokasikan berlebihan lagi.

Sekarang, ketika elemen tambahan ditambahkan, daftar memang melakukan alokasi berlebih ini untuk mencapai O (1) tambahan. Ini menghasilkan ukuran yang lebih besar karena MSeifert menutupi jawabannya dengan baik.

Dimitris Fasarakis Hilliard
sumber
Saya yakin ob_item[1]sebagian besar adalah placeholder (jadi masuk akal jika dikurangi dari ukuran dasar). The tupledialokasikan menggunakan PyObject_NewVar. Saya belum menemukan detailnya jadi itu hanya tebakan ...
MSeifert
@MSeifert Maaf untuk itu, diperbaiki :-). Saya benar-benar tidak tahu, saya ingat pernah menemukannya di masa lalu tetapi saya tidak pernah memberikan terlalu banyak perhatian, mungkin saya hanya akan mengajukan pertanyaan di masa mendatang :-)
Dimitris Fasarakis Hilliard
29

Jawaban MSeifert mencakupnya secara luas; agar tetap sederhana, Anda dapat memikirkan:

tupletidak dapat diubah. Setelah disetel, Anda tidak dapat mengubahnya. Jadi, Anda tahu sebelumnya berapa banyak memori yang perlu dialokasikan untuk objek itu.

listbisa berubah. Anda dapat menambah atau menghapus item ke atau dari itu. Itu harus mengetahui ukurannya (untuk impl. Internal). Ini mengubah ukuran sesuai kebutuhan.

Tidak ada makanan gratis - kemampuan ini dikenakan biaya. Karenanya overhead dalam memori untuk daftar.

Chen A.
sumber
3

Ukuran tupel diawali, yang berarti pada inisialisasi tupel, interpreter mengalokasikan ruang yang cukup untuk data yang terkandung, dan itulah akhirnya, memberikannya tidak dapat diubah (tidak dapat diubah), sedangkan daftar adalah objek yang dapat berubah sehingga menyiratkan dinamika alokasi memori, jadi untuk menghindari mengalokasikan ruang setiap kali Anda menambahkan atau memodifikasi daftar (mengalokasikan cukup ruang untuk memuat data yang diubah dan menyalin datanya ke dalamnya), ia mengalokasikan ruang tambahan untuk penambahan di masa mendatang, modifikasi, ... yang cukup banyak meringkasnya.

rachid el kedmiri
sumber