A tuple
chiếm ít không gian bộ nhớ hơn trong Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
trong khi list
s chiếm nhiều không gian bộ nhớ hơn:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Điều gì xảy ra trong nội bộ quản lý bộ nhớ Python?
A tuple
chiếm ít không gian bộ nhớ hơn trong Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
trong khi list
s chiếm nhiều không gian bộ nhớ hơn:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Điều gì xảy ra trong nội bộ quản lý bộ nhớ Python?
Câu trả lời:
Tôi giả sử bạn đang sử dụng CPython và với 64bit (Tôi nhận được kết quả tương tự trên CPython 2.7 64 bit của mình). Có thể có sự khác biệt trong các triển khai Python khác hoặc nếu bạn có Python 32bit.
Bất kể việc triển khai như thế nào, list
s có kích thước thay đổi trong khi tuple
s có kích thước cố định.
Vì vậy, tuple
s có thể lưu trữ các phần tử trực tiếp bên trong cấu trúc, mặt khác, các danh sách cần một lớp hướng dẫn (nó lưu trữ một con trỏ đến các phần tử). Lớp chuyển hướng này là một con trỏ, trên hệ thống 64bit là 64bit, do đó 8byte.
Nhưng có một điều khác phải list
làm: Họ phân bổ quá mức. Nếu không, list.append
sẽ luôn là một O(n)
phép toán - để phân bổ khấu hao (nhanh hơn nhiều !!!), nó phân bổ quá mức. Nhưng bây giờ nó phải theo dõi kích thước được phân bổ và kích thước được lấp đầy ( s chỉ cần lưu trữ một kích thước, vì kích thước được phân bổ và lấp đầy luôn giống nhau). Điều đó có nghĩa là mỗi danh sách phải lưu trữ một "kích thước" khác trên hệ thống 64bit là một số nguyên 64bit, lại là 8 byte.O(1)
tuple
Vì vậy, list
s cần nhiều bộ nhớ hơn tuple
s ít nhất 16 byte . Tại sao tôi lại nói "ít nhất"? Vì sự phân bổ quá mức. Phân bổ quá mức có nghĩa là nó phân bổ nhiều không gian hơn mức cần thiết. Tuy nhiên, số lượng phân bổ quá mức phụ thuộc vào "cách" bạn tạo danh sách và lịch sử thêm / xóa:
>>> 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
Tôi quyết định tạo một số hình ảnh để đi kèm với lời giải thích ở trên. Có thể những điều này hữu ích
Đây là cách nó (theo sơ đồ) được lưu trữ trong bộ nhớ trong ví dụ của bạn. Tôi đánh dấu sự khác biệt với các chu kỳ màu đỏ (rảnh tay):
Đó thực sự chỉ là một con số gần đúng vì int
các đối tượng cũng là các đối tượng Python và CPython thậm chí còn sử dụng lại các số nguyên nhỏ, vì vậy, biểu diễn có lẽ chính xác hơn (mặc dù không thể đọc được) của các đối tượng trong bộ nhớ sẽ là:
Liên kết hữu ích:
tuple
struct trong kho lưu trữ CPython cho Python 2.7list
struct trong kho lưu trữ CPython cho Python 2.7int
struct trong kho lưu trữ CPython cho Python 2.7Lưu ý rằng __sizeof__
không thực sự trả lại kích thước "chính xác"! Nó chỉ trả về kích thước của các giá trị được lưu trữ. Tuy nhiên khi bạn sử dụng sys.getsizeof
kết quả sẽ khác:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
Có 24 byte "phụ". Đây là thực , đó là chi phí thu gom rác không được tính trong __sizeof__
phương pháp. Đó là bởi vì bạn thường không được sử dụng các phương pháp ma thuật trực tiếp - hãy sử dụng các hàm biết cách xử lý chúng, trong trường hợp này: sys.getsizeof
(thực sự thêm chi phí GC vào giá trị được trả về từ đó __sizeof__
).
list
cấp phát bộ nhớ stackoverflow.com/questions/40018398/...
list()
hoặc hiểu danh sách.
Tôi sẽ đi sâu hơn vào cơ sở mã CPython để chúng ta có thể xem cách các kích thước thực sự được tính toán. Trong ví dụ cụ thể của bạn , không có phân bổ vượt mức nào được thực hiện, vì vậy tôi sẽ không đề cập đến điều đó .
Tôi sẽ sử dụng các giá trị 64-bit ở đây, giống như bạn.
Kích thước cho list
s được tính từ hàm sau 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);
}
Dưới đây Py_TYPE(self)
là một macro mà grabs ob_type
củaself
(trả về PyList_Type
) trong khi _PyObject_SIZE
là một macro khác lấy tp_basicsize
từ loại đó. tp_basicsize
được tính như sizeof(PyListObject)
đâu PyListObject
là cấu trúc cá thể.
Các PyListObject
cấu trúc có ba lĩnh vực:
PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
chúng có những nhận xét (mà tôi đã cắt) giải thích chúng là gì, hãy theo liên kết ở trên để đọc chúng. PyObject_VAR_HEAD
mở rộng thành ba 8 lĩnh vực byte ( ob_refcount
, ob_type
và ob_size
) do đó, một 24
đóng góp byte.
Vì vậy, bây giờ res
là:
sizeof(PyListObject) + self->allocated * sizeof(void*)
hoặc là:
40 + self->allocated * sizeof(void*)
Nếu thể hiện danh sách có các phần tử được cấp phát. phần thứ hai tính toán đóng góp của họ. self->allocated
, như tên của nó, chứa số phần tử được phân bổ.
Không có bất kỳ phần tử nào, kích thước của danh sách được tính là:
>>> [].__sizeof__()
40
tức là kích thước của cấu trúc thể hiện.
tuple
các đối tượng không xác định một tuple_sizeof
chức năng. Thay vào đó, họ sử dụngobject_sizeof
để tính toán kích thước của chúng:
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);
}
Điều này, đối với list
s, lấy tp_basicsize
và, nếu đối tượng có số khác 0 tp_itemsize
(có nghĩa là nó có các phiên bản có độ dài thay đổi), nó sẽ nhân số mục trong bộ (mà nó nhận được thông quaPy_SIZE
) với tp_itemsize
.
tp_basicsize
lại sử dụng sizeof(PyTupleObject)
nơi PyTupleObject
cấu trúc chứa :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Vì vậy, không có bất kỳ phần tử nào (nghĩa là Py_SIZE
trả về 0
), kích thước của các bộ giá trị trống bằng sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
Huh? Chà, đây là một điều kỳ lạ mà tôi chưa tìm ra lời giải thích, thực tế tp_basicsize
của tuple
s được tính như sau:
sizeof(PyTupleObject) - sizeof(PyObject *)
tại sao một 8
byte bổ sung bị xóa tp_basicsize
là điều mà tôi chưa thể tìm hiểu. (Xem bình luận của MSeifert để có lời giải thích khả thi)
Nhưng, về cơ bản đây là sự khác biệt trong ví dụ cụ thể của bạn . list
s cũng giữ khoảng một số phần tử đã phân bổ giúp xác định thời điểm phân bổ quá mức.
Bây giờ, khi các phần tử bổ sung được thêm vào, danh sách thực sự thực hiện phân bổ quá mức này để đạt được phần phụ O (1). Điều này dẫn đến kích thước lớn hơn khi MSeifert bao hàm độc đáo câu trả lời của mình.
ob_item[1]
phần lớn là một trình giữ chỗ (vì vậy nó có ý nghĩa là nó được trừ khỏi kích thước cơ bản). Các tuple
được phân bổ sử dụng PyObject_NewVar
. Tôi chưa tìm ra các chi tiết vì vậy đó là chỉ là một phỏng đoán giáo dục ...
Câu trả lời MSeifert bao hàm nó một cách rộng rãi; để giữ cho nó đơn giản, bạn có thể nghĩ đến:
tuple
là bất biến. Sau khi nó được đặt, bạn không thể thay đổi nó. Vì vậy, bạn biết trước mình cần cấp phát bao nhiêu bộ nhớ cho đối tượng đó.
list
là có thể thay đổi. Bạn có thể thêm hoặc xóa các mục vào hoặc khỏi nó. Nó phải biết kích thước của nó (đối với cấy ghép nội bộ). Nó thay đổi kích thước khi cần thiết.
Không có bữa ăn miễn phí - những khả năng này đi kèm với một khoản chi phí. Do đó, chi phí trong bộ nhớ cho danh sách.
Kích thước của tuple được đặt trước, có nghĩa là khi khởi tạo tuple, trình thông dịch phân bổ đủ không gian cho dữ liệu được chứa và đó là kết thúc của nó, cho nó là bất biến (không thể sửa đổi), trong khi danh sách là một đối tượng có thể thay đổi do đó ngụ ý động phân bổ bộ nhớ, do đó, để tránh phân bổ không gian mỗi khi bạn thêm hoặc sửa đổi danh sách (phân bổ đủ không gian để chứa dữ liệu đã thay đổi và sao chép dữ liệu vào đó), nó phân bổ không gian bổ sung cho các lần thêm, sửa đổi, ... trong tương lai. Tổng cộng lại.