Lên Đời Tốc Độ Phép Tính Toán Trong Python Với Numba

Lên Đời Tốc Độ Phép Tính Toán Trong Python Với Numba

Từ lâu, giới lập trình viên đều mặc định là Python tính toán chậm hơn C++. Thế nên, nếu ai đó bảo Python có thể tính toán nhanh như C++ thì chắc họ phải làm gì đó để "lên đời" những dòng code code Python. Trong bài viết này, tôi sẽ giới thiệu với các bạn một công cụ có tên Numba, là một trong những thứ có thể giúp cho Python trở thành một thế lực đáng gờm về mặt tốc độ thực thi so với C++.

Bài viết này sẽ gồm các phần: So sánh tốc độ thực thi giữa C++ và Python, Giải thích lý do tại sao lại có sự chênh lệch này và "lên thần" cho code Python.

Tốc độ thực thi của Python vs C++

Để so sánh tốc độ thực thi của Python và C++, chúng ta sẽ cùng nhau sử dụng Python và C++ để giải quyết bài toán tìm số nguyên tố bằng thuật toán cơ bản (bỏ qua việc tối ưu thuật toán):
- Nếu số đó bằng 2 thì trả về True.
- Nếu số đó nhỏ hơn bằng 1 hoặc chia hết cho 2 thì trả về False.
- Nếu số đó chia hết cho 1 số nguyên dương trong khoảng lớn hơn bằng 3 và nhỏ hơn bằng căn bậc hai của số đó thì trả về False (bước nhảy số là 2).
- Trả về True nếu đi qua được cả 3 điều kiện trên.

Ta sẽ thực hiện kiểm tra số nguyên tố cho danh sách các số từ 1 đến number = 10000000 (mười triệu) để có thể tăng thời gian thực thi của chương trình.
Nếu viết bằng Python, ta có đoạn source code như sau (lưu lại thành file is_prime.cpp):

import math
import timeit


def is_prime(number):
    if number == 2:
        return True
    if number <= 1 or not number % 2:
        return False
    max_range = int(math.sqrt(number)) + 1
    for div in range(3, max_range, 2):
        if not number % div:
            return False
    return True


def run_program(max_number):
    for number in range(max_number):
        is_prime(number)


if __name__ == '__main__':
    max_number = 10000000
    start = timeit.default_timer()
    run_program(max_number)
    stop = timeit.default_timer()
    print(stop - start, " (seconds)")

Nếu viết bằng C++, ta sẽ có đoạn code như sau (lưu lại thành file is_prime.cpp):

#include <iostream>
#include <cmath>
#include <time.h>

using namespace std;

bool isPrime(int number)
{
    if (number == 2)
        return true;
    if (number <= 1 || number % 2 == 0)
        return false;
    double sqrt_num = sqrt(double(number));
    for (int div = 3; div <= sqrt_num; div += 2)
    {
        if (number % div == 0)
            return false;
    }
    return true;
}

void runProgram(int max_number)
{
    for (int number = 0; number < max_number; number++)
        isPrime(number);
}

int main()
{
    int MAX_NUMBER = 10000000;
    clock_t start, end;
    start = clock();
    runProgram(MAX_NUMBER);
    end = clock();
    cout << (end - start) / ((double)CLOCKS_PER_SEC);
    cout << " (seconds)\n";
    return 0;
}

Kết quả chạy:

[email protected]:~$ g++ is_prime.cpp -o is_prime_exe && ./is_prime_exe
2.17188 (seconds)


[email protected]:~$ python3.8 is_prime.py
32.363274600000295  (seconds)
[email protected]:~$ python3.8 is_prime.py
30.730028500001936  (seconds)

Như này là trên máy của tôi C++ đang nhanh như Python khoảng ~15 lần.

Tại sao C++ lại nhanh hơn Python

C++ là ngôn ngữ lập trình với các mã nguồn được thực thi theo cách biên dịch, đó là việc trình biên dịch sẽ thực hiện "dịch" trực tiếp source code sang mã máy sau đó thực hiện thực thi mà không cần qua một bước trung gian nào.

Python thì khác, chúng ta thường sử dụng Cython để thực hiện thông dịch source code Python sang thành định dạng .pyc file (chúng ta hay gọi là pycache) sau đó mới thực hiện biên dịch pyc file sang mã máy. Do qua các bước trung gian nên việc thực thi Python source code mới bị chậm hơn rất nhiều so với C++.

Ngoài lý do trên, thì Python sử dụng kiểu dữ liệu động (dynamic type) còn C++ dùng static type.
Ví dụ:
Với C++:
int MAX_NUMBER = 10000000;

Với Python:
MAX_NUMBER = 10000000

Với C++, trình biên dịch đã biết ngay kiểu dữ liệu của MAX_NUMBER là integer và sẽ thực hiện cấp phát bộ nhớ được ngay lập tức.
Với C++, trình thông dịch sẽ phải đọc đến một object là PyObject_HEAD để biết được biến dữ liệu MAX_NUMBER là integer hay string.
Chính vì kiểu dữ liệu động nên Python quản lý việc cấp phát bộ nhớ không tốt cho lắm so với C++.

Sử dụng numba để tăng tốc độ tính toán cho Python.

Numba là một trình dịch JIT mã nguồn mở dùng dùng cho Python và đặc biệt là numpy. Numba sẽ thực hiện "dịch" source code python sang trực tiếp mã máy để tăng tốc độ thực thi.
Numba được tài trợ phát triển bởi Anaconda. Numba được ứng dụng nhiều trong Data Science giúp tăng tốc độ biên dịch code với NumPy Array.
Với ngôn ngữ lập trình Python, numba cung cấp một số Annotation (ký pháp decorator) để có thể được tối ưu hóa việc thực thi nhằm đạt được hiệu suất tương tự như C/C ++.

Các bạn có thể tham khảo thêm về numba tại link: https://numba.pydata.org/
Có một chút lưu ý là numba chỉ dùng với Python từ version 3.7 trở lên. Và khi dùng trên Windows 64bit thì ... hên xui do một số bug của numpy trên nền tảng hệ điều hành này.

Cài đặt numba: pip install numba

Sau đây là thay đổi nhỏ trong file is_prime.py để đẩy nhanh tốc độ thực thi của source code và lưu lại thành file is_prime_numba.py:

from numba import njit


@njit(fastmath=True, cache=True)
def is_prime(number):
    if number == 2:
        return True
    if number <= 1 or not number % 2:
        return False
    max_range = int(math.sqrt(number)) + 1
    for div in range(3, max_range, 2):
        if not number % div:
            return False
    return True


@njit(fastmath=True, cache=True)
def run_program(max_number):
    for number in range(max_number):
        is_prime(number)

Thời gian thực thi đã giảm xuống đáng kể:

[email protected]:~$ python3.8 is_prime_numba.py
2.4775927999980922  (seconds)
[email protected]:~$ python3.8 is_prime_numba.py
2.3385338000007323  (seconds)

Nếu như bình thường, chắc chắn tôi đã reo lên khi tốc độ thực thi giảm xuống sát chỉ kém C++ khoảng 10% và vui vẻ mang đi khoe. Tuy nhiên sau khi tìm hiểu tiếp thì Numba còn cung cấp cho các lập trình viên Python một trình biên dịch có tốc độ còn nhiều hơn con số -10% ở trên.

Numba cung cấp khả năng cho phép tính toán song song với các tác vụ có sự lặp lại bằng tham số parallel=True.
Để sử dụng được chức năng này, trong vòng lặp, chúng ta phải thay thế range thành prange. Với prange là một function được import từ numba.

from numba import njit, prange


@njit(fastmath=True, cache=True)
def is_prime(number):
    if number == 2:
        return True
    if number <= 1 or not number % 2:
        return False
    max_range = int(math.sqrt(number)) + 1
    for div in prange(3, max_range, 2):
        if not number % div:
            return False
    return True


@njit(fastmath=True, cache=True, parallel=True)
def run_program(max_number):
    for number in prange(max_number):
        is_prime(number)

Thời gian thực thi của function đã thay đổi đến mức...kinh ngạc:

[email protected]:~$ python3.8 is_prime_numba.py
0.4278720999973302  (seconds)
[email protected]:~$ python3.8 is_prime_numba.py
0.4179699999986042  (seconds)
[email protected]:~$ python3.8 is_prime_numba.py
0.43309489999955986  (seconds)
[email protected]:~$ python3.8 is_prime_numba.py
0.4464455999986967  (seconds

So với con số 2.x seconds của C++ thì kết quả trên mang lại niềm hứng khởi cho các lập trình viên Python. (Tất nhiên là do phía C++ đang chưa dùng đến parallel computing).

Ngoài các chức năng trên, chúng ta cũng có thể tìm được các function, các cách ứng dụng khác của numba từ tài liệu: https://numba.pydata.org/numba-doc/latest/user/5minguide.html

Kết.

Một bài ví dụ vui vui, mang lại sự tự tin cho các lập trình Python khi thực hiện xử lý các bài toán với mức độ tính toán nhiều và phức tạp. Bài viết hoàn toàn không có tính chất so sánh hay hạ thấp tính năng tính toán phức tạp của ngôn ngữ C++. Cảm ơn các bạn đã đọc bài viết của tôi.
Source code của bài toán được upload lên: https://github.com/quangvinh1986/python-numba-sample.