Tại sao code của bạn chạy chậm? Ngừng đoán mò, hãy đo lường
Kịch bản này chắc chắn rất quen thuộc: Bạn viết một script xử lý dữ liệu, chạy thử với 10 bản ghi thì mượt mà. Nhưng khi quăng vào production với 1 triệu bản ghi, hệ thống bỗng dưng “đứng hình”. Phản xạ tự nhiên là lao vào sửa ngay đoạn code nào đó bạn “cảm thấy” là chậm. Hồi mới vào nghề mình cũng vậy. Kết quả thường là sửa chỗ này hỏng chỗ kia, mà tốc độ vẫn dậm chân tại chỗ.
Donald Knuth từng nói: “Tối ưu hóa sớm là nguồn cơn của mọi rắc rối”. Thay vì đoán, chúng ta cần Profiling. Đây là kỹ thuật phân tích để xác định chính xác hàm nào tốn thời gian nhất hoặc dòng code nào đang ngốn CPU.
Dưới đây là hai công cụ “gối đầu giường” của mình: cProfile (có sẵn trong Python) và Py-Spy (cực mạnh cho các ứng dụng đang chạy thực tế).
Cài đặt công cụ để thực chiến
Với cProfile, bạn không cần cài thêm gì vì nó là thư viện tiêu chuẩn. Tuy nhiên, để đọc kết quả trực quan bằng biểu đồ, hãy cài thêm snakeviz.
Nếu cần soi các ứng dụng đang chạy mà không muốn làm gián đoạn, Py-Spy là lựa chọn số một. Nó được viết bằng Rust, cực nhẹ và gần như không gây overhead cho hệ thống.
# Cài đặt Py-Spy để theo dõi tiến trình
pip install py-spy
# Cài đặt snakeviz để xem biểu đồ trực quan
pip install snakeviz
Lưu ý: Trên Linux hoặc macOS, py-spy thường yêu cầu quyền sudo để can thiệp vào các process đang chạy.
Sử dụng cProfile để soi từng hàm trong code
cProfile là dạng deterministic profiler. Nó ghi lại mọi sự kiện gọi hàm với độ chi tiết cực cao. Nhược điểm duy nhất là nó có thể làm code chạy chậm lại khoảng 2-5 lần do phải ghi log liên tục.
Cách 1: Chạy trực tiếp từ Command Line
Giả sử bạn có file heavy_script.py. Thay vì chạy theo cách thông thường, hãy dùng lệnh:
python -m cProfile -s cumulative heavy_script.py
Flag -s cumulative giúp sắp xếp kết quả theo tổng thời gian chạy của hàm và các hàm con. Bạn cần chú ý các thông số sau:
- ncalls: Số lần hàm được gọi.
- tottime: Thời gian thực thi tại chính hàm đó.
- cumtime: Tổng thời gian tính từ lúc vào hàm đến khi thoát ra.
Cách 2: Nhúng vào code để kiểm tra đoạn logic hẹp
Nếu chỉ muốn test một hàm xử lý logic phức tạp, hãy dùng Profile object trực tiếp trong code:
import cProfile
import pstats
def process_data():
# Giả sử đây là đoạn code xử lý nặng với 10 triệu phần tử
return sum([i**2 for i in range(10000000)])
with cProfile.Profile() as pr:
process_data()
stats = pstats.Stats(pr)
stats.sort_stats(pstats.SortKey.TIME).print_stats(10)
Có lần mình debug script xử lý văn bản, cứ đinh ninh regex sai gây loop vô tận. Khi soi bằng cProfile, mình mới ngã ngửa vì lỗi nằm ở việc mở/đóng file quá nhiều lần trong vòng lặp. Tiện đây, nếu cần test nhanh regex, mình hay dùng regex tester tại toolcraft.app để tiết kiệm thời gian.
Py-Spy: Giải pháp cho ứng dụng đang chạy (Production)
Điểm yếu của cProfile là bạn phải khởi động lại script. Nhưng nếu API FastAPI của bạn đang chạy và CPU nhảy lên 100%, bạn không thể dừng nó để debug. Đây chính là lúc Py-Spy tỏa sáng.
Theo dõi live như lệnh ‘top’
Bạn có thể soi trực tiếp hàm nào đang ngốn CPU thông qua PID (Process ID):
# Tìm PID của tiến trình python
ps aux | grep python
# Xem live monitoring
py-spy top --pid 1234
Màn hình sẽ hiển thị danh sách các hàm đang chiếm tài nguyên theo thời gian thực, tương tự như lệnh top của hệ điều hành.
Tạo Flame Graph – Nhìn phát biết ngay nút thắt
Flame Graph (biểu đồ ngọn lửa) là cách nhanh nhất để tìm bottleneck. Khối nào càng rộng, hàm đó càng tốn nhiều thời gian thực thi.
py-spy record -o profile.svg --pid 1234
Sau khi chạy khoảng 30 giây, hãy nhấn Ctrl+C. Mở file profile.svg bằng trình duyệt, bạn sẽ thấy toàn cảnh bức tranh hiệu năng. Mình từng dùng biểu đồ này để chứng minh với sếp rằng code chậm do database query thiếu index, chứ không phải do logic Python.
Kinh nghiệm xử lý sau khi tìm ra nguyên nhân
Có số liệu rồi, bước tiếp theo là tối ưu. Dưới đây là 3 lỗi phổ biến mình thường gặp:
1. Tra cứu dữ liệu sai cấu trúc
Nếu thấy ncalls lớn và tottime cao trong các hàm tìm kiếm, hãy kiểm tra lại cấu trúc dữ liệu. Chuyển từ tìm kiếm trong list ($O(n)$) sang set hoặc dict ($O(1)$) có thể giúp code nhanh hơn hàng trăm lần.
2. Dùng sai thư viện
Xử lý mảng lớn bằng List thuần của Python rất chậm. Hãy cân nhắc dùng NumPy hoặc Pandas. Những thư viện này chạy trên nền C/C++, hiệu suất vượt trội hoàn toàn so với vòng lặp Python thông thường.
3. Nhầm lẫn giữa I/O Bound và CPU Bound
Nếu cumtime lớn nhưng tottime cực nhỏ, code của bạn đang phải chờ đợi I/O (gọi API, đọc DB). Lúc này, tối ưu thuật toán CPU sẽ vô ích. Bạn cần chuyển sang dùng asyncio hoặc tăng số lượng worker để tận dụng thời gian chờ.
Profiling không phải việc làm một lần rồi thôi. Hãy biến nó thành thói quen mỗi khi thêm tính năng lớn. Chỉ mất 15 phút đo lường, bạn sẽ tiết kiệm được hàng giờ đồng hồ sửa code vô ích.
