list () sử dụng nhiều bộ nhớ hơn một chút so với khả năng hiểu danh sách


79

Vì vậy, tôi đã chơi với listcác đồ vật và tìm thấy một điều kỳ lạ rằng nếu listđược tạo ra với list()nó sẽ sử dụng nhiều bộ nhớ hơn là khả năng hiểu danh sách? Tôi đang sử dụng Python 3.5.2

In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008

Từ các tài liệu :

Danh sách có thể được xây dựng theo một số cách:

  • Sử dụng một cặp dấu ngoặc vuông để biểu thị danh sách trống: []
  • Sử dụng dấu ngoặc vuông, tách mục có dấu phẩy: [a],[a, b, c]
  • Sử dụng khả năng hiểu danh sách: [x for x in iterable]
  • Sử dụng hàm tạo kiểu: list()hoặclist(iterable)

Nhưng có vẻ như sử dụng list()nó sử dụng nhiều bộ nhớ hơn.

Và càng listlớn, khoảng cách càng tăng.

Sự khác biệt trong bộ nhớ

Tại sao điều này xảy ra?

CẬP NHẬT # 1

Kiểm tra với Python 3.6.0b2:

Python 3.6.0b2 (default, Oct 11 2016, 11:52:53) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912

CẬP NHẬT # 2

Kiểm tra với Python 2.7.12:

Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920

3
Đó là một câu hỏi rất thú vị. Tôi có thể tái tạo hiện tượng trong Python 3.4.3. Thú vị hơn nữa: trên Python 2.7.5 sys.getsizeof(list(range(100)))là 1016, getsizeof(range(100))là 872 và getsizeof([i for i in range(100)])là 920. Tất cả đều có kiểu list.
Sven Festersen

Điều quan tâm là sự khác biệt này cũng có trong Python 2.7.10 (mặc dù các con số thực tế khác với Python 3). Cũng có trong 3.5 và 3.6b.
cdarke

Tôi cũng nhận được các số tương tự cho Python 2.7.6 như @SvenFestersen, khi sử dụng xrange.
RemcoGerlich

2
Có thể có một lời giải thích ở đây: stackoverflow.com/questions/7247298/size-of-list-in-memory . Nếu một trong các phương pháp tạo danh sách bằng cách sử dụng append(), có thể có sự phân bổ quá mức bộ nhớ. Tôi đoán cách duy nhất để thực sự làm rõ điều này là xem các nguồn Python.
Sven Festersen

Chỉ 10% nữa (bạn không thực sự nói điều đó ở bất cứ đâu). Tôi muốn nói lại tiêu đề thành "nhiều hơn một chút".
smci

Câu trả lời:


61

Tôi nghĩ rằng bạn đang thấy các mẫu phân bổ quá mức, đây là mẫu từ nguồn :


In các kích thước của danh sách có độ dài từ 0-88, bạn có thể thấy các mẫu trùng khớp:

# create comprehensions for sizes 0-88
comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)]

# only take those that resulted in growth compared to previous length
steps = zip(comprehensions, comprehensions[1:])
growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]]

# print the results:
for growth in growths:
    print(growth)

Kết quả (định dạng là (list length, (old total size, new total size))):

(0, (64, 96)) 
(4, (96, 128))
(8, (128, 192))
(16, (192, 264))
(25, (264, 344))
(35, (344, 432))
(46, (432, 528))
(58, (528, 640))
(72, (640, 768))
(88, (768, 912))

Việc phân bổ quá mức được thực hiện vì lý do hiệu suất cho phép danh sách phát triển mà không cần phân bổ thêm bộ nhớ với mỗi lần tăng trưởng ( hiệu suất phân bổ tốt hơn ).

Một lý do có thể xảy ra cho sự khác biệt với việc sử dụng tính năng hiểu danh sách, đó là tính năng hiểu danh sách không thể tính toán một cách xác định kích thước của danh sách đã tạo, nhưng list()có thể. Điều này có nghĩa là sự hiểu biết sẽ liên tục phát triển danh sách khi nó lấp đầy nó bằng cách sử dụng phân bổ quá mức cho đến khi cuối cùng lấp đầy nó.

Có thể là sẽ không phát triển bộ đệm phân bổ quá mức với các nút được cấp phát không sử dụng sau khi hoàn thành (trên thực tế, trong hầu hết các trường hợp, điều đó sẽ không đạt được mục đích phân bổ quá mức).

list(), tuy nhiên, có thể thêm một số bộ đệm bất kể kích thước danh sách vì nó biết trước kích thước danh sách cuối cùng.


Một bằng chứng hỗ trợ khác, cũng từ nguồn, là chúng ta thấy cách gọi toàn bộ danh sáchLIST_APPEND , cho biết việc sử dụng, từ đó cho biết sử dụng list.resizebộ đệm phân bổ trước mà không biết bao nhiêu phần trăm của nó sẽ được lấp đầy. Điều này phù hợp với hành vi bạn đang thấy.


Để kết luận, list()sẽ phân bổ trước nhiều nút hơn như một chức năng của kích thước danh sách

>>> sys.getsizeof(list([1,2,3]))
60
>>> sys.getsizeof(list([1,2,3,4]))
64

Tính năng hiểu danh sách không biết kích thước danh sách vì vậy nó sử dụng các hoạt động nối thêm khi nó phát triển, làm cạn bộ đệm phân bổ trước:

# one item before filling pre-allocation buffer completely
>>> sys.getsizeof([i for i in [1,2,3]]) 
52
# fills pre-allocation buffer completely
# note that size did not change, we still have buffered unused nodes
>>> sys.getsizeof([i for i in [1,2,3,4]]) 
52
# grows pre-allocation buffer
>>> sys.getsizeof([i for i in [1,2,3,4,5]])
68

4
Nhưng tại sao việc phân bổ quá mức lại xảy ra với cái này mà không phải cái kia?
cdarke

Điều này đặc biệt là từ list.resize. Tôi không phải là chuyên gia trong việc điều hướng theo nguồn của anh ấy, nhưng nếu một người gọi thay đổi kích thước và người kia thì không - điều đó có thể giải thích sự khác biệt.
Reut Sharabani

6
Python 3.5.2 tại đây. Thử in các kích thước danh sách từ 0 đến 35 trong vòng lặp. Đối với danh sách tôi thấy 64, 96, 104, 112, 120, 128, 136, 144, 160, 192, 200, 208, 216, 224, 232, 240, 256, 264, 272, 280, 288, 296, 304, 312, 328, 336, 344, 352, 360, 368, 376, 384, 400, 408, 416và để hiểu 64, 96, 96, 96, 96, 128, 128, 128, 128, 192, 192, 192, 192, 192, 192, 192, 192, 264, 264, 264, 264, 264, 264, 264, 264, 264, 344, 344, 344, 344, 344, 344, 344, 344, 344. Tôi sẽ ngoại trừ sự hiểu biết đó là người dường như phân bổ trước bộ nhớ là thuật toán sử dụng nhiều RAM hơn cho các kích thước nhất định.
tavo

Tôi cũng mong đợi như vậy. Tôi có thể xem xét thêm về nó sớm. Nhận xét tốt.
Reut Sharabani

4
thực sự list()xác định một cách xác định kích thước danh sách, điều mà khả năng hiểu danh sách không thể làm được. Điều này cho thấy khả năng hiểu danh sách không phải lúc nào cũng "kích hoạt" sự tăng trưởng "cuối cùng" của danh sách. Có thể có ý nghĩa.
Reut Sharabani

30

Cảm ơn mọi người đã giúp tôi hiểu Python tuyệt vời đó.

Tôi không muốn đặt câu hỏi quá lớn (đó là lý do tại sao tôi đăng câu trả lời), chỉ muốn thể hiện và chia sẻ suy nghĩ của mình.

Như @ReutSharabani đã lưu ý chính xác: "danh sách () xác định kích thước danh sách". Bạn có thể thấy nó từ biểu đồ đó.

đồ thị kích thước

Khi bạn appendhoặc sử dụng khả năng hiểu danh sách, bạn luôn có một số loại ranh giới mở rộng khi bạn đến một thời điểm nào đó. Và với list()bạn gần như có ranh giới giống nhau, nhưng chúng đang trôi nổi.

CẬP NHẬT

Vì vậy, cảm ơn @ReutSharabani , @tavo , @SvenFestersen

Tóm lại: việc list()phân bổ trước bộ nhớ phụ thuộc vào kích thước danh sách, khả năng hiểu danh sách không thể làm được điều đó (nó yêu cầu thêm bộ nhớ khi cần, chẳng hạn .append()). Đó là lý do tại sao list()lưu trữ nhiều bộ nhớ hơn.

Một biểu đồ khác, hiển thị list()bộ nhớ phân bổ trước. Vì vậy, đường màu xanh lá cây hiển thị list(range(830))phần tử phụ theo phần tử và trong một thời gian bộ nhớ không thay đổi.

list () định vị trước bộ nhớ

CẬP NHẬT 2

Như @Barmar đã lưu ý trong nhận xét bên dưới, list()tôi phải nhanh hơn khả năng hiểu danh sách, vì vậy tôi đã chạy timeit()với number=1000độ dài listtừ 4**0đến 4**10và kết quả là

đo thời gian


1
Câu trả lời tại sao dòng màu đỏ ở trên màu xanh lam là, khi hàm listtạo có thể xác định kích thước của danh sách mới từ đối số của nó, nó sẽ vẫn phân bổ trước cùng một lượng không gian như nếu phần tử cuối cùng đến đó và không có đủ không gian cho nó. Ít nhất đó là điều có ý nghĩa đối với tôi.
tavo

@tavo nó có vẻ giống với tôi, sau một lúc tôi muốn hiển thị nó trong biểu đồ.
vishes_shell

2
Vì vậy, trong khi việc hiểu danh sách sử dụng ít bộ nhớ hơn, chúng có thể chậm hơn đáng kể do tất cả các thay đổi kích thước xảy ra. Chúng thường phải sao chép danh sách xương sống vào một vùng bộ nhớ mới.
Barmar

@Barmar thực sự tôi có thể chạy một số phép đo thời gian với rangeđối tượng (điều đó có thể thú vị).
vishes_shell

Và nó sẽ làm cho đồ thị của bạn đẹp hơn nữa. :)
Barmar
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.