Tìm hiểu lý do tại sao các ứng dụng Python của bạn đang sử dụng quá nhiều bộ nhớ và giảm mức sử dụng RAM của chúng bằng các thủ thuật đơn giản và cấu trúc dữ liệu hiệu quả này
Ảnh của The Bored Apeventurer BAYC trên Bapt
Khi nói đến tối ưu hóa hiệu suất, mọi người thường chỉ tập trung vào tốc độ và mức sử dụng CPU. Hiếm khi có ai quan tâm đến mức tiêu thụ bộ nhớ, cho đến khi họ hết RAM. Có nhiều lý do để cố gắng hạn chế sử dụng bộ nhớ, không chỉ tránh việc ứng dụng của bạn bị treo vì lỗi hết bộ nhớ
Trong bài viết này, chúng ta sẽ khám phá các kỹ thuật để tìm phần nào trong ứng dụng Python của bạn đang tiêu tốn quá nhiều bộ nhớ, phân tích lý do và cuối cùng là giảm mức tiêu thụ bộ nhớ và dấu chân bằng cách sử dụng các thủ thuật đơn giản và cấu trúc dữ liệu hiệu quả về bộ nhớ
Tại sao phiền, Dù sao?Nhưng trước tiên, tại sao bạn phải tiết kiệm RAM?
Một lý do đơn giản là tiền. Tài nguyên — cả CPU và RAM — đều tốn tiền, tại sao lại lãng phí bộ nhớ bằng cách chạy các ứng dụng kém hiệu quả, nếu có nhiều cách để giảm dung lượng bộ nhớ?
Một lý do khác là quan niệm “dữ liệu có khối lượng”, nếu có nhiều thì nó sẽ di chuyển chậm. Nếu dữ liệu phải được lưu trữ trên đĩa thay vì trong RAM hoặc bộ nhớ cache nhanh, thì sẽ mất một lúc để tải và xử lý, ảnh hưởng đến hiệu suất tổng thể. Do đó, tối ưu hóa cho việc sử dụng bộ nhớ có thể có tác dụng phụ tốt là tăng tốc thời gian chạy ứng dụng
Cuối cùng, trong một số trường hợp, hiệu suất có thể được cải thiện bằng cách bổ sung thêm bộ nhớ [nếu hiệu suất của ứng dụng bị giới hạn bởi bộ nhớ], nhưng bạn không thể làm điều đó nếu không còn bộ nhớ nào trên máy
Tìm nút cổ chaiRõ ràng là có những lý do chính đáng để giảm mức sử dụng bộ nhớ của các ứng dụng Python của chúng ta, tuy nhiên, trước khi làm điều đó, trước tiên chúng ta cần tìm ra các nút thắt cổ chai hoặc các phần mã đang ngốn hết bộ nhớ
Công cụ đầu tiên chúng tôi sẽ giới thiệu là memory_profiler
. Công cụ này đo mức sử dụng bộ nhớ của chức năng cụ thể trên cơ sở từng dòng
Để bắt đầu sử dụng, chúng tôi cài đặt nó với gói pip
cùng với gói psutil
giúp cải thiện đáng kể hiệu suất của trình hồ sơ. Ngoài ra, chúng tôi cũng cần đánh dấu chức năng mà chúng tôi muốn đánh dấu bằng trình trang trí @profile
. Cuối cùng, chúng tôi chạy trình lược tả đối với mã của chúng tôi bằng cách sử dụng python -m memory_profiler
. Điều này cho thấy việc sử dụng/phân bổ bộ nhớ trên cơ sở từng dòng cho chức năng được trang trí - trong trường hợp này là memory_intensive
- cố ý tạo và xóa các danh sách lớn
Bây giờ chúng ta đã biết cách thu hẹp tiêu điểm và tìm các dòng cụ thể làm tăng mức tiêu thụ bộ nhớ, chúng ta có thể muốn tìm hiểu sâu hơn một chút và xem mỗi biến đang sử dụng bao nhiêu. Bạn có thể đã thấy sys.getsizeof
được sử dụng để đo lường điều này trước đây. Tuy nhiên, chức năng này sẽ cung cấp cho bạn thông tin đáng ngờ đối với một số loại cấu trúc dữ liệu. Đối với số nguyên hoặc mảng phụ, bạn sẽ nhận được kích thước thực tính bằng byte, đối với các vùng chứa như danh sách, bạn sẽ chỉ nhận được kích thước của chính vùng chứa đó chứ không phải nội dung của nó
Chúng ta có thể thấy rằng với các số nguyên đơn giản, mỗi khi chúng ta vượt qua một ngưỡng, 4 byte sẽ được thêm vào kích thước. Tương tự, với các chuỗi đơn giản, mỗi khi chúng ta thêm một ký tự khác thì một byte bổ sung sẽ được thêm vào. Tuy nhiên, với các danh sách, điều này không theo kịp — sys.getsizeof
không "đi" cấu trúc dữ liệu và chỉ trả về kích thước của đối tượng gốc, trong trường hợp này là list
Cách tiếp cận tốt hơn là sử dụng công cụ cụ thể được thiết kế để phân tích hành vi bộ nhớ. Một trong số đó là công cụ có thể giúp bạn có ý tưởng thực tế hơn về kích thước đối tượng Python
Pympler cung cấp mô-đun asizeof
với chức năng cùng tên báo cáo chính xác kích thước của danh sách cũng như tất cả các giá trị mà nó chứa. Ngoài ra, mô-đun này cũng có chức năng pip
0, có thể cung cấp cho chúng tôi phân tích kích thước chi tiết hơn của các thành phần riêng lẻ của đối tượng
Mặc dù vậy, Pympler có nhiều tính năng hơn, bao gồm theo dõi các thể hiện của lớp hoặc xác định rò rỉ bộ nhớ. Trong trường hợp đây là những thứ có thể cần thiết cho ứng dụng của bạn, thì tôi khuyên bạn nên xem các hướng dẫn có sẵn trong tài liệu
Tiết kiệm RAMBây giờ chúng ta đã biết cách tìm kiếm tất cả các loại vấn đề tiềm ẩn về bộ nhớ, chúng ta cần tìm cách khắc phục chúng. Giải pháp tiềm năng, nhanh nhất và dễ dàng nhất có thể là chuyển sang các cấu trúc dữ liệu hiệu quả hơn về bộ nhớ
Python pip
1 là một trong những tùy chọn ngốn bộ nhớ hơn khi lưu trữ các mảng giá trị
Hàm đơn giản ở trên [ pip
2] tạo Python list
số bằng cách sử dụng pip
4 đã chỉ định. Để đo lượng bộ nhớ nó chiếm, chúng ta có thể sử dụng memory_profiler
được hiển thị trước đó, cung cấp cho chúng ta lượng bộ nhớ được sử dụng trong 0. Khoảng thời gian 2 giây trong khi thực hiện chức năng. Chúng ta có thể thấy rằng việc tạo list
trong số 10 triệu số cần hơn 350MiB bộ nhớ. Vâng, đó có vẻ là rất nhiều cho một loạt các con số. Chúng ta có thể làm gì tốt hơn không?
Trong ví dụ này, chúng tôi đã sử dụng mô-đun pip
7 của Python, mô-đun này có thể lưu trữ các giá trị nguyên thủy, chẳng hạn như số nguyên hoặc ký tự. Chúng ta có thể thấy rằng trong trường hợp này, mức sử dụng bộ nhớ đạt đỉnh chỉ hơn 100MiB. Đó là một sự khác biệt rất lớn so với list
. Bạn có thể giảm mức sử dụng bộ nhớ hơn nữa bằng cách chọn độ chính xác phù hợp
Một nhược điểm lớn của việc sử dụng pip
7 làm bộ chứa dữ liệu là nó không hỗ trợ nhiều loại
Nếu bạn định thực hiện nhiều phép toán trên dữ liệu, thì có lẽ bạn nên sử dụng mảng NumPy để thay thế
Chúng ta có thể thấy rằng các mảng NumPy cũng hoạt động khá tốt khi sử dụng bộ nhớ với kích thước mảng cao nhất là ~123MiB. Đó là nhiều hơn một chút so với pip
7 nhưng với NumPy, bạn có thể tận dụng các hàm toán học nhanh cũng như các loại không được pip
7 hỗ trợ, chẳng hạn như số phức
Các tối ưu hóa ở trên giúp với kích thước tổng thể của các mảng giá trị, nhưng chúng ta cũng có thể thực hiện một số cải tiến đối với kích thước của các đối tượng riêng lẻ được xác định bởi các lớp Python. Điều này có thể được thực hiện bằng cách sử dụng thuộc tính lớp psutil
2 được sử dụng để khai báo rõ ràng các thuộc tính của lớp. Khai báo psutil
2 trên một lớp cũng có tác dụng phụ là từ chối việc tạo các thuộc tính psutil
4 và psutil
5
Ở đây chúng ta có thể thấy thể hiện của lớp psutil
6 thực sự nhỏ hơn bao nhiêu. Việc không có psutil
4 sẽ loại bỏ toàn bộ 104 byte khỏi mỗi phiên bản, điều này có thể tiết kiệm dung lượng bộ nhớ khổng lồ khi khởi tạo hàng triệu giá trị
Các mẹo và thủ thuật trên sẽ hữu ích trong việc xử lý các giá trị số cũng như đối tượng psutil
8. Tuy nhiên, còn dây thì sao? . Nếu bạn định tìm kiếm thông qua một số lượng lớn các giá trị chuỗi, thì - như chúng ta đã thấy - sử dụng list
là một ý tưởng rất tồi. @profile
0 có thể phù hợp hơn một chút nếu tốc độ thực thi là quan trọng, nhưng có thể sẽ tiêu tốn nhiều RAM hơn. Tùy chọn tốt nhất có thể là sử dụng cấu trúc dữ liệu được tối ưu hóa, chẳng hạn như trie, đặc biệt đối với các tập dữ liệu tĩnh mà bạn sử dụng để truy vấn chẳng hạn. Như thường thấy với Python, đã có một thư viện cho điều đó, cũng như cho nhiều cấu trúc dữ liệu dạng cây khác, bạn sẽ tìm thấy một số trong số đó tại https. //github. com/pytries
Cách dễ nhất để tiết kiệm RAM là không sử dụng nó ngay từ đầu. Rõ ràng là bạn không thể tránh hoàn toàn việc sử dụng RAM, nhưng bạn có thể tránh tải toàn bộ tập dữ liệu cùng một lúc và thay vào đó làm việc với dữ liệu tăng dần nếu có thể. Cách đơn giản nhất để đạt được điều này là sử dụng các trình tạo trả về một trình vòng lặp lười biếng, tính toán các phần tử theo yêu cầu thay vì tất cả cùng một lúc
Công cụ mạnh hơn mà bạn có thể tận dụng là các tệp ánh xạ bộ nhớ, cho phép chúng tôi chỉ tải các phần dữ liệu từ một tệp. Thư viện chuẩn của Python cung cấp mô-đun @profile
1 cho việc này, có thể được sử dụng để tạo các tệp ánh xạ bộ nhớ hoạt động giống như tệp và mảng phụ. Bạn có thể sử dụng cả hai với các thao tác tệp như @profile
2, @profile
3 hoặc @profile
4 cũng như các thao tác chuỗi
Tải/đọc tập tin ánh xạ bộ nhớ rất đơn giản. Trước tiên, chúng tôi mở tệp để đọc như chúng tôi thường làm. Sau đó, chúng tôi sử dụng bộ mô tả tệp của tệp [ @profile
5] để tạo tệp ánh xạ bộ nhớ từ nó. Từ đó, chúng ta có thể truy cập dữ liệu của nó bằng cả thao tác tệp như @profile
2 hoặc thao tác chuỗi như cắt
Hầu hết thời gian, bạn có thể sẽ quan tâm nhiều hơn đến việc đọc tệp như được hiển thị ở trên, nhưng cũng có thể ghi vào tệp ánh xạ bộ nhớ
Sự khác biệt đầu tiên trong mã mà bạn sẽ nhận thấy là sự thay đổi trong chế độ truy cập thành @profile
7, biểu thị cả đọc và viết. Để chứng minh rằng chúng tôi thực sự có thể thực hiện cả thao tác đọc và viết, trước tiên chúng tôi đọc từ tệp và sau đó sử dụng RegEx để tìm kiếm tất cả các từ bắt đầu bằng chữ in hoa. Sau đó, chúng tôi chứng minh việc xóa dữ liệu khỏi tệp. Điều này không đơn giản như đọc và tìm kiếm, bởi vì chúng tôi cần điều chỉnh kích thước của tệp khi xóa một số nội dung của tệp. Để làm như vậy, chúng tôi sử dụng phương pháp @profile
8 của mô-đun @profile
1 sao chép 40 byte dữ liệu từ chỉ mục python -m memory_profiler
1 sang chỉ mục python -m memory_profiler
2, trong trường hợp này có nghĩa là xóa 10 byte đầu tiên
Nếu bạn đang tính toán trong NumPy, thì bạn có thể thích các tính năng [tài liệu] python -m memory_profiler
3 của nó phù hợp với các mảng NumPy được lưu trữ trong các tệp nhị phân
Tối ưu ứng dụng là bài toán khó nói chung. Nó cũng phụ thuộc rất nhiều vào nhiệm vụ hiện tại cũng như loại dữ liệu. Trong bài viết này, chúng tôi đã xem xét các cách phổ biến để tìm các vấn đề về sử dụng bộ nhớ và một số tùy chọn để khắc phục chúng. Tuy nhiên, có nhiều cách tiếp cận khác để giảm dung lượng bộ nhớ của một ứng dụng. Điều này bao gồm độ chính xác giao dịch cho không gian lưu trữ bằng cách sử dụng cấu trúc dữ liệu xác suất như bộ lọc nở hoa hoặc HyperLogLog. Một tùy chọn khác là sử dụng các cấu trúc dữ liệu dạng cây như DAWG hoặc Marissa trie rất hiệu quả trong việc lưu trữ dữ liệu chuỗi