- 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
Ý 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
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
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
Có vẻ tác giả bài gốc đã hiểu sai đầu ra của lệnh
timeThời gian
systemlà thời gian CPU mà kernel dùng thay cho tiến trìnhTrong ví dụ, nếu
reallà 0.395s,userlà 0.196s,syslà 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ỗiTứ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
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ú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) và 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ốngmalloc(), 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 OSRấ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
fsync()vẫn chậmMuố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
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/writeTrong 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
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ả
Telegram hay FB Messenger thì nhanh, nhưng Teams hay Skype thì không
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
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
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ự