So sánh hiệu suất multi-thread và multi-process trong Python

  Apr 14, 2020      2m      0   
 

Multithreading & Multiprocessing

So sánh hiệu suất multi-thread và multi-process trong Python

Multi-thread trong Python thực sự chưa giúp cải thiện hiệu suất chương trình nhiều, bởi tiến trình GIL (Global Interpreter Lock) đảm bảo chỉ cho một thread trong chương trình truy xuất biến dữ liệu. Điều này vô tình chặn các hoạt động của luồng xử lý (thread) khác trong chương trình (blocking I/O). Giải thích chi tiết bạn tham khảo thêm tại: https://www.quantstart.com/articles/Parallelising-Python-with-Threading-and-Multiprocessing/.

Ta có thể cải thiện hiệu suất chương trình Python để khai thác hết các core CPU xử lý bằng cách dùng multi-process (đa tiến trình). Lúc này, trong chương trình mình sẽ tạo thêm nhiều tiến trình con để độc lập xử lý các tác vụ. Trong Python có thư viện multiprocessing hỗ trợ việc này. Tốc độ xử lý multi-process thực sự nhanh hơn và hiệu suất cao hơn so với multi-thread.

Minh sẽ viết code thực nghiệm đo đạc đánh giá thời gian xử lý giữa dùng multi-thread và multi-process trong Python 3.

Hiện thực tác vụ xử lý

Mình sẽ hiện thực một tác vụ lý (tạm gọi là operation) tính toán tốn nhiều tài nguyên. Cụ thể là toán tử correlation / convolution (tham khảo bài viết: Xử lý ảnh - Convolution là gì?).

Một phép convolution này mặc định sẽ convolve ma trận 3x3 với một ảnh kích thước 128x128. Như vậy, số phép tính cần thiện hiện: 126 * 126 * (3 * 3 + 1) = 158760 phép toán (nhân và cộng). Vậy ta chỉ cần 10 operation này để có hơn 1 triệu phép tính.

operation.py

# -*- coding: utf-8 -*-
import random


def run_operation(
    num_op, matrix_size=128, kernel_size=3, float_from=-1.0, float_to=1.0
):
    for _ in range(num_op):
        op = Operation(matrix_size, kernel_size, float_from, float_to)
        op()
    pass


class Operation(object):
    def __init__(self, matrix_size=128, kernel_size=3, float_from=-1.0, float_to=1.0):
        self.matrix_size = matrix_size
        self.kernel_size = kernel_size
        self.float_from = float_from
        self.float_to = float_to

        assert self.matrix_size >= 3
        assert self.kernel_size >= 3
        assert self.matrix_size >= self.kernel_size
        pass

    def _init_matrix(self, size):
        matrix = []
        for r in range(size):
            one_row = []
            for c in range(size):
                one_row.append(random.uniform(self.float_from, self.float_to))
            matrix.append(one_row)
        return matrix

    def __call__(self):
        self.matrix = self._init_matrix(size=self.matrix_size)
        self.kernel = self._init_matrix(size=self.kernel_size)

        nloop = self.matrix_size - self.kernel_size + 1
        self.result = self._init_matrix(size=nloop)
        for my in range(nloop):
            for mx in range(nloop):
                for ky in range(self.kernel_size):
                    for kx in range(self.kernel_size):
                        kernel_val = self.kernel[ky][kx]
                        matrix_val = self.matrix[my + ky][mx + kx]
                        self.result[my][mx] = matrix_val * kernel_val
        return True

Hiện thực xử lý multi-thread trong Python

Tiếp đến, mình sẽ hiện thực multi-thread trong Python để chạy các operation trên. Tất cả các file Python các bạn để trong cùng thư mục nhé: operation.py, multi_thread.py, multi_process.py.

multi_thread.py

# -*- coding: utf-8 -*-
import time
import threading
from operation import run_operation

class MultiThread(object):
    def __init__(self, num_thread=4, num_op=100):
        self.num_thread = num_thread
        self.num_op = num_op
        assert self.num_thread > 0
        assert self.num_op > 0
        pass

    def __call__(self):
        thread_list = []
        for _ in range(self.num_thread):
            t = threading.Thread(target=run_operation, args=(self.num_op,))
            t.start()
            thread_list.append(t)

        for _ in range(len(thread_list)):
            t = thread_list[_]
            t.join()

        pass
    
def main(num_cpus=4, num_ops=10):
    tstart = time.time()
    multi = MultiThread(num_thread=num_cpus, num_op=num_ops)
    multi()
    tend = time.time()
    print("Time for running %d threads (%d ops) is %.2f seconds" % (num_cpus, num_ops, tend-tstart))
    
if __name__ == "__main__":
    main()    

Chạy script trên với câu lệnh và được kết quả in ra như bên dưới:

$ python3 multi_thread.py
Time for running 4 threads (10 ops) is 3.16 seconds

Vậy là mình mất 3.16 giây để xử lý 40 operation (4 thread, mỗi thread xử lý 10 operation)

Hiện thực xử lý multi-process trong Python

Tương tự, ta hiện thực tác vụ xử lý hệt như trên nhưng dùng multiprocessing.

multi_process.py

# -*- coding: utf-8 -*-
import time
from multiprocessing import Process
from operation import run_operation

class MultiProcess(object):
    def __init__(self, num_process=4, num_op=100):
        self.num_process = num_process
        self.num_op = num_op
        assert self.num_process > 0
        assert self.num_op > 0
        pass

    def __call__(self):
        process_list = []
        for _ in range(self.num_process):
            p = Process(target=run_operation, args=(self.num_op,))
            p.start()
            process_list.append(p)

        for _ in range(len(process_list)):
            p = process_list[_]
            p.join()

        pass
    
def main(num_cpus=4, num_ops=10):
    tstart = time.time()
    multi = MultiProcess(num_process=num_cpus, num_op=num_ops)
    multi()
    tend = time.time()
    print("Time for running %d processes (%d ops) is %.2f seconds" % (num_cpus, num_ops, tend-tstart))
    
if __name__ == "__main__":
    main()    

Chạy thử:

$ python3 multi_process.py
Time for running 4 processes (10 ops) is 1.34 seconds

Multi-process chỉ mất 1.34 giây để xử lý 40 operation (4 process, mỗi process xử lý 10 operation)

Chưa cần phân tích sâu thêm, ta cũng đã thấy rằng multi-process thực sự nhanh hơn multi-thread nhiều.

Thư viện đánh giá hiệu suất xử lý giữa multi-thread và multi-process

Minh cũng đã hiện thực việc đánh giá hiệu suất thành một gói thư viện Python để tiện việc chạy thử và đánh giá trên máy.

Cách sử dụng:

$ sudo pip3 install python_benchmark_thread_vs_process
$ python_benchmark_thread_vs_process
Benchmarking (4 CPUs @ 3559MHz) ... please wait...
====================
| BENCHMARK RESULT |
====================
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+
| Num CPUs | CPU Model                               | Current CPU Freq (MHz) | Multi-Thread Time (s) | Multi-Process Time (s) | Total Test Operation |
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+
| 4        | Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz | 3559                   | 32.5341               | 13.1884                | 400                  |
+----------+-----------------------------------------+------------------------+-----------------------+------------------------+----------------------+

Lưu ý: chạy đánh giá bằng thư viện mất từ 1-5 phút để hoàn thành, vì số lượng operation test sẽ là 100 operation trên mỗi core CPU.

Nhận xét việc dùng multi-thread và multi-process

Minh đã chạy thư viện đánh giá trên nhiều máy tính, server mà mình được phép truy cập để có bảng đánh giá so sánh hiệu suất giữa multi-thread và multi-process như dưới đây:

Num CPUs CPU Model Current CPU Freq (MHz) Multi-Thread Time (s) Multi-Process Time (s) Total Test Operation Multi-Process Speedup
1 Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz 2500 11.7581 12.0673 100 0.97x
4 Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz 2474 55.3840 8.8589 400 6.25x
4 Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz 2683 20.9098 10.9195 400 1.91x
16 Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz 2597 98.6584 7.1033 1600 13.89x
24 Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz 1331 372.3926 18.5923 2400 20.03x
32 Intel(R) Xeon(R) Silver 4108 CPU @ 1.80GHz 809 478.8115 15.0538 3200 31.80x
72 Intel(R) Xeon(R) Gold 5220S CPU @ 2.70GHz 1016 550.4936 11.6759 7200 47.14x

Nhận xét multi-thread và multi-proces trong Python:

  • Cấu hình máy 1 core CPU, xử lý đơn luồng (single thread, chỉ có main thread), đa luồng (multi-thread) hay đa tiến trình (multi-process) đều sẽ cho kết quả như nhau. Thậm chí, multi-process sẽ chậm hơn một chút cho chi phí khởi tạo tiến trình lớn hơn khởi tạo luồng. Trong Python, thread còn được gọi là light-weight process (https://docs.python.org/3/library/_thread.html).
  • Tốc độ xử lý phụ thuộc vào cấu hình phần cứng (CPU model, CPU Frequency MHz, số core CPU) cũng như mức độ bận rộn của tài nguyên máy tính ở thời điểm test.
  • Đa tiến trình (multi process) nhanh hơn đa luồng (multi thread) trong Python, do đa tiến trình khai thác được tính toán song song đa lõi của CPU.
  • Multi thread chia sẻ bộ nhớ giữa các thread con dễ dàng hơn so với process, do đó developer dễ hiện thực hơn, để có thể chia sẻ giữa các process cần hiện thực "công phu" hơn. Tham khảo thêm thư viện multiprocessing của Python: https://docs.python.org/3/library/multiprocessing.html

Bài viết về Python:

Tham gia ngay group trên Facebook để cùng thảo luận với đồng bọn nhé:

Khám phá Python - PyGroup