"Chương trình chạy chậm." Câu này vô dụng cho việc sửa, vì "chậm" có ít nhất ba nguyên nhân hoàn toàn khác nhau: nó tính toán nhiều (nghẽn CPU), nó chờ thứ gì đó (I/O, mạng, khoá), hay nó gọi kernel quá nhiều (syscall). Ba nguyên nhân này cần ba cách sửa khác nhau — và may thay, một công cụ có sẵn trên mọi máy Unix cho bạn biết ngay là cái nào: time. Nó in ba con số real, user, sys, và tỉ lệ giữa chúng là dấu vân tay chẩn đoán. Bài này (phần 6 loạt Debug) chạy thật ba kiểu chương trình để đọc ba con số này.
Ba con số nghĩa là gì
time ./chuong-trinh
# real thời gian THẬT trôi qua (đồng hồ tường) — bạn ngồi chờ bấy nhiêu
# user thời gian CPU chạy CODE của bạn (user space)
# sys thời gian CPU chạy trong KERNEL (syscall làm hộ bạn)
real(wall clock): tổng thời gian từ lúc bắt đầu tới lúc kết thúc — thời gian bạn thực sự chờ.user: thời gian CPU tiêu cho code của bạn chạy trong user space (vòng lặp, tính toán).sys: thời gian CPU tiêu trong kernel thay mặt bạn (thực thi syscall: đọc/ghi, cấp bộ nhớ).
Quan hệ then chốt: user + sys = tổng thời gian CPU thực sự làm việc. Còn real có thể lớn hơn nhiều — phần dôi ra là thời gian chờ (CPU ngồi không, đợi đĩa/mạng/khoá).

Hình 1: time cho real (đồng hồ tường), user (CPU chạy code bạn), sys (CPU trong kernel); ba dấu vân tay: user≈real = CPU-bound, real≫user+sys = đang chờ, sys cao = quá nhiều syscall.
Đo thật: ba chương trình, ba dấu vân tay
Mình chạy ba chương trình cùng "tốn thời gian" nhưng vì lý do khác nhau:

Hình 2: Chạy thật — (a) CPU-bound: real=1.021s user=1.020s (user≈real, nghẽn CPU); (b) sleep-bound: real=2.019s user=0.006s sys=0.003s (real≫CPU, đang chờ); (c) syscall-heavy: real=0.094s user=0.041s sys=0.053s (sys > user, nhiều syscall).
- (a) CPU-bound — vòng lặp tính toán 30 triệu lần:
user=1.020s ≈ real=1.021s,sys≈0. Toàn bộ thời gian là CPU chạy code của bạn. Nghẽn ở thuật toán → cần tối ưu code hoặc profiling CPU (bài sau) để tìm hàm nóng. - (b) sleep-bound —
sleep 2s:real=2.019snhưnguser+sys ≈ 0. Chương trình chờ 2 giây mà không dùng CPU. Đây là dấu của đang chờ — I/O, mạng, database, khoá cũng cho dấu này. Sửa CPU vô ích; phải tìm cái gì đang chặn (strace, đo đĩa/DB). - (c) syscall-heavy — 500k lần
writevào/dev/null:sys=0.053s > user=0.041s. Phần lớn CPU nằm trong kernel, làm syscall. Nghẽn ở số lượng syscall → giảm chúng (gom I/O), đúng như bàistrace -c.
Cùng một triệu chứng "chậm", ba nguyên nhân, ba hướng sửa — và tỉ lệ real : user : sys chỉ đúng hướng ngay lập tức, trước khi bạn đào sâu.
Đánh đổi cần cân nhắc
real < user là bình thường trên đa lõi. Nếu chương trình chạy song song nhiều luồng trên nhiều lõi, user (cộng thời gian CPU của mọi lõi) có thể lớn hơn real. Ví dụ real=2s user=8s nghĩa là dùng khoảng 4 lõi trong 2 giây. Đây không phải lỗi — nó cho biết mức độ song song. Ngược lại, chương trình một luồng CPU-bound có user ≈ real (không tận dụng đa lõi — có thể là cơ hội tối ưu).
time là bước sàng lọc đầu, không phải câu trả lời cuối. Nó cho biết loại vấn đề (CPU/chờ/syscall) nhưng không cho biết chỗ nào trong code. Sau khi time chỉ ra CPU-bound, cần profiling CPU (perf/pprof) để tìm hàm nóng; chỉ ra đang chờ thì cần strace/đo I/O để tìm cái đang chặn. Dùng time để chọn công cụ đào sâu tiếp theo.
Phân biệt time builtin của shell và /usr/bin/time. Shell có time dựng sẵn (in real/user/sys cơ bản), còn /usr/bin/time -v (GNU) cho nhiều hơn: RSS đỉnh, số page fault, số lần chuyển ngữ cảnh — hữu ích để chẩn đoán sâu hơn. Nếu cần thông tin bộ nhớ/context switch cùng lúc, dùng /usr/bin/time -v.
Ba ý mang về
real/user/sysphân loại "chậm": đo thật, CPU-bound chouser≈real(1.020≈1.021s), sleep-bound choreal≫user+sys(2.019 vs ~0), syscall-heavy chosys>user(0.053 vs 0.041) — ba nguyên nhân khác nhau từ cùng triệu chứng.- Tỉ lệ chỉ đúng hướng sửa:
user≈real→ tối ưu thuật toán/profiling CPU;real≫CPU→ tìm cái đang chờ (I/O/mạng/khoá);syscao → giảm syscall (gom I/O). timelà bước sàng lọc: nó cho biết loại vấn đề chứ không chỗ nào trong code;real<userlà bình thường trên đa lõi; dùng/usr/bin/time -vkhi cần thêm RSS/page fault/context switch.
Nguồn
- man7.org — time(1): https://man7.org/linux/man-pages/man1/time.1.html
- man7.org — times(2) (user/sys time): https://man7.org/linux/man-pages/man2/times.2.html
- Brendan Gregg — Linux Performance: https://www.brendangregg.com/linuxperf.html
Phần sau ta dùng tín hiệu để debug: gửi SIGQUIT để một chương trình Go tự in stack tất cả goroutine, và core dump — ảnh chụp bộ nhớ lúc crash để mổ xẻ sau bằng gdb.