Một khía cạnh của mã hóa trong Python mà chúng ta vẫn chưa thảo luận chi tiết là làm thế nào để tối ưu hóa hiệu suất thực thi của các mô phỏng của chúng ta. Mặc dù NumPy, SciPy và pandas cực kỳ hữu ích về mặt này khi xem xét mã được vector hóa, nhưng chúng tôi không thể sử dụng các công cụ này một cách hiệu quả khi xây dựng các hệ thống hướng sự kiện. Có bất kỳ phương tiện nào khác có sẵn cho chúng tôi để tăng tốc mã của chúng tôi không?
Trong bài viết này, chúng ta sẽ xem xét các mô hình song song khác nhau có thể được đưa vào các chương trình Python của chúng ta. Các mô hình này hoạt động đặc biệt tốt cho các mô phỏng không cần chia sẻ trạng thái. Mô phỏng Monte Carlo được sử dụng để định giá quyền chọn và mô phỏng kiểm tra lại các thông số khác nhau cho giao dịch theo thuật toán thuộc danh mục này
Cụ thể, chúng ta sẽ xem xét thư viện Threading và thư viện Multiprocessing
Đồng thời trong Python
Một trong những câu hỏi thường gặp nhất đối với các lập trình viên Python mới bắt đầu khi họ khám phá mã đa luồng để tối ưu hóa mã giới hạn CPU là "Tại sao chương trình của tôi chạy chậm hơn khi tôi sử dụng nhiều luồng?"
Kỳ vọng là trên một máy đa lõi, mã đa luồng sẽ sử dụng các lõi bổ sung này và do đó tăng hiệu suất tổng thể. Thật không may, phần bên trong của trình thông dịch Python chính, CPython, phủ nhận khả năng đa luồng thực sự do một quá trình được gọi là Khóa thông dịch viên toàn cầu [GIL]
GIL là cần thiết vì trình thông dịch Python không an toàn cho luồng. Điều này có nghĩa là có một khóa được thi hành trên toàn cầu khi cố gắng truy cập an toàn các đối tượng Python từ bên trong các luồng. Tại bất kỳ thời điểm nào, chỉ một luồng duy nhất có thể có khóa cho đối tượng Python hoặc API C. Trình thông dịch sẽ yêu cầu lại khóa này cho mỗi 100 mã byte của hướng dẫn Python và xung quanh [có khả năng] chặn hoạt động I/O. Do khóa này, mã giới hạn CPU sẽ không tăng hiệu suất khi sử dụng thư viện Phân luồng, nhưng nó có thể sẽ tăng hiệu suất nếu sử dụng thư viện Đa xử lý
Thực hiện thư viện song song hóa
Bây giờ chúng tôi sẽ sử dụng hai thư viện riêng biệt ở trên để cố gắng tối ưu hóa song song vấn đề "đồ chơi"
Thư viện luồng
Ở trên, chúng tôi đã đề cập đến thực tế là Python trên trình thông dịch CPython không hỗ trợ thực thi đa lõi thực sự thông qua đa luồng. Tuy nhiên, Python KHÔNG có thư viện Threading. Vì vậy, lợi ích của việc sử dụng thư viện là gì nếu chúng ta [được cho là] không thể sử dụng nhiều lõi?
Nhiều chương trình, đặc biệt là những chương trình liên quan đến lập trình mạng hoặc đầu vào/đầu ra dữ liệu [I/O] thường bị ràng buộc mạng hoặc bị ràng buộc I/O. Điều này có nghĩa là trình thông dịch Python đang đợi kết quả của lệnh gọi hàm thao tác dữ liệu từ nguồn "từ xa" chẳng hạn như địa chỉ mạng hoặc đĩa cứng. Truy cập như vậy chậm hơn nhiều so với đọc từ bộ nhớ cục bộ hoặc bộ đệm CPU
Do đó, một phương tiện để tăng tốc mã như vậy nếu nhiều nguồn dữ liệu đang được truy cập là tạo một chuỗi cho từng mục dữ liệu cần được truy cập
Ví dụ: hãy xem xét mã Python đang thu thập nhiều URL web. Cho rằng mỗi URL sẽ có thời gian tải xuống được liên kết vượt quá khả năng xử lý CPU của máy tính, việc triển khai đơn luồng sẽ bị ràng buộc I/O đáng kể
Bằng cách thêm một luồng mới cho mỗi tài nguyên tải xuống, mã có thể tải xuống nhiều nguồn dữ liệu song song và kết hợp các kết quả ở cuối mỗi lần tải xuống. Điều này có nghĩa là mỗi lần tải xuống tiếp theo không chờ tải xuống các trang web trước đó. Trong trường hợp này, chương trình hiện bị ràng buộc bởi giới hạn băng thông của [các] máy khách/máy chủ thay vì
Tuy nhiên, nhiều ứng dụng tài chính LÀ CPU bị ràng buộc vì chúng rất chuyên sâu về số lượng. Chúng thường liên quan đến các giải pháp đại số tuyến tính số quy mô lớn hoặc các phép rút thăm thống kê ngẫu nhiên, chẳng hạn như trong mô phỏng Monte Carlo. Do đó, đối với Python và GIL, không có lợi ích gì khi sử dụng thư viện Python Threading cho các tác vụ đó
Triển khai Python
Đoạn mã sau minh họa cách triển khai đa luồng cho mã "đồ chơi" để thêm các số vào danh sách một cách tuần tự. Mỗi luồng tạo một danh sách mới và thêm các số ngẫu nhiên vào đó. Cái này đã được chọn làm ví dụ đồ chơi vì nó nặng CPU
Đoạn mã sau sẽ phác thảo giao diện cho thư viện Threading nhưng nó sẽ không cấp cho chúng tôi bất kỳ khả năng tăng tốc bổ sung nào ngoài khả năng có thể đạt được trong triển khai đơn luồng. Khi chúng ta bắt đầu sử dụng thư viện Đa xử lý bên dưới, chúng ta sẽ thấy rằng nó sẽ giảm đáng kể thời gian chạy tổng thể
Hãy kiểm tra xem mã hoạt động như thế nào. Đầu tiên, chúng tôi nhập thư viện
time python thread_test.py
4. Sau đó, chúng tôi tạo một hàm time python thread_test.py
5 có ba tham số. Đầu tiên, time python thread_test.py
6, xác định kích thước của danh sách để tạo. Thứ hai, time python thread_test.py
0, là ID của "công việc" [có thể hữu ích nếu chúng tôi đang ghi thông tin gỡ lỗi vào bảng điều khiển]. Tham số thứ ba, time python thread_test.py
1, là danh sách để nối các số ngẫu nhiên vàoHàm
time python thread_test.py
2 tạo một time python thread_test.py
3 của $10^7$ và sử dụng hai time python thread_test.py
4 để thực hiện công việc. Sau đó, nó tạo một danh sách time python thread_test.py
5, được sử dụng để lưu trữ các chủ đề riêng biệt. Đối tượng time python thread_test.py
6 lấy hàm time python thread_test.py
5 làm tham số rồi nối nó vào danh sách time python thread_test.py
5Cuối cùng, các công việc được bắt đầu tuần tự và sau đó được "tham gia" tuần tự. Phương thức
time python thread_test.py
9 chặn luồng gọi [i. e. luồng trình thông dịch Python chính] cho đến khi luồng kết thúc. Điều này đảm bảo rằng tất cả các luồng đã hoàn thành trước khi in thông báo hoàn thành ra bàn điều khiển# thread_test.py
import random
import threading
def list_append[count, id, out_list]:
"""
Creates an empty list and then appends a
random number to the list 'count' number
of times. A CPU-heavy operation!
"""
for i in range[count]:
out_list.append[random.random[]]
if __name__ == "__main__":
size = 10000000 # Number of random numbers to add
threads = 2 # Number of threads to create
# Create a list of jobs and then iterate through
# the number of threads appending each thread to
# the job list
jobs = []
for i in range[0, threads]:
out_list = list[]
thread = threading.Thread[target=list_append[size, i, out_list]]
jobs.append[thread]
# Start the threads [i.e. calculate the random number lists]
for j in jobs:
j.start[]
# Ensure all of the threads have finished
for j in jobs:
j.join[]
print "List processing complete."
Chúng tôi có thể tính thời gian mã này bằng lệnh gọi bàn điều khiển sau
time python thread_test.py
Nó tạo ra đầu ra sau
time python thread_test.py
1Lưu ý rằng cả
time python thread_test.py
10 và time python thread_test.py
11 đều xấp xỉ tổng của thời gian time python thread_test.py
12. Điều này cho thấy rằng chúng tôi không thu được lợi ích gì từ việc sử dụng thư viện Threading. Nếu chúng tôi có thì chúng tôi mong đợi thời gian time python thread_test.py
12 sẽ ít hơn đáng kể. Các khái niệm này trong lập trình đồng thời thường được gọi là thời gian CPU và thời gian đồng hồ treo tường tương ứngThư viện đa xử lý
Để thực sự sử dụng các lõi bổ sung có trong hầu hết các bộ xử lý tiêu dùng hiện đại, thay vào đó, chúng ta có thể sử dụng thư viện Đa xử lý. Điều này hoạt động theo một cách cơ bản khác với thư viện Threading, mặc dù cú pháp của cả hai cực kỳ giống nhau
Thư viện Đa xử lý thực sự sinh ra nhiều quy trình hệ điều hành cho mỗi tác vụ song song. Điều này hỗ trợ GIL một cách độc đáo, bằng cách cung cấp cho mỗi quy trình trình thông dịch Python của riêng nó và do đó, GIL của riêng nó. Do đó, mỗi quy trình có thể được đưa vào một lõi bộ xử lý riêng biệt và sau đó được nhóm lại ở cuối khi tất cả các quy trình đã hoàn thành
Có một số nhược điểm, tuy nhiên. Sinh ra các quy trình bổ sung giới thiệu chi phí I/O vì dữ liệu phải được xáo trộn giữa các bộ xử lý. Điều này có thể thêm vào thời gian chạy tổng thể. Tuy nhiên, giả sử dữ liệu bị hạn chế đối với từng quy trình, có thể tăng tốc đáng kể. Tất nhiên, người ta phải luôn nhận thức được Định luật Amdahl
Triển khai Python
Các sửa đổi duy nhất cần thiết cho việc triển khai Đa xử lý bao gồm thay đổi dòng nhập và dạng chức năng của dòng
time python thread_test.py
14. Trong trường hợp này, các đối số của hàm đích được truyền riêng. Ngoài ra, mã này gần giống với cách triển khai Threading ở trêntime python thread_test.py
7Chúng ta có thể lặp lại mã này một lần nữa bằng cách sử dụng lệnh gọi bảng điều khiển tương tự
time python thread_test.py
8Chúng tôi nhận được đầu ra sau
time python thread_test.py
9Trong trường hợp này, bạn có thể thấy rằng mặc dù thời gian của
time python thread_test.py
10 và time python thread_test.py
11 gần như giống nhau nhưng thời gian của time python thread_test.py
12 đã giảm gần hai lần. Điều này có ý nghĩa vì chúng tôi đang sử dụng hai quy trình. Chia tỷ lệ thành bốn quy trình trong khi giảm một nửa kích thước danh sách để so sánh sẽ cho kết quả sau [với giả định rằng bạn có ít nhất bốn lõi. ]time python thread_test.py
3Đây là khoảng 3. Tăng tốc 8 lần với bốn quy trình. Tuy nhiên, chúng ta phải cẩn thận khi khái quát hóa điều này cho các chương trình lớn hơn, phức tạp hơn. Truyền dữ liệu, mức bộ đệm phần cứng và các vấn đề khác gần như chắc chắn sẽ làm giảm loại hiệu suất đạt được này trong các mã "thực"
Trong các bài viết sau, chúng tôi sẽ sửa đổi Trình kiểm tra theo hướng sự kiện để sử dụng các kỹ thuật song song nhằm cải thiện khả năng thực hiện các nghiên cứu tối ưu hóa tham số đa chiều