2 điểm bởi GN⁺ 2026-01-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • Bài viết kiểm chứng bằng thực nghiệm về sự mất cân đối giữa hiệu năng I/O và tốc độ xử lý của CPU được bàn luận gần đây, và cho thấy trên thực tế CPU vẫn là giới hạn chính
  • Tốc độ đọc tuần tự đạt 1.6GB/s khi cache lạnh và 12.8GB/s khi cache nóng, nhưng phép tính tần suất từ trên một luồng chỉ dừng ở mức 278MB/s
  • Cấu trúc nhánh (branch) trong mã cản trở quá trình vector hóa (vectorization), và ngay cả khi chỉ tối ưu việc chuyển chữ thường cũng chỉ cải thiện lên khoảng 330MB/s
  • Ngay cả lệnh wc -w cũng chỉ đạt 245MB/s, xác nhận rằng tính toán CPU và xử lý nhánh, chứ không phải đĩa, mới là nút thắt cổ chai
  • Vector hóa thủ công dựa trên AVX2 đã đẩy tốc độ lên 1.45GB/s, nhưng vẫn chỉ ở khoảng 11% tốc độ đọc tuần tự, qua đó chứng minh CPU chứ không phải I/O mới là nút thắt cổ chai

So sánh tốc độ I/O và hiệu năng CPU

  • Theo lập luận của Ben Hoyt, bài viết thử nghiệm xem liệu mức tăng tốc độ đọc tuần tự gần đây có vượt qua sự chững lại của tốc độ CPU hay chưa
    • Khi đo theo cùng một cách, kết quả là 1.6GB/s với cache lạnh và 12.8GB/s với cache nóng
  • Tuy nhiên, khi thực hiện phép tính tần suất từ trên một luồng, tốc độ chỉ đạt 278MB/s
    • Ngay cả khi cache đã nóng, con số này cũng chỉ bằng khoảng 1/5 tốc độ đọc từ đĩa

Thử nghiệm tính tần suất từ bằng C

  • Dùng GCC 12 để biên dịch optimized.c với tùy chọn -O3 -march=native, sau đó chạy trên tệp đầu vào 425MB
    • Kết quả: mất 1.525 giây, tốc độ xử lý 278MB/s
  • Nhiều nhánh và điểm thoát sớm trong mã đã cản trở tối ưu vector hóa của trình biên dịch
    • Sau khi đưa logic chuyển chữ thường ra ngoài vòng lặp, tốc độ tăng lên 330MB/s
    • Khi dùng Clang, việc vector hóa được thực hiện tốt hơn

So sánh với phép đếm từ đơn giản (wc -w)

  • Chạy lệnh wc -w, chỉ đếm số từ thay vì tính tần suất
    • Kết quả: 245.2MB/s, chậm hơn dự kiến
  • wc xử lý nhiều loại ký tự trắng và ký tự theo locale như ' ', '\n', '\t'
    • Vì vậy lượng phép toán nhiều hơn so với mã chỉ phân tách bằng khoảng trắng đơn giản

Thử vector hóa dựa trên AVX2

  • Tận dụng tính năng CPU hiện đại để triển khai vector hóa bằng tập lệnh AVX2
    • Sử dụng thanh ghi 256 bit, dữ liệu được căn chỉnh 32 byte
    • Dùng lệnh VPCMPEQB để so sánh ký tự trắng
  • Dùng bit mask (PMOVMSKB) và lệnh Find First Set(ffs) để phát hiện ranh giới từ
    • Ý tưởng lấy cảm hứng từ phần triển khai strlen trong Cosmopolitan libc

Kết quả hiệu năng và kết luận

  • Mã vector hóa thủ công (wc-avx2) đạt tốc độ xử lý 1.45GB/s
    • Đã xác minh cho ra cùng kết quả với wc -w (82,113,300 từ)
  • Ngay cả ở trạng thái cache lạnh, thời gian tính toán ở chế độ user vẫn chiếm ưu thế
    • Điều này xác nhận rằng CPU, chứ không phải I/O đĩa, mới là nút thắt cổ chai
  • Tổng thể, tốc độ đĩa đã đủ nhanh, nhưng xử lý nhánh và tính toán hash trên CPU vẫn là yếu tố giới hạn
  • Mã nguồn và kết quả thử nghiệm đã được công khai trên GitHub (haampie/wc-avx2)

1 bình luận

 
GN⁺ 2026-01-08
Ý kiến trên Hacker News
  • Tôi cho rằng giới hạn hiệu năng của CPU hiện đại được quyết định bởi lượng dữ liệu mà một lõi đơn có thể xử lý, tức tốc độ memcpy()
    Phần lớn lõi x86 đạt khoảng 6GB/s, còn dòng Apple M ở mức khoảng 20GB/s
    Những con số kiểu “200GB/s” trong quảng cáo chỉ là băng thông cộng gộp của toàn bộ các lõi, còn một lõi đơn vẫn quanh mức 6GB/s
    Vì vậy, dù viết parser hoàn hảo đến đâu cũng không thể vượt qua giới hạn này
    Tuy nhiên, nếu dùng định dạng zero-copy thì CPU có thể bỏ qua dữ liệu không cần thiết, nên về mặt lý thuyết có thể “vượt” 6GB/s
    Định dạng Lite³ mà tôi đang phát triển tận dụng nguyên lý này và cho hiệu năng nhanh hơn simdjson tới 120 lần

    • Tôi nghĩ các con số cho một lõi đơn được đưa ra ở đây là quá thấp
      Ví dụ, Zen 1 cho thấy 25GB/s trên một lõi đơn (liên kết tham khảo)
      Theo kết quả microbenchmark tôi tự viết, Zen 2 đạt 17GB/s khi không dùng AVX và lên tới 35GB/s khi dùng non-temporal AVX
      Trên Apple M3 Max, đo được tới 125GB/s với non-temporal NEON
      Vì vậy, các con số 6GB/s cho x86 và 20GB/s cho Apple thấp hơn thực tế rất nhiều
    • Tôi tò mò giới hạn này đến từ đâu — có phải do cấu trúc bus giữa lõi, cache và bộ điều khiển bộ nhớ hay không
    • Tôi thắc mắc vì sao dòng Apple M lại có băng thông trên mỗi lõi cao gấp 3 lần x86
    • Trên các chip hiện đại, chỉ CPU thôi thì khó bão hòa băng thông bộ nhớ, phải tận dụng iGPU mới làm được
      Vì iGPU có thể truy cập bộ nhớ hợp nhất
      Do đó, với các tác vụ như sao chép bộ nhớ dung lượng lớn, phân tích song song, nén/giải nén, việc dùng iGPU như một blitter là có lợi về mặt kỹ thuật
      Tuy nhiên, việc “bỏ qua” trong định dạng zero-copy vẫn diễn ra theo đơn vị cache line
    • Samsung quảng cáo SSD NVMe có tốc độ đọc 14GB/s, nên nếu CPU một lõi chỉ là 6GB/s thì mối quan hệ giữa hai con số này khá thú vị
  • Có vẻ tác giả bài gốc đã hiểu sai đầu ra của lệnh time
    Thời gian system là thời gian CPU mà kernel dùng thay cho tiến trình
    Trong ví dụ, nếu real là 0.395s, user là 0.196s, sys là 0.117s thì CPU tổng cộng chỉ làm việc 313ms, còn 82ms còn lại là trạng thái nhàn rỗi
    Tức là nó đúng là chạy nhanh hơn hệ thống đĩa, nhưng chênh lệch không lớn
    Ngoài ra, đường đi I/O đang ở trạng thái CPU-bound — dù đĩa và mã có nhanh vô hạn thì việc chạy mã I/O của kernel vẫn cần 117ms

  • Tôi là tác giả bài viết. Có bài tiếp theo ở đây: I/O is no longer the bottleneck, part 2

    • Trước đây tôi từng tham gia một cuộc thi đếm tần suất từ
      Bài phân tích về các kỹ thuật tối ưu khác nhau mà người tham gia sử dụng rất thú vị
      Tùy theo độ phức tạp của bài toán hay số loại ký tự trắng cần phân loại mà cách tiếp cận cũng khác nhau
    • Nếu bài kiểm thử này được chạy trên một lõi đơn thì luận điểm “giới hạn 6GB/s” ở trên coi như đã bị bác bỏ bằng thực nghiệm
  • Nút thắt hiệu năng không phải lúc nào cũng là một yếu tố đơn lẻ kiểu “CPU hay I/O”, mà là tài nguyên bão hòa đầu tiên trong khối lượng công việc thực tế
    Nó có thể là CPU, băng thông bộ nhớ, cache, đĩa, mạng, lock, độ trễ, v.v.
    Vì vậy phải đo đạc, chứng minh bằng profiling, rồi thay đổi và đo lại

  • Vấn đề không nằm ở CPU hay I/O mà là sự cân bằng giữa độ trễ (latency)thông lượng (throughput)
    Phần lớn phần mềm chậm vì bỏ qua độ trễ
    Nếu sắp xếp dữ liệu tuyến tính trong bộ nhớ, hoặc áp dụng xử lý theo lô và song song hóa thì có thể nhanh hơn rất nhiều

  • Hãy tưởng tượng một kiến trúc chỉ gồm CPU ↔ cache ↔ bộ lưu trữ không bay hơi
    Nếu mmap() có đặc tính hiệu năng giống malloc(), thì ta thậm chí có thể chỉ định bộ nhớ của chương trình bằng tên tệp và giao tính bền vững cho OS
    Rất nhiều thiết kế phần mềm vẫn còn bị trói buộc bởi các ràng buộc của thời đại ổ cứng

    • Nhưng fsync() vẫn chậm
      Muốn có tính bền vững thực sự thì dù có phải đĩa quay hay không cũng vẫn cần một cách tiếp cận khác
    • Trên Linux cũng có thể triển khai thứ gì đó tương tự
      Thực tế, phần lớn yêu cầu bộ nhớ đều diễn ra thông qua mmap()
      Chỉ là kernel khó dự đoán mẫu truy cập nên đôi khi có thể chậm hơn read/write
  • Trong môi trường cloud, hiệu năng đôi khi còn trở thành công cụ điều chỉnh giá
    Hiệu năng phần cứng đã tiến bộ đáng kinh ngạc, nhưng một số phần mềm (đặc biệt là Windows hay các ứng dụng nhắn tin) lại cho cảm giác còn chậm hơn trước

    • Thực tế, hiệu năng của instance cloud chậm hơn MacBook M1 5 lần mà vẫn đắt hơn nhiều
      Dùng làm máy trạm từ xa cho lập trình viên thì rất kém hiệu quả
    • Lý do đa số ứng dụng GUI vẫn chậm là vì vẫn phải chờ I/O
      Telegram hay FB Messenger thì nhanh, nhưng Teams hay Skype thì không
    • Màn hình CRT hiển thị dữ liệu nhanh hơn
      Một số màn hình LCD có độ trễ tới 500ms
  • Khi SSD NVMe mới ra mắt, tôi từng đùa rằng “giờ coi như ta có 2TB RAM”
    Nhưng ngày nay các máy chủ GPU thực sự đã có 2TB RAM — đúng là một kỳ tích kỹ thuật

    • Trước đây tôi từng thấy một máy chủ Epyc cũ với cấu hình 2TB DDR4 RAM giá 5.000 USD
      Giờ nghĩ lại vẫn tiếc vì đã không mua
  • Theo kinh nghiệm tối ưu hóa cơ sở dữ liệu OLAP trong môi trường đồng thời hóa cao, nút thắt cổ chai phần lớn là tốc độ bộ nhớ

  • Khái niệm nút thắt I/O ban đầu vốn không liên quan đến đọc tuần tự, mà gắn với thời gian seek
    Tôi hiểu ý chính của bài viết, nhưng vẫn muốn nhấn mạnh điểm này

    • Nhờ các công nghệ mới như CXL/PCIe, giờ đây RAM và bộ điều khiển bộ nhớ cũng có thể được xem như một dạng thiết bị I/O
    • Trong các lớp cơ sở dữ liệu ngày xưa, hiệu năng I/O được đo bằng thời gian seek của ổ cứng
      Tốc độ đọc tuần tự thì không thể cải thiện bằng mã nguồn, nên trọng tâm là tối ưu truy cập không tuần tự