Tại sao các bộ dữ liệu chiếm ít không gian trong bộ nhớ hơn danh sách?


105

A tuplechiếm ít không gian bộ nhớ hơn trong Python:

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

trong khi lists 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?


1
Tôi không chắc điều này hoạt động như thế nào bên trong, nhưng đối tượng danh sách ít nhất có nhiều chức năng hơn như append chẳng hạn mà tuple không có. Do đó, nó làm cho tinh thần cho các tuple như một loại đơn giản của đối tượng nhỏ hơn
Metareven

Tôi nghĩ rằng nó cũng phụ thuộc vào máy để máy .... cho tôi khi tôi kiểm tra a = (1,2,3) mất 72 và b = [1,2,3] mất 88.
Amrit

6
Bộ giá trị Python là bất biến. Các đối tượng có thể thay đổi có thêm chi phí để đối phó với các thay đổi trong thời gian chạy.
Lee Daniel Crocker

@ Kiểm tra số lượng phương thức mà một kiểu có không ảnh hưởng đến không gian bộ nhớ mà các cá thể sử dụng. Danh sách phương thức và mã của chúng được xử lý bởi nguyên mẫu đối tượng, nhưng các phiên bản chỉ lưu trữ dữ liệu và các biến nội bộ.
jjmontes

Câu trả lời:


144

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, lists có kích thước thay đổi trong khi tuples có kích thước cố định.

Vì vậy, tuples 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 listlàm: Họ phân bổ quá mức. Nếu không, list.appendsẽ 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, lists cần nhiều bộ nhớ hơn tuples í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

Hình ảnh

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

nhập mô tả hình ảnh ở đây

Đó thực sự chỉ là một con số gần đúng vì intcá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à:

nhập mô tả hình ảnh ở đây

Liên kết hữu ích:

Lư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.getsizeofkế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__).


Re " Vì vậy, danh sách cần nhiều bộ nhớ hơn ít nhất 16 byte so với bộ giá trị. ", Đó sẽ không phải là 8 sao? Một kích thước cho bộ giá trị và hai kích thước cho danh sách có nghĩa là một kích thước bổ sung cho danh sách.
ikegami

1
Có, danh sách có thêm một "kích thước" (8 byte) nhưng cũng lưu trữ một con trỏ (8byte) tới "mảng PyObject" thay vì lưu trữ chúng trong cấu trúc trực tiếp (bộ tuple thực hiện). 8 + 8 = 16.
MSeifert

2
Một hữu ích liên kết về listcấp phát bộ nhớ stackoverflow.com/questions/40018398/...
vishes_shell

@vishes_shell Điều đó không thực sự liên quan đến câu hỏi vì mã trong câu hỏi không phân bổ quá mức . Nhưng có, nó hữu ích trong trường hợp bạn muốn biết thêm về lượng phân bổ quá mức khi sử dụng list()hoặc hiểu danh sách.
MSeifert

1
@ user3349993 Tuples là bất biến, vì vậy bạn không thể nối vào một tuple hoặc xóa một mục khỏi một tuple.
MSeifert

31

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 lists đượ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_typecủaself (trả về PyList_Type) trong khi _PyObject_SIZElà một macro khác lấy tp_basicsizetừ loại đó. tp_basicsizeđược tính như sizeof(PyListObject)đâu PyListObjectlà cấu trúc cá thể.

Các PyListObjectcấ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_HEADmở rộng thành ba 8 lĩnh vực byte ( ob_refcount, ob_typeob_size) do đó, một 24đóng góp byte.

Vì vậy, bây giờ reslà:

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.


tuplecác đối tượng không xác định một tuple_sizeofchứ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 lists, lấy tp_basicsizevà, 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_basicsizelại sử dụng sizeof(PyTupleObject)nơi PyTupleObjectcấ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_SIZEtrả 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_basicsizecủa tuples được tính như sau:

sizeof(PyTupleObject) - sizeof(PyObject *)

tại sao một 8byte bổ sung bị xóa tp_basicsizelà đ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 . lists 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.


Tôi tin rằng 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 ...
MSeifert

@MSeifert Xin lỗi vì điều đó, đã sửa :-). Tôi thực sự không biết, tôi nhớ đã tìm thấy nó trong quá khứ tại một số thời điểm nhưng tôi không bao giờ chú ý đến nó quá nhiều, có lẽ tôi sẽ hỏi một câu hỏi vào một thời điểm nào đó trong tương lai :-)
Dimitris Fasarakis Hilliard

29

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:

tuplelà 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 đó.

listlà 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.


3

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.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.