Python là một ngôn ngữ lập trình tuyệt vời. Nó cũng được biết đến là khá chậm, chủ yếu là do tính linh hoạt và tính năng động rất lớn của nó. Đối với nhiều ứng dụng và miền, đó không phải là vấn đề do yêu cầu của chúng và các kỹ thuật tối ưu hóa khác nhau. Người ta ít biết rằng các biểu đồ đối tượng Python [từ điển lồng nhau của danh sách và bộ dữ liệu và kiểu nguyên thủy] chiếm một lượng bộ nhớ đáng kể. Đây có thể là một yếu tố hạn chế nghiêm trọng hơn nhiều do ảnh hưởng của nó đối với bộ nhớ đệm, bộ nhớ ảo, cho thuê nhiều chương trình khác và nói chung là làm cạn kiệt bộ nhớ khả dụng, vốn là một nguồn tài nguyên khan hiếm và đắt đỏ
Hóa ra không khó để tìm ra lượng bộ nhớ thực sự được sử dụng. Trong bài viết này, tôi sẽ hướng dẫn bạn những điều phức tạp trong quản lý bộ nhớ của đối tượng Python và chỉ ra cách đo chính xác bộ nhớ đã sử dụng
Trong bài viết này, tôi chỉ tập trung vào CPython—việc triển khai chính của ngôn ngữ lập trình Python. Các thử nghiệm và kết luận ở đây không áp dụng cho các triển khai Python khác như IronPython, Jython và PyPy
Tùy thuộc vào phiên bản Python, các con số đôi khi hơi khác một chút [đặc biệt đối với các chuỗi luôn là Unicode], nhưng các khái niệm đều giống nhau. Trong trường hợp của tôi, đang sử dụng Python 3. 10
Kể từ ngày 1 tháng 1 năm 2020, Python 2 không còn được hỗ trợ và bạn nên nâng cấp lên Python 3
Thực hành khám phá cách sử dụng bộ nhớ Python
Trước tiên, hãy khám phá một chút và hiểu cụ thể về việc sử dụng bộ nhớ thực tế của các đối tượng Python
Chức năng tích hợp 3
86
Mô-đun sys của thư viện tiêu chuẩn cung cấp chức năng
387. Hàm đó chấp nhận một đối tượng [và mặc định tùy chọn], gọi phương thức
388 của đối tượng và trả về kết quả, vì vậy bạn cũng có thể kiểm tra đối tượng của mình
Đo bộ nhớ của các đối tượng Python
Hãy bắt đầu với một số loại số
1
import sys
2
3
sys.getsizeof[5]
4
28
Hấp dẫn. Một số nguyên mất 28 byte
1
31____5
import sys0
Hmm… một float mất 24 byte
1
import sys2____5______44
3
import sys6
Ồ. 104 byte. Điều này thực sự khiến bạn phải suy nghĩ xem bạn muốn biểu diễn một số lượng lớn các số thực dưới dạng
389 hay
390
Hãy chuyển sang chuỗi và bộ sưu tập
1
import sys8
2_______50
3
22
4_______54
25
26
27
28
29
30
31
32
33
34
35
36
VÂNG. Một chuỗi trống chiếm 49 byte và mỗi ký tự bổ sung sẽ thêm một byte khác. Điều đó nói lên rất nhiều điều về sự đánh đổi của việc giữ nhiều chuỗi ngắn trong đó bạn sẽ trả 49 byte chi phí cho mỗi chuỗi so với. một chuỗi dài duy nhất mà bạn chỉ trả chi phí chung một lần
Đối tượng
391 có tổng phí chỉ 33 byte.
1
38____5
sys.getsizeof[5]0
Hãy nhìn vào danh sách
1
sys.getsizeof[5]2
2
sys.getsizeof[5]4
3
sys.getsizeof[5]6
4_______78
25
40
27
42
29
44
31
46
33
48
35
28
028
128
228
328
4sys.getsizeof[5]8
Chuyện gì đang xảy ra vậy? . Một danh sách chứa một chuỗi dài chỉ chiếm 64 byte
Đáp án đơn giản. Danh sách không chứa các đối tượng
392. Nó chỉ chứa một con trỏ 8 byte [trên phiên bản 64-bit của CPython] tới đối tượng thực tế
392. Điều đó có nghĩa là hàm
396 không trả về bộ nhớ thực của danh sách và tất cả các đối tượng mà nó chứa, mà chỉ trả về bộ nhớ của danh sách và các con trỏ tới các đối tượng của nó. Trong phần tiếp theo, tôi sẽ giới thiệu hàm
397, giải quyết vấn đề này
1
28
72_______99
3
11
4
13
25
15
27
sys.getsizeof[5]4
29
19
31
sys.getsizeof[5]8
33
313
35
42
28
1317
28
213
Câu chuyện tương tự đối với các bộ dữ liệu. Chi phí hoạt động của một bộ dữ liệu trống là 40 byte so với. 56 của một danh sách. Một lần nữa, sự khác biệt 16 byte trên mỗi chuỗi này là kết quả thấp nếu bạn có cấu trúc dữ liệu với nhiều chuỗi nhỏ, không thay đổi
1
21
2_______53
3
25
4
23
25
29
27
23
29
31
import sys04
33
sys.getsizeof[5]8
35
import sys08
28
1import sys10
28
2import sys12
28
4import sys10
Các bộ và từ điển có vẻ như không phát triển chút nào khi bạn thêm các mục, nhưng lưu ý rằng chi phí rất lớn
Điểm mấu chốt là các đối tượng Python có chi phí cố định rất lớn. Nếu cấu trúc dữ liệu của bạn bao gồm một số lượng lớn các đối tượng bộ sưu tập như chuỗi, danh sách và từ điển chứa một số lượng nhỏ các mục, mỗi đối tượng thì bạn phải trả một khoản phí lớn
Hàm 3
97
Bây giờ tôi đã làm bạn sợ chết khiếp và cũng đã chứng minh rằng
386 chỉ có thể cho bạn biết một đối tượng nguyên thủy chiếm bao nhiêu bộ nhớ, hãy xem xét một giải pháp phù hợp hơn. Hàm
397 đi sâu vào đệ quy và tính toán mức sử dụng bộ nhớ thực tế của biểu đồ đối tượng Python
1
import sys16
2
import sys18
3
4
import sys21
25
import sys23
27
29
import sys26
31
import sys28
33
import sys30
35
28
1import sys33
28
2import sys35
28
4import sys37
import sys38
import sys39
import sys40
import sys41
import sys42
import sys43
import sys44
import sys45
import sys46
import sys47
import sys48
import sys49
import sys50
import sys51
import sys52
import sys53
import sys54
import sys55
import sys56
import sys57
import sys58
import sys59
import sys60
import sys61
import sys62
import sys63
import sys64
import sys65
import sys66
import sys67
import sys68
import sys69
import sys70
import sys71
import sys72
import sys73
import sys74
import sys75
Có một số khía cạnh thú vị cho chức năng này. Nó tính đến các đối tượng được tham chiếu nhiều lần và chỉ đếm chúng một lần bằng cách theo dõi các id đối tượng. Tính năng thú vị khác của việc triển khai là nó tận dụng tối đa các lớp cơ sở trừu tượng của mô-đun bộ sưu tập. Điều đó cho phép hàm xử lý rất chính xác bất kỳ bộ sưu tập nào thực hiện các lớp cơ sở Ánh xạ hoặc Bộ chứa thay vì xử lý trực tiếp với vô số loại bộ sưu tập như.
sys.getsizeof[5]01,
sys.getsizeof[5]02,
sys.getsizeof[5]03,
sys.getsizeof[5]04,
sys.getsizeof[5]05,
sys.getsizeof[5]06,
sys.getsizeof[5]07,
sys.getsizeof[5]08,
sys.getsizeof[5]09,
sys.getsizeof[5]10, v.v.
Hãy xem nó hoạt động
1
import sys77____5
import sys79
3
sys.getsizeof[5]4
Một chuỗi có độ dài 7 chiếm 56 byte [49 byte trên đầu + 7 byte cho mỗi ký tự]
1
import sys83____5
sys.getsizeof[5]4
Một danh sách trống chiếm 56 byte [chỉ tính trên đầu]
1____487____5
import sys89
Một danh sách chứa chuỗi "x" chiếm 124 byte [56 + 8 + 56]
1
import sys91____5
import sys93
Một danh sách chứa chuỗi "x" năm lần chiếm 156 byte [56 + 5\*8 + 56]
Ví dụ cuối cùng cho thấy rằng
397 đếm các tham chiếu đến cùng một đối tượng [chuỗi x] chỉ một lần, nhưng mỗi con trỏ của tham chiếu đều được tính
Điều trị hoặc Tricks
Hóa ra CPython có một số mánh khóe, vì vậy những con số bạn nhận được từ
397 không thể hiện đầy đủ mức sử dụng bộ nhớ của chương trình Python
Đếm tham chiếu
Python quản lý bộ nhớ bằng ngữ nghĩa đếm tham chiếu. Khi một đối tượng không còn được tham chiếu nữa, bộ nhớ của nó sẽ bị giải phóng. Nhưng miễn là có một tham chiếu, đối tượng sẽ không bị hủy bỏ. Những thứ như tài liệu tham khảo theo chu kỳ có thể cắn bạn khá khó khăn
Đối tượng nhỏ
CPython quản lý các đối tượng nhỏ [dưới 256 byte] trong nhóm đặc biệt trên ranh giới 8 byte. Có các nhóm cho 1-8 byte, 9-16 byte và tất cả các cách tới 249-256 byte. Khi một đối tượng có kích thước 10 được phân bổ, nó sẽ được phân bổ từ nhóm 16 byte cho các đối tượng có kích thước 9-16 byte. Vì vậy, mặc dù chỉ chứa 10 byte dữ liệu nhưng nó sẽ tốn 16 byte bộ nhớ. Nếu bạn phân bổ 1.000.000 đối tượng có kích thước 10, bạn thực sự sử dụng 16.000.000 byte chứ không phải 10.000.000 byte như bạn có thể giả định. Chi phí phụ thêm 60% này rõ ràng là không nhỏ
số nguyên
CPython giữ một danh sách toàn cầu của tất cả các số nguyên trong phạm vi -5 đến 256. Chiến lược tối ưu hóa này có ý nghĩa vì các số nguyên nhỏ xuất hiện ở khắp mọi nơi và với điều kiện là mỗi số nguyên chiếm 28 byte, nó tiết kiệm rất nhiều bộ nhớ cho một chương trình thông thường
Điều đó cũng có nghĩa là CPython phân bổ trước 266 * 28 = 7448 byte cho tất cả các số nguyên này, ngay cả khi bạn không sử dụng hầu hết chúng. Bạn có thể xác minh nó bằng cách sử dụng hàm
sys.getsizeof[5]13 cung cấp con trỏ tới đối tượng thực tế. Nếu bạn gọi
sys.getsizeof[5]14 cho bất kỳ
sys.getsizeof[5]15 nào trong phạm vi -5 đến 256, bạn sẽ nhận được cùng một kết quả mỗi lần [cho cùng một số nguyên]. Nhưng nếu bạn thử nó với các số nguyên nằm ngoài phạm vi này, mỗi số sẽ khác nhau [mỗi lần một đối tượng mới được tạo nhanh chóng]
Dưới đây là một vài ví dụ trong phạm vi
1
import sys95
2
import sys97
3
4
import sys95
25
202
27
29
import sys95
31
202
33
35
210
28
1212
28
228
4210
import sys38
212
import sys39
import sys41___510
import sys43
21
Dưới đây là một số ví dụ bên ngoài phạm vi
1
224
2_______526
3
4
229
25
231
27
29
229
31
231
33
35
239
28
1241
28
228
4239
import sys38
241
Bộ nhớ Python so với. Bộ nhớ hệ thống
CPython là loại sở hữu. Trong nhiều trường hợp, khi các đối tượng bộ nhớ trong chương trình của bạn không được tham chiếu nữa, chúng sẽ không được trả về hệ thống [e. g. những đồ vật nhỏ]. Điều này tốt cho chương trình của bạn nếu bạn phân bổ và giải phóng nhiều đối tượng thuộc cùng một nhóm 8 byte vì Python không phải làm phiền hệ thống, điều này tương đối tốn kém. Nhưng sẽ không tuyệt lắm nếu chương trình của bạn thường sử dụng X byte và trong một số điều kiện tạm thời, nó sử dụng gấp 100 lần [e. g. phân tích cú pháp và xử lý một tệp cấu hình lớn chỉ khi nó bắt đầu]
Giờ đây, bộ nhớ 100X đó có thể bị giữ lại một cách vô ích trong chương trình của bạn, không bao giờ được sử dụng lại và khiến hệ thống không thể phân bổ bộ nhớ đó cho các chương trình khác. Điều trớ trêu là nếu bạn sử dụng mô-đun xử lý để chạy nhiều phiên bản chương trình của mình, thì bạn sẽ bị giới hạn nghiêm trọng số lượng phiên bản bạn có thể chạy trên một máy cụ thể
Hồ sơ bộ nhớ
Để đánh giá và đo mức sử dụng bộ nhớ thực tế của chương trình, bạn có thể sử dụng mô-đun memory\_profiler. Tôi đã chơi với nó một chút và tôi không chắc mình tin tưởng vào kết quả. Sử dụng nó rất đơn giản. Bạn trang trí một chức năng [có thể là chức năng chính] bằng trình trang trí
sys.getsizeof[5]16 và khi chương trình thoát, trình cấu hình bộ nhớ in ra đầu ra tiêu chuẩn một báo cáo tiện dụng hiển thị tổng số và các thay đổi trong bộ nhớ cho mỗi dòng. Đây là một chương trình mẫu mà tôi đã chạy dưới profiler
1
248
2
3
251
4
253
25
255
27
257
29
259
31
261
33
263
35
261
28
1267
28
2261
28
4271
import sys38
273
import sys39
275
import sys41
277
import sys43
import sys45
280
import sys47
282
import sys49
284
import sys51
286
Đây là đầu ra
1
288
2
3
291
4
293
25
295
27
297
29
299
31
301
33
303
35
305
28
1307
28
2309
28
4311
import sys38
313
import sys39
315
import sys41
317
import sys43
319
import sys45
321
import sys47
323
import sys49
325
Như bạn có thể thấy, có 17. Chi phí bộ nhớ 3 MB. Lý do bộ nhớ không tăng khi thêm số nguyên cả bên trong và bên ngoài phạm vi [-5, 256] và cả khi thêm chuỗi là một đối tượng duy nhất được sử dụng trong mọi trường hợp. Không rõ tại sao vòng lặp đầu tiên của phạm vi [100000] trên dòng 9 lại thêm 0. 8 MB trong khi dòng thứ hai trên dòng 11 chỉ thêm 0. 7MB và vòng lặp thứ ba trên dòng 13 thêm 0. 8MB. Cuối cùng, khi xóa danh sách a, b và c, -0. 6 MB được phát hành cho a, -0. 8 MB được phát hành cho b và -0. 8MB được phát hành cho c
Cách theo dõi rò rỉ bộ nhớ trong ứng dụng Python của bạn với tracemalloc
tracemalloc là một mô-đun Python hoạt động như một công cụ gỡ lỗi để theo dõi các khối bộ nhớ được cấp phát bởi Python. Sau khi bật Tracemalloc, bạn có thể lấy thông tin sau
- xác định nơi đối tượng được phân bổ
- đưa ra số liệu thống kê về bộ nhớ được phân bổ
- phát hiện rò rỉ bộ nhớ bằng cách so sánh ảnh chụp nhanh
Hãy xem xét ví dụ dưới đây
1
327
2
3
330
4
25
333
27
335
29
337
31
339
33
341
35
339
28
1345
28
2339
28
4349
import sys38
351
import sys39
353
import sys41
355
import sys43
import sys45
import sys47
359
import sys49
361
import sys51
363
import sys53
365
import sys54
282
Giải trình
sys.getsizeof[5]
17—bắt đầu truy tìm ký ứcsys.getsizeof[5]
18—chụp nhanh bộ nhớ và trả về đối tượngsys.getsizeof[5]
19sys.getsizeof[5]
20—sắp xếp các bản ghi theo dõi và trả về số lượng cũng như kích thước của các đối tượng từ truy nguyên.sys.getsizeof[5]
21 chỉ ra rằng việc sắp xếp sẽ được thực hiện theo số dòng trong tệp
Khi bạn chạy mã, đầu ra sẽ là
1
369
2_______071
3
373
4
375
25
377
27
379
29
381
31
383
33
385
Phần kết luận
CPython sử dụng rất nhiều bộ nhớ cho các đối tượng của nó. Nó cũng sử dụng nhiều thủ thuật và tối ưu hóa để quản lý bộ nhớ. Bằng cách theo dõi mức sử dụng bộ nhớ của đối tượng và nhận thức được mô hình quản lý bộ nhớ, bạn có thể giảm đáng kể dung lượng bộ nhớ của chương trình
Bài đăng này đã được cập nhật với sự đóng góp từ Esther Vaati. Esther là nhà phát triển và viết phần mềm cho Envato Tuts+