Tại sao hai danh sách giống hệt nhau có dấu chân bộ nhớ khác nhau?


155

Tôi đã tạo hai danh sách l1l2mỗi danh sách có một phương thức tạo khác nhau:

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

Nhưng đầu ra làm tôi ngạc nhiên:

Size of l1 = 144
Size of l2 = 192

Danh sách được tạo với sự hiểu biết danh sách là một kích thước lớn hơn trong bộ nhớ, nhưng hai danh sách này giống hệt nhau trong Python.

Tại sao vậy? Đây có phải là một số điều nội bộ CPython, hoặc một số giải thích khác?


2
Có lẽ, toán tử lặp lại sẽ gọi một số hàm có kích thước chính xác cho mảng bên dưới. Lưu ý, 144 == sys.getsizeof([]) + 8*10)trong đó 8 là kích thước của một con trỏ.
juanpa.arrivillaga

1
Lưu ý rằng nếu bạn thay đổi 10thành 11, [None] * 11danh sách có kích thước 152, nhưng mức độ hiểu danh sách vẫn có kích thước 192. Câu hỏi được liên kết trước đó không phải là một bản sao chính xác, nhưng nó có liên quan để hiểu lý do tại sao điều này xảy ra.
Patrick Haugh

Câu trả lời:


162

Khi bạn viết [None] * 10, Python biết rằng nó sẽ cần một danh sách chính xác 10 đối tượng, vì vậy nó phân bổ chính xác điều đó.

Khi bạn sử dụng khả năng hiểu danh sách, Python không biết nó sẽ cần bao nhiêu. Vì vậy, nó dần dần phát triển danh sách như các yếu tố được thêm vào. Đối với mỗi lần tái phân bổ, nó phân bổ nhiều phòng hơn mức cần thiết ngay lập tức, do đó nó không phải phân bổ lại cho từng yếu tố. Danh sách kết quả có thể sẽ lớn hơn một chút so với cần thiết.

Bạn có thể thấy hành vi này khi so sánh các danh sách được tạo với kích thước tương tự:

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

Bạn có thể thấy rằng phương thức đầu tiên chỉ phân bổ những gì cần thiết, trong khi phương thức thứ hai phát triển theo định kỳ. Trong ví dụ này, nó phân bổ đủ cho 16 phần tử và phải phân bổ lại khi đạt đến thứ 17.


1
Vâng, điều đó có ý nghĩa. Có lẽ tốt hơn nên tạo danh sách *khi tôi biết kích thước ở phía trước.
Andrej Kesely

27
@AndrejKesely Chỉ sử dụng [x] * nvới bất biến xtrong danh sách của bạn. Danh sách kết quả sẽ giữ các tham chiếu đến đối tượng giống hệt nhau.
schwobaseggl

5
@schwobaseggl tốt, đó có thể là những gì bạn muốn, nhưng thật tốt khi hiểu điều đó.
juanpa.arrivillaga

19
@ juanpa.arrivillaga Đúng, nó có thể. Nhưng thường thì không phải và đặc biệt là SO có đầy đủ các áp phích tự hỏi tại sao tất cả dữ liệu của họ thay đổi đồng thời: D
schwobaseggl

50

Như đã lưu ý trong câu hỏi này, việc hiểu danh sách sử dụng list.appenddưới mui xe, vì vậy nó sẽ gọi phương thức thay đổi kích thước danh sách, mà tổng thể hóa.

Để chứng minh điều này với chính mình, bạn thực sự có thể sử dụng trình giải mã 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
>>>

Lưu ý LIST_APPENDopcode trong việc tháo gỡ <listcomp>đối tượng mã. Từ các tài liệu :

LIST_APPEND (i)

Các cuộc gọi list.append(TOS[-i], TOS). Được sử dụng để thực hiện việc hiểu danh sách.

Bây giờ, đối với hoạt động lặp lại danh sách, chúng tôi có một gợi ý về những gì đang xảy ra nếu chúng tôi xem xét:

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

Vì vậy, nó dường như có thể phân bổ chính xác kích thước. Nhìn vào mã nguồn , chúng tôi thấy đây chính xác là những gì xảy ra:

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

Cụ thể, ở đây : size = Py_SIZE(a) * n;. Phần còn lại của các hàm chỉ đơn giản là điền vào mảng.


"Như đã lưu ý trong câu hỏi này, việc hiểu danh sách sử dụng list.append under the hood" Tôi nghĩ rằng chính xác hơn để nói rằng nó sử dụng .extend().
Tích lũy

@Acccumulation tại sao bạn tin như vậy?
juanpa.arrivillaga

Bởi vì nó không nối từng phần tử một. Khi bạn nối các phần tử vào danh sách, bạn thực sự đang tạo một danh sách mới, với cấp phát bộ nhớ mới và đưa danh sách vào phân bổ bộ nhớ mới đó. Mặt khác, việc hiểu danh sách, đưa hầu hết các phần tử mới vào bộ nhớ đã được phân bổ và khi hết bộ nhớ được phân bổ, chúng phân bổ một bộ nhớ khác, không đủ cho phần tử mới.
Tích lũy

7
@Acccumulation Điều đó không chính xác. list.appendlà một hoạt động thời gian không đổi được khấu hao bởi vì khi một danh sách thay đổi kích thước, nó sẽ phân bổ lại. Do đó, không phải mọi hoạt động chắp thêm đều dẫn đến một mảng mới được phân bổ. Trong mọi trường hợp các câu hỏi mà tôi có liên quan đến chương trình bạn trong mã nguồn mà trên thực tế, danh sách comprehensions làm sử dụng list.append,. Tôi sẽ quay lại với máy tính xách tay của mình ngay lập tức và tôi có thể cho bạn thấy mã byte được phân tách để hiểu danh sách và LIST_APPENDmã opcode tương ứng
juanpa.arrivillaga 26/07/18

3

Không có khối bộ nhớ, nhưng nó không phải là kích thước được chỉ định trước. Ngoài ra, có một số khoảng cách bổ sung trong một mảng giữa các phần tử mảng. Bạn có thể thấy điều này bằng cách chạy:

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

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

Mà không tổng kích thước của l2, nhưng là ít hơn.

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

Và điều này là lớn hơn nhiều so với một phần mười của kích thước l1.

Số của bạn sẽ thay đổi tùy thuộc vào cả chi tiết của hệ điều hành và chi tiết về việc sử dụng bộ nhớ hiện tại trong hệ điều hành của bạn. Kích thước của [Không] không bao giờ có thể lớn hơn bộ nhớ liền kề khả dụng nơi biến được đặt thành được lưu trữ và biến có thể phải được di chuyển nếu sau đó nó được cấp phát động lớn hơn.


1
Nonekhông thực sự được lưu trữ trong mảng bên dưới, điều duy nhất được lưu trữ là một PyObjectcon trỏ (8 byte). Tất cả các đối tượng Python được phân bổ trên heap. Nonelà một singleton, do đó, việc có một danh sách có nhiều nones chỉ đơn giản là sẽ tạo ra một mảng các con trỏ PyObject cho cùng một Noneđối tượng trên heap (và không sử dụng bộ nhớ bổ sung trong quá trình cho mỗi bổ sung None). Tôi không chắc bạn có ý gì "Không ai có kích thước được chỉ định trước", nhưng điều đó không đúng. Cuối cùng, vòng lặp của bạn với getsizeofmỗi yếu tố không thể hiện những gì bạn dường như nghĩ rằng nó đang thể hiện.
juanpa.arrivillaga

Nếu như bạn nói là đúng, kích thước của [Không] * 10 phải giống với kích thước của [Không]. Nhưng rõ ràng đây không phải là như vậy-- một số lưu trữ bổ sung đã được thêm vào. Trên thực tế, kích thước của [Không] lặp lại mười lần (160) cũng nhỏ hơn kích thước của [Không] nhân với mười. Như bạn chỉ ra, rõ ràng kích thước của con trỏ tới [Không] nhỏ hơn kích thước của chính [Không] (16 byte thay vì 72 byte). Tuy nhiên, 160 + 32 là 192. Tôi không nghĩ câu trả lời trước cũng giải quyết được vấn đề hoàn toàn. Rõ ràng là một số lượng nhỏ bộ nhớ (có thể phụ thuộc vào trạng thái máy) được phân bổ.
StevenJD

"Nếu như bạn nói là đúng, kích thước của [Không] * 10 phải giống với kích thước của [Không]" thì tôi đang nói điều gì có thể ám chỉ điều đó? Một lần nữa, bạn dường như đang tập trung vào thực tế là bộ đệm bên dưới được phân bổ quá mức hoặc kích thước của danh sách bao gồm nhiều hơn kích thước của bộ đệm bên dưới (tất nhiên là vậy), nhưng đó không phải là điểm của câu hỏi này. Một lần nữa, việc bạn sử dụng gestsizeoftrên mỗi elecủa l2được gây hiểu lầm vì getsizeof(l2) không đưa vào tài khoản kích thước của các yếu tố bên trong container .
juanpa.arrivillaga

Để chứng minh cho bản thân rằng yêu cầu cuối cùng, hãy làm l1 = [None]; l2 = [None]*100; l3 = [l2]sau đó print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). bạn sẽ nhận được một kết quả như : 72 864 72. Đó là, tương ứng, 64 + 1*8, 64 + 100*8, và 64 + 1*8, một lần nữa, giả sử một hệ thống 64bit với kích thước con trỏ 8 byte.
juanpa.arrivillaga

1
Như tôi đã nói, sys.getsizeof* không tính đến kích thước của các vật phẩm trong container. Từ tài liệu : "Chỉ có tiêu thụ bộ nhớ do trực tiếp đến đối tượng được chiếm, không phải là tiêu thụ bộ nhớ của các đối tượng nó đề cập đến ... Xem sizeof đệ quy công thức cho một ví dụ của việc sử dụng getsizeof () một cách đệ quy để tìm kích thước của container và tất cả nội dung của họ. "
juanpa.arrivillaga
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.