Sử dụng line_profiler để tối ưu hóa mã nguồn
Việc phân tích hiệu năng giúp xác định thời gian thực thi của từng đoạn mã, từ đó phát hiện các điểm nghẽn trong chương trình. Trong khi cProfile và profile chỉ cung cấp thông tin theo hàm, line_profiler đi sâu hơn bằng cách hiển thị chi phí thời gian ở cấp độ từng dòng lệnh — điều này cực kỳ hữu ích khi cần tối ưu hóa mã.
Công cụ này đặc biệt phù hợp với các hàm có thời gian chạy dài hơn đáng kể so với chi phí theo dõi của chính nó. Với các hàm quá nhỏ (nanosecond), dữ liệu phân tích có thể bị méo mó do overhead. Dưới đây là hướng dẫn thực hành về cách tích hợp line_profiler vào quy trình phát triển.
Cài đặt và kiểm tra phiên bản
pip install line_profiler
Kiểm tra phiên bản đã cài:
import line_profiler
print(line_profiler.__version__) # Ví dụ: '4.1.3'
Phân tích cơ bản theo hàm
Xét một ví dụ gồm các hàm mô phỏng xử lý tốn CPU và thời gian chờ:
import time
import math
def tinh_tong_binh_phuong(start, stop):
return sum(math.pow(i, 2) for i in range(start, stop))
def xu_ly(thoi_gian_cho):
time.sleep(thoi_gian_cho)
ket_qua = tinh_tong_binh_phuong(0, 1000000)
return ket_qua / 1000000
def chay_thu():
xu_ly(1)
xu_ly(3)
xu_ly(5)
Để theo dõi từng dòng trong chay_thu, sử dụng LineProfiler:
from line_profiler import LineProfiler
profiler = LineProfiler()
wrapper = profiler(chay_thu)
wrapper()
profiler.print_stats()
Đầu ra sẽ bao gồm:
- Timer unit: đơn vị thời gian (thường là nanosecond)
- Total time: tổng thời gian thực thi hàm
- Hits: số lần dòng được thực thi
- Time: tổng thời gian thực thi dòng đó
- Per Hit: trung bình mỗi lần thực thi
- % Time: phần trăm thời gian chiếm dụng — chỉ tiêu then chốt để tìm điểm nghẽn
Theo dõi nhiều hàm lồng nhau
Nếu muốn phân tích cả các hàm con như xu_ly và tinh_tong_binh_phuong, cần đăng ký chúng trước:
profiler.add_function(xu_ly)
profiler.add_function(tinh_tong_binh_phuong)
wrapper = profiler(chay_thu)
wrapper()
profiler.print_stats(output_unit=1e-3) # Đổi sang millisecond
Lúc này, kết quả sẽ hiển thị riêng cho từng hàm, giúp hiểu rõ luồng gọi và chi phí phân bổ.
Dùng decorator để đánh dấu hàm cần phân tích
Có thể dùng cú pháp @ để gắn profiler trực tiếp lên hàm:
@profiler
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Gọi hàm bình thường
fibonacci(10)
profiler.print_stats()
Phương pháp này gọn nhẹ, thích hợp khi chỉ cần theo dõi một vài hàm trọng tâm.
Phân tích phương thức trong lớp
Với các lớp, cần truyền phương thức đã bind vào profiler:
class XuLyDuLieu:
def __init__(self, du_lieu):
self.du_lieu = du_lieu
self.bang_nho = {}
def xu_ly_tat_ca(self):
return [self._xu_ly_mot_muc(item) for item in self.du_lieu]
def _xu_ly_mot_muc(self, muc):
if muc not in self.bang_nho:
self.bang_nho[muc] = self._tinh_toan_phuc_tap(muc)
return self.bang_nho[muc]
def _tinh_toan_phuc_tap(self, x):
return sum((x + i)**2 for i in range(100))
doi_tuong = XuLyDuLieu(list(range(100)))
profiler = LineProfiler(
doi_tuong.xu_ly_tat_ca,
doi_tuong._xu_ly_mot_muc,
doi_tuong._tinh_toan_phuc_tap
)
runner = profiler(doi_tuong.xu_ly_tat_ca)
ket_qua = runner()
print(f"Xử lý xong {len(ket_qua)} mục")
profiler.print_stats()
Giới hạn phạm vi phân tích bằng context manager
Đôi khi chỉ muốn theo dõi một đoạn mã cụ thể. Kỹ thuật dưới đây giúp bật/tắt phân tích linh hoạt:
def tac_vu_nang(n):
ket_qua = 0
for i in range(n):
for j in range(1000):
ket_qua += (i * j) % (n + 1)
return ket_qua
def tac_vu_nhanh(data):
return [x * 2 for x in data]
profiler = LineProfiler(tac_vu_nang)
with profiler:
kq1 = tac_vu_nang(1000)
print("Kết quả nhanh:", tac_vu_nhanh(range(10)))
profiler.print_stats()
Lưu kết quả ra file
Để lưu báo cáo phân tích vào tập tin văn bản:
with open('ket_qua_phan_tich.txt', 'w') as f:
profiler.print_stats(stream=f)
Kết hợp với memory_profiler
Một kỹ thuật nâng cao là kết hợp line_profiler (theo dõi thời gian) cùng memory_profiler (theo dõi bộ nhớ):
from memory_profiler import profile as mem_profile
class XuLyLon:
@mem_profile
def xu_ly_khoi_lon(self, data):
return list(map(self._bien_doi, data))
def _bien_doi(self, item):
return {'binh_phuong': item**2, 'gap_ba': item*3}
# Vì @mem_profile bao bọc hàm gốc, cần lấy __wrapped__
profiler.add_function(XuLyLon.xu_ly_khoi_lon.__wrapped__)
Chạy trên đa luồng
line_profiler không phân biệt luồng — nó tổng hợp tất cả các lần gọi hàm dù ở thread nào:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(profiler(xu_ly), data) for data in danh_sach_data]
ket_qua = [f.result() for f in futures]
Kết quả cuối cùng là tổng hợp từ mọi thread, rất hữu ích khi đánh giá hiệu năng toàn cục.
Giới hạn sử dụng
Không nên dùng line_profiler cho các hàm cực ngắn như:
def cong_1(x):
return x + 1
Do chi phí instrumentation có thể lớn hơn chính hàm đó. Công cụ này phát huy tốt nhất khi áp dụng cho các tác vụ mất từ vài mili giây trở lên.