2013-03-16 18 views
94

Tôi có một vài câu hỏi liên quan đến việc sử dụng bộ nhớ trong ví dụ sau.Giải phóng bộ nhớ trong Python

  1. Nếu tôi chạy trong người phiên dịch,

    foo = ['bar' for _ in xrange(10000000)] 
    

    bộ nhớ thực sử dụng trên máy tính của tôi đi lên đến 80.9mb. Sau đó,

    del foo 
    

    bộ nhớ thực bị hỏng, nhưng chỉ đến 30.4mb. Phiên dịch viên sử dụng đường cơ sở 4.4mb vì vậy lợi thế khi không phát hành 26mb bộ nhớ cho hệ điều hành là gì? Có phải vì Python đang "lên kế hoạch trước", nghĩ rằng bạn có thể sử dụng nhiều bộ nhớ đó một lần nữa?

  2. Tại sao bản phát hành độc quyền riêng biệt là 50.5mb - số tiền được phát hành dựa trên số tiền nào?

  3. Có cách nào để buộc Python giải phóng tất cả bộ nhớ đã được sử dụng (nếu bạn biết bạn sẽ không sử dụng nhiều bộ nhớ đó nữa)?

+3

Cần lưu ý rằng hành vi này không cụ thể đối với Python. Nó thường là trường hợp đó, khi một quá trình giải phóng một số bộ nhớ phân bổ heap, bộ nhớ không được phát hành trở lại hệ điều hành cho đến khi quá trình chết. – NPE

+0

Câu hỏi của bạn yêu cầu nhiều thứ - một số trong số đó là những con dups, một số trong số đó không phù hợp với SO, một số trong số đó có thể là câu hỏi hay. Bạn đang hỏi liệu Python có phát hành bộ nhớ hay không, dưới những hoàn cảnh chính xác mà nó có thể/không thể, cơ chế cơ bản là gì, tại sao nó được thiết kế theo cách đó, cho dù có cách giải quyết nào hay cái gì khác hoàn toàn? – abarnert

+2

@abarnert Tôi kết hợp các truy vấn con tương tự. Để trả lời các câu hỏi của bạn: Tôi biết Python phát hành một số bộ nhớ cho hệ điều hành nhưng tại sao không phải tất cả của nó và tại sao số tiền mà nó làm. Nếu có những hoàn cảnh không thể, tại sao? Cách giải quyết khác là gì. – Jared

Trả lời

70

Bộ nhớ được phân bổ trên heap có thể bị đánh dấu nước cao. Điều này là phức tạp bởi tối ưu hóa nội bộ của Python để phân bổ các đối tượng nhỏ (PyObject_Malloc) trong 4 nhóm KiB, được phân loại cho kích thước phân bổ ở bội số của 8 byte - lên đến 256 byte (512 byte trong 3,3). Bản thân các hồ bơi nằm trong 256 đấu trường KiB, vì vậy nếu chỉ một khối trong một hồ bơi được sử dụng, toàn bộ 256 đấu trường KiB sẽ không được phát hành. Trong Python 3.3, trình phân bổ đối tượng nhỏ được chuyển sang sử dụng bản đồ bộ nhớ ẩn danh thay cho heap, vì vậy nó sẽ hoạt động tốt hơn khi giải phóng bộ nhớ.

Ngoài ra, các kiểu được cài đặt sẵn sẽ duy trì các nhà tự do của các đối tượng được phân bổ trước đây có thể hoặc không thể sử dụng trình phân bổ đối tượng nhỏ. Loại int duy trì một người tự do với bộ nhớ được cấp phát riêng của nó và thanh toán bù trừ yêu cầu gọi số PyInt_ClearFreeList(). Điều này có thể được gọi gián tiếp bằng cách thực hiện đầy đủ gc.collect.

Hãy thử như thế này và cho tôi biết những gì bạn nhận được. Đây là liên kết cho psutil.

import os 
import gc 
import psutil 

proc = psutil.Process(os.getpid()) 
gc.collect() 
mem0 = proc.get_memory_info().rss 

# create approx. 10**7 int objects and pointers 
foo = ['abc' for x in range(10**7)] 
mem1 = proc.get_memory_info().rss 

# unreference, including x == 9999999 
del foo, x 
mem2 = proc.get_memory_info().rss 

# collect() calls PyInt_ClearFreeList() 
# or use ctypes: pythonapi.PyInt_ClearFreeList() 
gc.collect() 
mem3 = proc.get_memory_info().rss 

pd = lambda x2, x1: 100.0 * (x2 - x1)/mem0 
print "Allocation: %0.2f%%" % pd(mem1, mem0) 
print "Unreference: %0.2f%%" % pd(mem2, mem1) 
print "Collect: %0.2f%%" % pd(mem3, mem2) 
print "Overall: %0.2f%%" % pd(mem3, mem0) 

Output:

Allocation: 3034.36% 
Unreference: -752.39% 
Collect: -2279.74% 
Overall: 2.23% 

Edit:

Tôi chuyển sang đo liên quan đến quá trình kích thước VM để loại bỏ những ảnh hưởng của quá trình khác trong hệ thống.

Thời gian chạy C (ví dụ: glibc, msvcrt) co lại vùng heap khi không gian trống liền kề ở trên đạt đến ngưỡng không đổi, động hoặc có thể định cấu hình. Với glibc bạn có thể điều chỉnh điều này với mallopt (M_TRIM_THRESHOLD). Với điều này, nó không phải là đáng ngạc nhiên nếu heap co lại bởi nhiều hơn - thậm chí nhiều hơn rất nhiều - hơn khối mà bạn free.

Trong 3.x range không tạo danh sách, do đó, kiểm tra ở trên sẽ không tạo ra các đối tượng 10 triệu int. Ngay cả khi nó đã làm, các loại int trong 3.x về cơ bản là một 2.x long, mà không thực hiện một freelist.

90

Tôi đoán các câu hỏi mà bạn thực sự quan tâm ở đây là:

Có cách nào để buộc Python để giải phóng tất cả các bộ nhớ đã được sử dụng (nếu bạn biết bạn sẽ không được sử dụng nhiều bộ nhớ một lần nữa)?

Không, không có. Nhưng có một cách giải quyết dễ dàng: quy trình con.

Nếu bạn cần 500MB bộ nhớ tạm thời trong 5 phút, nhưng sau đó bạn cần chạy thêm 2 giờ nữa và sẽ không chạm vào nhiều bộ nhớ đó nữa, hãy sinh ra quy trình con để thực hiện công việc chuyên sâu bộ nhớ. Khi quá trình con biến mất, bộ nhớ sẽ được giải phóng.

Điều này không hoàn toàn nhỏ và miễn phí, nhưng nó khá dễ dàng và rẻ tiền, thường đủ tốt để giao dịch trở nên đáng giá.

Thứ nhất, cách dễ nhất để tạo ra một quá trình con là với concurrent.futures (hoặc, đối với 3.1 trở về trước, các futures backport trên PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: 
    result = executor.submit(func, *args, **kwargs).result() 

Nếu bạn cần một chút kiểm soát nhiều hơn, sử dụng multiprocessing mô-đun.

Các chi phí bao gồm:

  • Process khởi động là loại chậm trên một số nền tảng, đặc biệt là Windows. Chúng tôi đang nói mili giây ở đây, không phải vài phút, và nếu bạn đang quay lên một đứa trẻ để làm công việc 300 giây ', bạn thậm chí sẽ không nhận thấy nó. Nhưng nó không miễn phí.
  • Nếu số lượng bộ nhớ tạm thời bạn sử dụng thực sự là lớn, việc này có thể khiến chương trình chính của bạn bị hoán đổi. Tất nhiên bạn đang tiết kiệm thời gian trong thời gian dài, bởi vì nếu bộ nhớ đó treo xung quanh mãi mãi nó sẽ phải dẫn đến trao đổi tại một số điểm. Nhưng điều này có thể làm chậm sự chậm chạp vào sự chậm trễ tất cả cùng một lúc (và sớm) đáng chú ý trong một số trường hợp sử dụng.
  • Gửi lượng lớn dữ liệu giữa các quy trình có thể chậm. Một lần nữa, nếu bạn đang nói về việc gửi hơn 2K đối số và nhận lại 64K kết quả, bạn thậm chí sẽ không nhận thấy nó, nhưng nếu bạn đang gửi và nhận lượng lớn dữ liệu, bạn sẽ muốn sử dụng một số cơ chế khác (một tệp, mmap bộ vi xử lý hoặc cách khác, các API bộ nhớ chia sẻ trong multiprocessing; v.v.).
  • Gửi số lượng lớn dữ liệu giữa các quy trình có nghĩa là dữ liệu phải được chọn (hoặc, nếu bạn dán chúng vào một tệp hoặc bộ nhớ dùng chung, struct hoặc có thể là ctypes-có thể).
+12

Bất kỳ ai bị bỏ phiếu, quan tâm giải thích tại sao? – abarnert

+0

lừa thực sự tốt đẹp, mặc dù không giải quyết được vấn đề :(Nhưng tôi thực sự thích nó – ddofborg

24

eryksun đã trả lời câu hỏi # 1, và tôi đã trả lời câu hỏi # 3 (bản gốC# 4), nhưng bây giờ chúng ta hãy trả lời câu hỏi # 2:

Tại sao nó phát hành 50.5mb đặc biệt - Số tiền được phát hành dựa trên số tiền gì?

Điều gì dựa trên, cuối cùng, toàn bộ chuỗi trùng hợp bên trong Python và malloc rất khó dự đoán.

Đầu tiên, tùy thuộc vào cách bạn đo lường bộ nhớ, bạn chỉ có thể đo các trang thực sự được ánh xạ vào bộ nhớ.Trong trường hợp đó, bất kỳ khi nào một trang được hoán đổi bởi máy nhắn tin, bộ nhớ sẽ hiển thị là "được giải phóng", mặc dù nó chưa được giải phóng. Hoặc bạn có thể đang đo lường các trang được sử dụng, có thể hoặc không thể đếm các trang được phân bổ nhưng không bao giờ chạm (trên các hệ thống phân bổ quá mức, như linux), các trang được phân bổ nhưng được gắn thẻ MADV_FREE, v.v.

Nếu bạn thực sự đang đo các trang được phân bổ (thực sự không phải là điều rất hữu ích để làm, nhưng dường như đó là điều bạn đang hỏi), và các trang đã thực sự được phân bổ, hai trường hợp trong đó xảy ra: Hoặc bạn đã sử dụng brk hoặc tương đương để thu hẹp phân đoạn dữ liệu (rất hiếm hiện nay) hoặc bạn đã sử dụng munmap hoặc tương tự để phát hành phân đoạn được ánh xạ. (Ngoài ra, về mặt lý thuyết có một biến thể nhỏ về sau, trong đó có nhiều cách để giải phóng một phần của một phân đoạn được ánh xạ — ví dụ, ăn cắp nó với MAP_FIXED cho phân đoạn MADV_FREE mà bạn ngay lập tức unmap.)

Nhưng hầu hết các chương trình không trực tiếp phân bổ mọi thứ ra khỏi các trang bộ nhớ; họ sử dụng một phân bổ theo kiểu malloc. Khi bạn gọi free, người cấp phát chỉ có thể phát hành các trang cho hệ điều hành nếu bạn chỉ xảy ra là free nhập đối tượng trực tiếp cuối cùng trong ánh xạ (hoặc trong các trang N cuối cùng của đoạn dữ liệu). Không có cách nào ứng dụng của bạn có thể dự đoán hợp lý điều này, hoặc thậm chí phát hiện ra rằng nó đã xảy ra trước.

CPython làm cho việc này trở nên phức tạp hơn — nó có bộ phân bổ đối tượng 2 cấp tùy chỉnh ở trên cùng của bộ cấp phát bộ nhớ tùy chỉnh ở trên cùng của malloc. (Xem the source comments để có giải thích chi tiết hơn.) Và trên hết, ngay cả ở cấp C API, ít hơn nhiều Python, bạn thậm chí không trực tiếp kiểm soát khi các đối tượng cấp cao nhất được deallocated.

Vì vậy, khi bạn phát hành một đối tượng, làm thế nào để bạn biết liệu nó sẽ phát hành bộ nhớ cho hệ điều hành? Trước tiên, bạn phải biết rằng bạn đã phát hành tham chiếu cuối cùng (bao gồm bất kỳ tham chiếu nội bộ nào mà bạn không biết), cho phép GC giải quyết nó. Điều này thường giải quyết ít nhất hai điều ở cấp độ tiếp theo xuống (ví dụ, đối với một chuỗi, bạn đang phát hành đối tượng PyString và chuỗi đó đệm).

Nếu bạn làm deallocate một đối tượng, để biết liệu điều này có làm giảm cấp độ tiếp theo để deallocate một khối lưu trữ đối tượng hay không, bạn phải biết trạng thái bên trong của phân bổ đối tượng. (Nó rõ ràng là không thể xảy ra trừ khi bạn đang deallocating điều cuối cùng trong khối, và thậm chí sau đó, nó có thể không xảy ra.)

Nếu bạn làm deallocate một khối lưu trữ đối tượng, để biết liệu điều này gây ra một cuộc gọi free, bạn phải biết trạng thái bên trong của trình phân bổ PyMem, cũng như cách nó được thực hiện. (Một lần nữa, bạn phải được deallocating cuối cùng khối trong sử dụng trong một khu vực malloc ed, và thậm chí sau đó, nó có thể không xảy ra.)

Nếu bạn làmfree một vùng malloc ed, để biết liệu nguyên nhân này an munmap hoặc tương đương (hoặc brk), bạn phải biết trạng thái nội bộ của malloc, cũng như cách được triển khai. Và cái này, không giống như những cái khác, là đặc trưng cho nền tảng cao. (Và một lần nữa, bạn thường phải được deallocating cuối cùng trong sử dụng malloc trong một phân đoạn mmap, và thậm chí sau đó, nó có thể không xảy ra.)

Vì vậy, nếu bạn muốn hiểu tại sao nó xảy ra để phát hành chính xác 50.5mb , bạn sẽ phải theo dõi nó từ dưới lên.Tại sao malloc giá trị unmap 50,5mb của trang khi bạn thực hiện một hoặc nhiều cuộc gọi free (có thể nhiều hơn một chút so với 50,5mb)? Bạn sẽ phải đọc malloc nền tảng của bạn, và sau đó đi bộ các bảng và danh sách khác nhau để xem trạng thái hiện tại của nền tảng. (Trên một số nền tảng, nó thậm chí có thể sử dụng thông tin cấp hệ thống, mà không thể chụp được mà không tạo ảnh chụp nhanh của hệ thống để kiểm tra ngoại tuyến, nhưng may mắn thay đây không phải là vấn đề.) Và sau đó bạn phải làm điều tương tự ở 3 cấp trên.

Vì vậy, câu trả lời hữu ích duy nhất cho câu hỏi là "Bởi vì".

Trừ khi bạn đang thực hiện phát triển tài nguyên giới hạn (ví dụ: được nhúng), bạn không có lý do gì để quan tâm đến các chi tiết này.

Và nếu bạn phát triển nguồn lực hạn chế, việc biết các chi tiết này là vô dụng; bạn khá nhiều phải làm một kết thúc xung quanh tất cả các cấp và đặc biệt là mmap bộ nhớ bạn cần ở cấp ứng dụng (có thể với một đơn giản, được hiểu rõ, ứng dụng cụ thể vùng phân bổ ở giữa).

+0

Tôi không biết tôi có thể đăng nhiều câu trả lời .... – laike9m