Python: Đánh giá hiệu năng của code với cProfiler

Giới thiệu

Profiler là công cụ cho phép lập trình viên đánh giá hiệu năng của các đoạn code trong chương trình, từ đó tìm ra đoạn code chạy chậm hoặc tiêu tốn nhiều tài nguyên để optimize.

Từ phiên bản 2.5, Python cung cấp module cProfile cho phép đo đạc thời gian tính toán, tài nguyên sử dụng của code.

cProfile

Trong phần này, ta xét một ví dụ đơn giản, đó là tính số hạng thứ n trong dãy Fibbonacci sử dụng đệ quy.

Tạo file fib.py có nội dung như sau:

import sys
import functools

def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

@functools.lru_cache()
def fib_cached(n):
    if n < 2:
        return n
    return fib_cached(n-1) + fib_cached(n-2)

if __name__ == '__main__':
    n = 32
    if sys.argv[-1] == 'cached':
        fib_cached(n)
    else:
        fib(n)

Cả 2 hàm đều tính dãy Fibonacci sử dụng đệ quy. Tuy nhiên, hàm fib_cached sử dụng thêm cache để ghi lại các giá trị được tính toán. Chi tiết về cách sử dụng lru_cache trong Python, bạn đọc có thể xem tại đây.

Tiếp theo, ta thử đánh giá hiệu năng của chương trình trong trường hợp không sử dụng bộ nhớ cache. Chạy dòng sau trong terminal:

python3 -m cProfile -s calls fib.py

Kết quả nhận được:

7049175 function calls (21 primitive calls) in 1.468 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
7049155/1    1.468    0.000    1.468    1.468 fib.py:5(fib)
        7    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.setattr}
        1    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}
        1    0.000    0.000    1.468    1.468 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    1.468    1.468 fib.py:1(<module>)
        1    0.000    0.000    0.000    0.000 functools.py:479(decorating_function)
        1    0.000    0.000    0.000    0.000 functools.py:448(lru_cache)
        1    0.000    0.000    0.000    0.000 functools.py:44(update_wrapper)

Các thông số trong kết quả:

  • ncalls: tổng số lần gọi hàm
  • tottime: tổng thời gian sử dụng để thực hiện một hàm (không bao gồm thời gian gọi các hàm con)
  • percall: bằng tottime / ncalls
  • cumtime: tổng thời gian tích luỹ để thực hiện xong phần code, từ khi được gọi cho đến khi kết thúc
  • filename:lineno(function): phần code được thực hiện Bây giờ, thử đánh giá chương trình trong trường hợp sử dụng cache:
python3 -m cProfile -s calls fib.py cached

Kết quả:

53 function calls (21 primitive calls) in 0.000 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     33/1    0.000    0.000    0.000    0.000 fib.py:11(fib_cached)
        7    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.setattr}
        1    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 fib.py:1(<module>)
        1    0.000    0.000    0.000    0.000 functools.py:479(decorating_function)
        1    0.000    0.000    0.000    0.000 functools.py:448(lru_cache)
        1    0.000    0.000    0.000    0.000 functools.py:44(update_wrapper)

Trường hợp sử dụng cache, số lần gọi hàm giảm xuống còn 33, thời gian thực hiện xấp xỉ 0s (trường hợp không sử dụng cache, số lần gọi hàm là hơn 7 triệu 😅).

Visualization

Rõ ràng việc hiển thị kết quả trên terminal khá nhàm chán và khó theo dõi được kết quả. Do đó cần một công cụ để biểu diễn trực quan kết quả. Một trong các công cụ đó là KCachegrind. Với Ubuntu, KCachegrind có thể dễ dàng cài đặt chỉ bằng một câu lệnh

sudo apt-get install kcachegrind

Để biểu diễn kết quả trên KCachegrind, trước tiên cần lưu kết quả ra cProfile file, sau đó cần đổi sang định dạng callgrind file mà KCachegrind có thể hiểu được. Để làm dược việc này cần thêm module pyprof2calltree của Python:

pip3 install pyprof2calltree --user

Ví dụ với chương trình tính dãy số Fibonacci:

  • Lưu lại kết quả
python3 -m cProfile -o fib.profile fib.py
  • Chuyển đổi kết quả sang định dạng file callgrind
pyprof2calltree -i fib.profile -o fib.callgrind
  • Visualize kết quả với KCachegrind
kcachegrind fib.callgrind

hoặc có thể mở chương trình KCachegrind, chọn File/Open, sau đó chọn đường dẫn đến file fib.callgrind.

Kết quả thu được:

Kết luận

Trên đây là trình bày về cách sử dụng cơ bản công cụ cProfile. Bài viết có thể còn nhiều thiếu sót, rất mong nhận được sự góp ý của mọi người.

Tài liệu tham khảo