Giải phóng bộ nhớ trong Python


128

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.

  1. Nếu tôi chạy trong trình thông dịch,

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

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

    del foo

    bộ nhớ thực đi xuống, nhưng chỉ để 30.4mb. Trình thông dịch sử dụng 4.4mbđường cơ sở, vậy lợi thế của việc không giải phóng 26mbbộ nhớ cho HĐH 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 lại nhiều bộ nhớ đó không?

  2. Tại sao nó phát hành 50.5mbcụ thể - số tiền được phát hành dựa trên là gì?

  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 lại nhiều bộ nhớ đó)?

LƯU Ý Câu hỏi này khác với Làm thế nào tôi có thể giải phóng bộ nhớ trong Python? bởi vì câu hỏi này chủ yếu liên quan đến việc tăng mức sử dụng bộ nhớ từ đường cơ sở ngay cả sau khi trình thông dịch đã giải phóng các đối tượng thông qua bộ sưu tập rác (có sử dụng gc.collecthay không).


4
Điều đáng chú ý là hành vi này không dành riêng cho Python. Thông thường, khi một quá trình giải phóng một số bộ nhớ được phân bổ heap, bộ nhớ sẽ không được giải phóng trở lại hệ điều hành cho đến khi quá trình này chết.
NPE

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

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


@jww Tôi không nghĩ vậy. Câu hỏi này thực sự liên quan đến lý do tại sao quá trình phiên dịch không bao giờ giải phóng bộ nhớ ngay cả sau khi thu thập đầy đủ rác với các cuộc gọi đến gc.collect.
Jared

Câu trả lời:


86

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

Ngoài ra, các loại tích hợp duy trì tự do của các đối tượng được phân bổ trước đó có thể hoặc không thể sử dụng phân bổ đối tượng nhỏ. Các intloại duy trì một freelist với bộ nhớ phân bổ riêng của mình, và cách xóa nó đòi hỏi gọi PyInt_ClearFreeList(). Điều này có thể được gọi một cách gián tiếp bằng cách làm đầy đủ gc.collect.

Hãy thử nó 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.Process.memory_info .

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)

Đầu ra:

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

Biên tập:

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

Thời gian chạy C (ví dụ: glibc, msvcrt) thu nhỏ heap khi không gian trống liền kề ở trên cùng đạ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ều này, không có gì đáng ngạc nhiên nếu đống lớn co lại nhiều hơn - thậm chí nhiều hơn - so với khối mà bạn free.

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


Sử dụng memory_info()thay vì get_memory_info()xđược xác định
Aziz Alto

Bạn nhận được 10 ^ 7 intgiây ngay cả trong Python 3, nhưng mỗi cái thay thế biến cuối cùng trong biến vòng lặp để chúng không tồn tại cùng một lúc.
Davis Herring

Tôi đã gặp một vấn đề rò rỉ bộ nhớ, và tôi đoán lý do nó đã trả lời ở đây. Nhưng làm thế nào tôi có thể chứng minh dự đoán của tôi? Có công cụ nào có thể hiển thị nhiều pool được malloced, nhưng chỉ một khối nhỏ được sử dụng?
ruiruige1991

130

Tôi đoán câu hỏi 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ả bộ nhớ đã được sử dụng (nếu bạn biết bạn sẽ không sử dụng lại nhiều bộ nhớ đó)?

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

Nếu bạn cần 500 MB dung lượng lưu trữ 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 bộ nhớ đó nữa, hãy tạo ra một quy trình con để thực hiện công việc đòi hỏi nhiề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 tầm thường 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á.

Đầu tiên, cách dễ nhất để tạo quy trình con là với concurrent.futures(hoặc, cho 3.1 trở về trước, futuresbackport 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 kiểm soát nhiều hơn một chút, hãy sử dụng multiprocessingmô-đun.

Các chi phí là:

  • Quá trình khởi động là loại chậm trên một số nền tảng, đặc biệt là Windows. Chúng ta đang nói chuyện một phần nghìn giây ở đây, không phải vài phút và nếu bạn quay cuồng một đứa trẻ để thực hiện công việc trị giá 300 giây, bạn thậm chí sẽ không nhận ra điều đó. Nhưng nó không miễn phí.
  • Nếu số lượng lớn bộ nhớ tạm thời bạn sử dụng thực sự lớn , thì việc này có thể khiến chương trình chính của bạn bị tráo đổi. Tất nhiên là bạn đang tiết kiệm thời gian trong thời gian dài, bởi vì nếu bộ nhớ đó cứ quẩn quanh mãi thì nó sẽ phải dẫn đến việc hoán đổi vào một lúc nào đó. Nhưng điều này có thể biến sự chậm chạp dần dần thành sự chậm trễ rất đáng chú ý (và sớm) trong một số trường hợp sử dụng.
  • Gửi một lượng lớn dữ liệu giữa các quy trình có thể bị 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 điều đó, nhưng nếu bạn đang gửi và nhận một 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, mmapped hoặc cách khác; API bộ nhớ dùng chung multiprocessing; v.v.).
  • Gửi một 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 lọc (hoặc, nếu bạn dán chúng vào một tệp hoặc bộ nhớ dùng chung, có thể structhoặc có thể lý tưởng ctypes).

Thủ thuật thực sự hay, mặc dù không giải quyết được vấn đề :( Nhưng tôi thực sự thích nó
ddofborg

32

eryksun đã trả lời câu hỏi số 1 và tôi đã trả lời câu hỏi số 3 (số 4 ban đầu), nhưng bây giờ hãy trả lời câu hỏi số 2:

Tại sao nó phát hành cụ thể 50,5mb - số tiền được phát hành dựa trên là bao nhiêu?

Cuối cùng, cái mà nó dựa vào là một chuỗi các sự trùng hợp ngẫu nhiên bên trong Python và mallocđiều đó rất khó dự đoán.

Đầu tiên, tùy thuộc vào cách bạn đo 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 cứ khi nào một trang bị hoán đổi bởi máy nhắn tin, bộ nhớ sẽ hiển thị dưới dạng "giải phóng", mặc dù nó không được giải phóng.

Hoặc bạn có thể đang đo các trang đang sử dụng, có thể hoặc không thể tính các trang được phân bổ nhưng không bao giờ chạm vào (trên các hệ thống phân bổ quá mức tối ưu, 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à một điều rất hữu ích để làm, nhưng dường như đó là những gì bạn đang hỏi về) và các trang đã thực sự bị xử lý, hai trường hợp có thể xảy ra: Hoặc là bạn ' đã sử dụng brkhoặc tương đương để thu nhỏ phân đoạn dữ liệu (rất hiếm hiện nay) hoặc bạn đã sử dụng munmaphoặc tương tự để phát hành phân đoạn được ánh xạ. (Về lý thuyết cũng có một biến thể nhỏ cho biến thể thứ hai, trong đó có nhiều cách để phát hành một phần của phân đoạn được ánh xạ, ví dụ, đánh cắp nó với MAP_FIXEDmột MADV_FREEphân đoạn mà bạn lập tức hủy bỏ.)

Nhưng hầu hết các chương trình không phân bổ trực tiếp mọi thứ ra khỏi các trang bộ nhớ; họ sử dụng một malloccấp phát kiểu. Khi bạn gọi free, bộ cấp phát chỉ có thể phát hành các trang cho HĐH nếu bạn tình cờ nhận được freeđối tượng trực tiếp cuối cùng trong ánh xạ (hoặc trong N trang cuối của phân đ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 điều này thậm chí còn phức tạp hơn nữa, nó có bộ cấp phát đối tượng 2 cấp tùy chỉnh ở trên bộ cấp phát bộ nhớ tùy chỉnh ở trên malloc. (Xem các nhận xét nguồn để được giải thích chi tiết hơn.) Và trên hết, ngay cả ở cấp độ API C, ít Python hơn, 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 bị hủy.

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

Nếu bạn thực hiện phân bổ một đối tượng, để biết liệu điều này có làm giảm cấp độ tiếp theo để phân bổ 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 cấp phát đối tượng, cũng như cách thức triển khai. (Rõ ràng điều đó không thể xảy ra trừ khi bạn giải quyết điều cuối cùng trong khối và thậm chí sau đó, điều đó có thể không xảy ra.)

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

Nếu bạn thực hiện free một mallocvùng ed, để biết liệu điều này gây ra một munmaphoặc tương đương (hoặc brk), bạn phải biết trạng thái bên trong của nó malloc, cũng như cách nó được thực hiện. Và cái này, không giống như những cái khác, rất đặc trưng cho nền tảng. (Và một lần nữa, bạn thường phải giải quyết việc sử dụng cuối cùng malloctrong một mmapphân khúc, và thậm chí sau đó, điều đó có thể không xảy ra.)

Vì vậy, nếu bạn muốn hiểu lý do tại sao nó lại 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 mallochủy bỏ các trang có giá trị 50,5mb khi bạn thực hiện một hoặc nhiều freecuộc gọi đó (có thể hơn một chút so với 50,5mb)? Bạn sẽ phải đọc nền tảng của bạn malloc, 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ó. (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, rất khó có thể chụp mà không chụp ảnh hệ thống để kiểm tra ngoại tuyến, nhưng may mắn thay, đây thường 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 giới hạn tài nguyên (ví dụ: được nhúng), bạn không có lý do gì để quan tâm đến những chi tiết này.

Và nếu bạn đang thực hiện phát triển giới hạn tài nguyên, biết những chi tiết này là vô ích; bạn phải thực hiện một bước cuối cùng xung quanh tất cả các cấp độ đó và cụ thể mmaplà bộ nhớ bạn cần ở cấp ứng dụng (có thể với một cấp phát vùng cụ thể, đơn giản, được hiểu rõ về ứng dụng ở giữa).


2

Đầu tiên, bạn có thể muốn cài đặt các ánh nhìn:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Sau đó chạy nó trong thiết bị đầu cuối!

glances

Trong mã Python của bạn, hãy thêm vào đầu tệp, như sau:

import os
import gc # Garbage Collector

Sau khi sử dụng biến "Lớn" (ví dụ: myBigVar), bạn muốn giải phóng bộ nhớ, viết mã python của bạn như sau:

del myBigVar
gc.collect()

Trong một thiết bị đầu cuối khác, hãy chạy mã python của bạn và quan sát trong thiết bị đầu cuối "liếc", cách bộ nhớ được quản lý trong hệ thống của bạn!

Chúc may mắn!

PS Tôi giả sử bạn đang làm việc trên hệ thống Debian hoặc Ubuntu

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.