30 điểm bởi GN⁺ 2026-01-15 | 5 bình luận | Chia sẻ qua WhatsApp
  • ThreadMXBean.getCurrentThreadUserTime() của OpenJDK được thay thế việc phân tích file /proc bằng lời gọi clock_gettime(), đạt cải thiện hiệu năng tối đa 400 lần
  • Cách triển khai cũ đi qua đường I/O phức tạp: mở, đọc và phân tích file /proc/self/task/<tid>/stat
  • Cách triển khai mới tận dụng mã hóa bit clockid_t của nhân Linux để điều chỉnh các bit thấp của ID lấy từ pthread_getcpuclockid(), qua đó truy vấn trực tiếp chỉ thời gian user
  • Kết quả benchmark cho thấy thời gian gọi trung bình giảm từ 11μs → 279ns; sau đó khi áp dụng fast-path của kernel thì cải thiện thêm khoảng 13%
  • Đây là ví dụ cho thấy có thể tối ưu vượt ra ngoài ràng buộc của POSIX bằng cách hiểu ABI nội bộ của Linux

Vấn đề của cách triển khai cũ

  • getCurrentThreadUserTime() mở file /proc/self/task/<tid>/stat và phân tích trường thứ 13 và 14 để tính thời gian CPU user
    • Cần nhiều bước xử lý: tạo đường dẫn file, mở file, đọc bộ đệm, phân tích chuỗi, gọi sscanf()
    • Vì tên lệnh có thể chứa dấu ngoặc, mã còn phải dùng logic phức tạp với strrchr() để tìm ký tự ) cuối cùng
  • Trong khi đó, getCurrentThreadCpuTime() chỉ cần một lời gọi duy nhất clock_gettime(CLOCK_THREAD_CPUTIME_ID)
  • Theo báo cáo lỗi năm 2018 (JDK-8210452), chênh lệch tốc độ giữa hai phương thức này lên tới 30~400 lần

So sánh đường truy cập /proc và đường clock_gettime()

  • Cách dùng /proc bao gồm nhiều system call và việc tạo chuỗi trong kernel, như open(), read(), sscanf(), close()
  • Cách dùng clock_gettime()một system call duy nhất, đọc trực tiếp giá trị thời gian từ cấu trúc sched_entity
  • Khi có tải song song, truy cập /proc bị chậm hơn do tranh chấp khóa trong kernel

Cách triển khai mới

  • Chuẩn POSIX quy định CLOCK_THREAD_CPUTIME_ID phải trả về thời gian user + system
  • Nhân Linux mã hóa loại đồng hồ trong các bit thấp của clockid_t
    • 00=PROF, 01=VIRT(chỉ user), 10=SCHED(user+system)
  • Nếu đổi các bit thấp của clockid lấy từ pthread_getcpuclockid() thành 01, có thể chuyển sang đồng hồ chỉ tính thời gian user
  • Mã mới loại bỏ I/O file và bước phân tích chuỗi, chỉ còn gọi clock_gettime() để trả về thời gian user

Kết quả đo hiệu năng

  • Trước khi sửa, thời gian gọi trung bình là 11.186μs; sau khi sửa còn 0.279μs, tức cải thiện khoảng 40 lần
    • Đo trong môi trường 16 luồng, khớp với khoảng 30~400 lần đã được báo cáo trước đó
  • Trên CPU profile, các system call liên quan đến mở/đóng file biến mất, chỉ còn lại một lời gọi clock_gettime() duy nhất

Tối ưu bổ sung bằng fast-path của kernel

  • Kernel cung cấp fast-path để truy cập trực tiếp luồng hiện tại khi clockid được mã hóa với PID=0
  • Nếu JVM tự dựng clockid thay vì gọi pthread_getcpuclockid(), rồi chèn PID=0 vào đó, có thể bỏ qua bước tra cứu radix tree
  • Khi dùng clockid được dựng thủ công, thời gian trung bình giảm từ 81.7ns → 70.8ns, tức cải thiện thêm khoảng 13%
  • Tuy nhiên, cách này phụ thuộc vào chi tiết triển khai nội bộ của kernel như kích thước clockid_t, nên có rủi ro giảm tính dễ đọc và khả năng tương thích

Kết luận và bài học

  • Việc xóa 40 dòng mã đã xóa bỏ khoảng cách hiệu năng gấp 400 lần, đạt được mà không cần tính năng kernel mới, chỉ nhờ tận dụng chi tiết của ABI hiện có
  • Bài viết nhấn mạnh giá trị của việc đọc sâu mã nguồn kernel: POSIX bảo đảm tính di động, nhưng mã kernel cho thấy giới hạn của những gì có thể làm được
  • Tầm quan trọng của việc xem xét lại các giả định cũ: phân tích /proc trước đây từng hợp lý, nhưng nay đã trở nên kém hiệu quả
  • Thay đổi này sẽ có trong JDK 26 (dự kiến phát hành vào tháng 3/2026), mang lại cải thiện hiệu năng tự động khi gọi ThreadMXBean.getCurrentThreadUserTime()

5 bình luận

 
crawler 2026-01-15

Thật ấn tượng.

Nếu nhanh hơn 2 lần thì có thể là đã làm điều gì đó thông minh, còn nếu nhanh hơn 100 lần thì chỉ là đã ngừng làm điều ngu ngốc.

Tôi nghĩ câu này không hẳn là sai hoàn toàn, nhưng trong trường hợp có liên quan đến kernel thì ngay cả việc nhận ra nguyên nhân chậm cũng hẳn đã là chuyện thực sự khó khăn.

 
[Bình luận này đã bị ẩn.]
 
princox 2026-01-19

Làm sao có thể phát hiện những thứ như thế này trong dự án nhỉ? Có vẻ như chỉ chạy AI thì cũng khó mà biết được..

Nhìn những trường hợp như vậy, tôi cũng nghĩ rằng mình muốn học hỏi và nhất định tự mình trải nghiệm thử.

 
aobamisaki 2026-01-15

Thực ra, ngay cả khi viết lại toàn bộ mã mà muốn cải thiện 2~3 lần cũng đã là việc khó rồi, nên việc chỉ đổi vài dòng mà tăng hiệu năng tối đa tới 400 lần thực sự quá ấn tượng.

 
GN⁺ 2026-01-15
Ý kiến trên Hacker News
  • Tôi là tác giả. Sau bài viết trước về lỗi kernel, tôi đã xem cách JVM tự báo cáo hoạt động của thread
    Tôi nhận ra rằng câu hỏi “thời gian CPU mà thread này đã dùng là bao nhiêu?” hóa ra là một phép toán quá đắt đỏ hơn tôi tưởng nhiều
    • Khi bàn đến phép đo ở mức nano giây, cần hiểu thật rõ về độ ổn định và độ chính xác của đồng hồ
      Nếu không có mốc chuẩn ở cấp độ đồng hồ nguyên tử, tôi nghĩ rất khó để khẳng định các con số tuyệt đối
    • Tôi tò mò không biết bạn có xem lý do vì sao phân bố lại trải rộng trên nhiều bậc độ lớn như vậy không. Bản thân đó đã là một hiện tượng thú vị
    • Tôi thật sự biết ơn phần TL;DR tóm tắt ngắn gọn. Những phần tóm tắt như vậy giúp hạ thấp rào cản tiếp cận bài viết và tạo động lực để đọc
    • Có người để lại phản hồi: “Không ngạc nhiên lắm (Quelle Surprise)”
  • clock_gettime() tránh context switch thông qua vDSO. Vì vậy dấu vết của nó cũng hiện lên trên flamegraph
    • Tuy nhiên chỉ áp dụng cho một số clock. Với các trường hợp như CLOCK_VIRT hay CLOCK_SCHED thì vẫn cần gọi syscall
    • Nếu nhìn bên dưới frame vDSO thì vẫn còn syscall. Có vẻ như đường đi nhanh (fast path) cho một số clock id cụ thể chưa được triển khai
    • CLOCK_THREAD_CPUTIME_ID rốt cuộc vẫn đi xuống kernel, vì nó cần tham chiếu đến task struct
      Có thể xem mã nguồn kernel liên quan tại posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • Dùng PERF_COUNT_SW_TASK_CLOCK thì có thể đo ở mức khoảng 8ns
    Cách làm là đọc từ shared page qua perf_event_mmap_page, rồi tính delta bằng lệnh gọi rdtsc
    Cách này chưa được tài liệu hóa tốt và gần như không có triển khai mã nguồn mở nào
    • Đây là một mẹo thực sự rất hay. Tuy vậy vì yêu cầu cấu hình perf_event và quyền hạn khá cao nên có vẻ phù hợp hơn với các thread sống lâu
    • Có người hỏi vì sao cần seqlock. Có phải để tránh context switch xảy ra giữa giá trị trong page và rdtsc hay không
      Có vẻ cấu trúc là sau rdtsc sẽ kiểm tra lại giá trị trong page, nếu đã thay đổi thì thử lại
      Nhân tiện, clock_gettime cũng là một virtual syscall dựa trên vdso
    • clock_gettime không phải syscall mà dùng vdso
  • Flamegraph thực sự là một công cụ tuyệt vời
    Chỉ nhìn code thì có vẻ ổn, nhưng khi xem flamegraph thì nhiều lúc lại thấy kiểu “Cái gì đây?!”
    Tôi đã phát hiện đủ loại vấn đề như khởi tạo không phải static initialization, hay một lệnh gọi logger chỉ một dòng nhưng lại gây ra serialization đắt đỏ
    • Tôi cũng thích icicle graph. Nó tích lũy theo hướng ngược lại với flamegraph, nên dễ nhìn ra điểm nghẽn khi nhiều đường đi cùng gọi vào một thư viện chung
    • Nếu mở ví dụ SVG này trong tab mới thì có thể zoom tương tác
    • Thử nghiệm profiling hiệu năng và tối ưu hóa là một trong những phần vui nhất của phát triển. Có rất nhiều khoảnh khắc kiểu “Tại sao cái này lại chậm thế?”
    • Cũng có ý kiến cho rằng sự kết hợp giữa phân tích chuỗi và memoization nghe hơi lạ. Thực tế vấn đề là do không cache quá trình parse các mẫu regex đắt đỏ
    • Có người hỏi về các khái niệm cơ bản và điểm khởi đầu cho người lần đầu muốn dùng flamegraph
  • Có người ngạc nhiên rằng “mở ảnh trong tab mới” lại thực sự cung cấp tương tác SVG
    • Tính năng này có được là nhờ script FlameGraph của Brendan Gregg
      Bình thường tôi dùng trình tạo HTML của async-profiler, nhưng lần này tôi dùng công cụ của Brendan để có một file SVG đơn lẻ
  • Tôi là tác giả của bản vá OpenJDK. Tôi đã đề cập đến overhead bộ nhớ khi đọc /proc, profiling bằng eBPF, và lịch sử của ABI user-space vốn thiếu tài liệu
    Chi tiết hơn tôi đã tổng hợp trong bài blog của tôi
    • Có người hỏi vì sao cách triển khai ban đầu lại như vậy. Việc làm file IO và parse chuỗi ở mỗi lần gọi là không hiệu quả, nhưng tôi nghĩ hẳn khi đó đã có lý do nào đó
    • Jaromir đọc bài của tôi rồi nói rằng “tôi cũng đã viết bản nháp vào cùng thời điểm”, và hai bên đã liên kết bài viết của nhau. Tôi rất vui khi anh ấy đánh giá bài của tôi chặt chẽ hơn
  • Chỉ vì là ngôn ngữ hệ thống như C hay C++ không có nghĩa là lúc nào cũng nhanh. Tốc độ khác biệt rất lớn tùy vào bạn đang làm gì
  • Đọc qua vDSO nhanh hơn rất nhiều vì tránh được chuyển vào kernel, serialization bộ đệm và quá trình parsing
  • Có người chia sẻ câu trích dẫn: “Nếu nhanh hơn 2 lần thì có thể là bạn đã làm điều gì đó thông minh, còn nếu nhanh hơn 100 lần thì chỉ là bạn đã ngừng làm điều ngu ngốc
    Tweet nguồn
  • Đội ngũ QuestDB là đẳng cấp hàng đầu trong lĩnh vực này. Con người cũng tuyệt vời mà phần mềm cũng vậy
    Blog của Jaromir cũng rất hay