Không giống như các ngôn ngữ như C, phần lớn thời gian Python sẽ giải phóng bộ nhớ cho bạn. Nhưng đôi khi, nó sẽ không hoạt động theo cách bạn mong đợi
Hãy xem xét chương trình Python sau—bạn nghĩ nó sẽ sử dụng tối đa bao nhiêu bộ nhớ?
import numpy as np
def load_1GB_of_data[]:
return np.ones[[2 ** 30], dtype=np.uint8]
def process_data[]:
data = load_1GB_of_data[]
return modify2[modify1[data]]
def modify1[data]:
return data * 2
def modify2[data]:
return data + 10
process_data[]
Giả sử chúng tôi không thể thay đổi dữ liệu gốc, điều tốt nhất chúng tôi có thể làm là bộ nhớ tối đa 2GB. trong một khoảng thời gian ngắn, cả 1GB dữ liệu gốc và bản sao dữ liệu đã sửa đổi sẽ cần phải có mặt. Trên thực tế, mức sử dụng cao nhất thực tế sẽ là 3GB—ở dưới xuống, bạn sẽ thấy kết quả lập hồ sơ bộ nhớ thực tế chứng minh rằng
Điều tốt nhất chúng tôi có thể làm là 2GB, mức sử dụng thực tế là 3GB. 1GB sử dụng bộ nhớ bổ sung đó đến từ đâu?
Để hiểu tại sao và những gì bạn có thể làm để khắc phục nó, bài viết này sẽ đề cập đến
- Tổng quan nhanh về cách Python tự động quản lý bộ nhớ cho bạn
- Các chức năng tác động đến việc theo dõi bộ nhớ của Python như thế nào
- Bạn có thể làm gì để khắc phục sự cố này
Cách quản lý bộ nhớ tự động của Python giúp cuộc sống của bạn dễ dàng hơn
Trong một số ngôn ngữ lập trình, bạn cần giải phóng rõ ràng bất kỳ bộ nhớ nào bạn đã phân bổ. Ví dụ, một chương trình C có thể làm
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
Nếu bạn không
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
6 thủ công bộ nhớ được cấp phát bởi uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
0, nó sẽ không bao giờ được giải phóngNgược lại, Python theo dõi các đối tượng và tự động giải phóng bộ nhớ của chúng khi chúng không còn được sử dụng nữa. Nhưng đôi khi điều đó không thành công và để hiểu tại sao bạn cần hiểu cách nó theo dõi chúng
Đối với phép tính gần đúng đầu tiên, việc triển khai Python mặc định thực hiện điều này bằng cách sử dụng phép đếm tham chiếu
- Mỗi đối tượng có một bộ đếm số lượng địa điểm mà nó đang được sử dụng
- Khi một địa điểm/đối tượng mới nhận được tham chiếu đến đối tượng, bộ đếm sẽ tăng thêm 1
- Khi một tham chiếu biến mất, bộ đếm sẽ giảm đi 1
- Khi bộ đếm chạm 0, bộ nhớ của đối tượng được giải phóng, vì không ai đề cập đến nó
Có một số cơ chế bổ sung [“thu gom rác”] để xử lý các tham chiếu vòng tròn, nhưng những cơ chế này không liên quan đến chủ đề hiện tại
Cách các chức năng tương tác với quản lý bộ nhớ Python
Một cách bạn có thể thêm một tham chiếu đến một đối tượng là thêm nó vào một đối tượng khác. một danh sách, một từ điển, một thuộc tính của một thể hiện của lớp, v.v. Nhưng các tham chiếu cũng được tạo bởi các biến cục bộ trong các hàm
Hãy xem một ví dụ
def f[]:
obj = {"x": 1}
g[obj]
return
def g[o]:
print[o]
return
Giả sử chúng ta gọi
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1 và xem qua mã từng bướcf[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
Ở dạng văn xuôi
- Chúng tôi làm
2, có nghĩa là có một biến cục bộuint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
3 trỏ đến từ điển mà chúng tôi đã tạo. Biến đó, được tạo bằng cách chạy hàm, tăng bộ đếm tham chiếu của đối tượnguint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
- Tiếp theo, chúng tôi chuyển đối tượng đó cho
4. Hiện tại có một biến cục bộ có tên làuint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
5 là một tham chiếu bổ sung cho cùng một từ điển, vì vậy tổng số tham chiếu là 2uint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
- Tiếp theo, chúng tôi in
5, có thể thêm hoặc không thêm tham chiếu, nhưng khiuint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
7 trả về, chúng tôi không có tham chiếu bổ sung nào và chúng tôi vẫn ở mức 2uint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
8 trả về, có nghĩa là biếnuint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
5 cục bộ biến mất, giảm số lượng tham chiếu xuống 1uint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
- Cuối cùng,
1 trả về, biến cục bộuint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
3 biến mất, giảm số lượng tham chiếu trở lại 0uint8_t *arr = malloc[1024 * 1024]; // .. do work with array ... free[arr];
- Số tham chiếu bây giờ là 0 và từ điển có thể được giải phóng. Điều này cũng làm giảm số lượng tham chiếu cho chuỗi
2 và số nguyêndef f[]: obj = {"x": 1} g[obj] return def g[o]: print[o] return
3 mà chúng tôi đã tạo, điều chỉnh một số tối ưu hóa dành riêng cho chuỗi và số nguyên mà tôi sẽ không đi sâu vàodef f[]: obj = {"x": 1} g[obj] return def g[o]: print[o] return
Bây giờ hãy xem lại mã đó, ở cấp độ ngữ nghĩa. Sau khi từ điển được chuyển cho
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
8, nó sẽ không bao giờ được sử dụng bởi uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1 nữa—tuy nhiên, vẫn có một tham chiếu từ uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1 do biến uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
3, đó là lý do tại sao số lượng tham chiếu là 2. Tham chiếu của biến cục bộ sẽ không bao giờ biến mất cho đến khi uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1 thoát, mặc dù uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1 đã sử dụng xongGiờ đây, việc lưu giữ một từ điển nhỏ trong bộ nhớ lâu hơn một chút không thực sự là vấn đề. Nhưng nếu đối tượng đó sử dụng nhiều bộ nhớ thì sao?
Thêm 1GB
Hãy quay lại mã ban đầu của chúng tôi, nơi chúng tôi có thêm 1GB bộ nhớ sử dụng ngoài dự kiến. Tóm lại
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
4Nếu chúng tôi cấu hình nó bằng trình cấu hình bộ nhớ Fil để nhận phân bổ tại thời điểm sử dụng bộ nhớ cao nhất, đây là những gì chúng tôi sẽ nhận được
Vào lúc cao điểm, chúng tôi sử dụng 3GB do ba lần phân bổ;
- Mảng ban đầu được tạo bởi
1f[]: obj = {"x": 1} # `obj` increments counter to 1 g[o=obj]: # `o` reference increments counter to 2 print[o] return # `o` goes away, decrements counter to 1 return # `obj` goes away, decrements counter 0 # Dictionary is freed from memory
- Mảng được sửa đổi đầu tiên, được tạo bởi
2;f[]: obj = {"x": 1} # `obj` increments counter to 1 g[o=obj]: # `o` reference increments counter to 2 print[o] return # `o` goes away, decrements counter to 1 return # `obj` goes away, decrements counter 0 # Dictionary is freed from memory
- Mảng sửa đổi thứ hai, được tạo bởi
0f[]: obj = {"x": 1} # `obj` increments counter to 1 g[o=obj]: # `o` reference increments counter to 2 print[o] return # `o` goes away, decrements counter to 1 return # `obj` goes away, decrements counter 0 # Dictionary is freed from memory
Vấn đề là phân bổ đầu tiên. chúng tôi không cần nó nữa khi
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
2 đã tạo phiên bản sửa đổi. Nhưng vì biến cục bộ f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
6 trong f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
7, nó không được giải phóng khỏi bộ nhớ cho đến khi f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
7 trả về. Và điều đó có nghĩa là mức sử dụng bộ nhớ cao hơn 1GB so với mức bình thườngCác giải pháp. Làm cho các chức năng buông bỏ
Vấn đề của chúng tôi là
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
7 đang giữ mảng ban đầu quá lâudef f[]:
obj = {"x": 1}
g[obj]
return
def g[o]:
print[o]
return
5Do đó, các giải pháp liên quan đến việc đảm bảo rằng biến cục bộ
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
6 không giữ mảng ban đầu lâu hơn mức cần thiếtGiải pháp số 1. Không có biến cục bộ nào cả
Nếu không có tham chiếu bổ sung, mảng ban đầu có thể bị xóa khỏi bộ nhớ ngay khi nó không được sử dụng
def f[]:
obj = {"x": 1}
g[obj]
return
def g[o]:
print[o]
return
7Hiện tại, không có tham chiếu
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
6 nào giữ nguyên 1GB dữ liệu ban đầu và mức sử dụng bộ nhớ cao nhất sẽ là 2GBGiải pháp số 2. Sử dụng lại biến cục bộ
Chúng ta có thể thay thế rõ ràng
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
6 bằng kết quả của f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
2uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
1Một lần nữa, chúng tôi kết thúc với bộ nhớ tối đa 2GB, vì mảng ban đầu có thể được giải phóng ngay sau khi
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
2 kết thúcGiải pháp số 3. Chuyển quyền sở hữu đối tượng
Đây là một thủ thuật vay mượn từ C++. chúng tôi có một đối tượng có nhiệm vụ sở hữu khối dữ liệu lớn 1GB và chúng tôi chuyển chủ sở hữu thay vì đối tượng ban đầu
uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
3Bí quyết là
f[]:
obj = {"x": 1} # `obj` increments counter to 1
g[o=obj]:
# `o` reference increments counter to 2
print[o]
return # `o` goes away, decrements counter to 1
return # `obj` goes away, decrements counter 0
# Dictionary is freed from memory
7 không còn tham chiếu đến khối dữ liệu lớn nữa mà thay vào đó là chủ sở hữu—và sau đó uint8_t *arr = malloc[1024 * 1024];
// .. do work with array ...
free[arr];
46 sẽ xóa/đặt lại chủ sở hữu sau khi trích xuất dữ liệu cần thiếtTheo dõi tham chiếu đối tượng
Trong mã bình thường, việc các đối tượng tồn tại lâu hơn một chút không thành vấn đề. Nhưng khi một đối tượng sử dụng nhiều gigabyte RAM, việc tồn tại quá lâu có thể khiến chương trình của bạn hết bộ nhớ hoặc yêu cầu trả tiền để mua thêm phần cứng
Vì vậy, hãy tập thói quen theo dõi trong đầu xem các tham chiếu đến các đối tượng nằm ở đâu. Và nếu mức sử dụng bộ nhớ quá cao và trình lược tả gợi ý các tham chiếu cấp chức năng là vấn đề, hãy thử một trong các kỹ thuật trên
Tìm hiểu thêm các kỹ thuật để giảm mức sử dụng bộ nhớ—đọc phần còn lại của hướng dẫn Bộ dữ liệu lớn hơn bộ nhớ dành cho Python
Bài viết tiếp theo. Chi phí bộ nhớ lớn. Số trong Python và cách NumPy hỗ trợ
Bài viết trước. Sao chép dữ liệu là lãng phí, thay đổi dữ liệu là nguy hiểm
Xử lý dữ liệu quá chậm?
Bạn có thể nhận được kết quả nhanh hơn từ quy trình khoa học dữ liệu của mình—và cũng nhận lại được một số tiền—nếu bạn có thể tìm ra lý do tại sao mã của mình chạy chậm
Xác định các nút thắt cổ chai hiệu suất và ngốn bộ nhớ trong khoa học dữ liệu sản xuất của bạn Các công việc Python với Sciagraph, trình lược tả luôn bật cho các công việc sản xuất hàng loạt
Tìm hiểu các kỹ năng kỹ thuật phần mềm Python thực tế mà bạn có thể sử dụng trong công việc của mình
Đăng ký nhận bản tin của tôi và tham gia cùng hơn 6500 nhà phát triển Python và nhà khoa học dữ liệu học các công cụ và kỹ thuật thực tế, từ hiệu suất Python đến đóng gói Docker, với một bài viết mới miễn phí trong hộp thư đến của bạn mỗi tuần