2 điểm bởi GN⁺ 2024-11-30 | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Đây là benchmark so sánh mức sử dụng bộ nhớ của từ 1 đến 1 triệu tác vụ đồng thời theo các ngôn ngữ và runtime mới nhất vào cuối năm 2024; bài viết cũng hướng dẫn xem trang Take 2 riêng để biết kết quả mới nhất
  • Tất cả bài test được thiết kế cùng một cấu trúc: mỗi tác vụ chờ 10 giây rồi đợi toàn bộ hoàn tất; mục tiêu là so sánh đặc tính bộ nhớ của coroutine, tác vụ bất đồng bộ, goroutine và virtual thread thay vì nhiều thread
  • Đối tượng so sánh gồm Rust tokio·async_std, C# và NativeAOT, NodeJS, Python asyncio, Go goroutine, Java virtual thread, Java GraalVM native image; toàn bộ mã nguồn được công bố trên GitHub
  • Khi số lượng tác vụ tăng, mức tăng bộ nhớ khác biệt lớn theo từng runtime; ở mức 1 triệu tác vụ, C# có mức sử dụng bộ nhớ thấp nhất, trong khi Rust cũng duy trì kết quả hiệu quả
  • .NET mới nhất cho thấy cải thiện lớn và NativeAOT cạnh tranh với Rust, nhưng Go goroutine dùng nhiều bộ nhớ hơn kết quả thắng cuộc trên 13 lần và nhiều hơn Java trên 2 lần ở mức 1 triệu tác vụ

Phương pháp benchmark và tài liệu công khai

  • Đây là kết quả thực hiện lại phép so sánh mức tiêu thụ bộ nhớ của lập trình bất đồng bộ năm 2023, dùng các phiên bản ngôn ngữ mới nhất tính đến cuối năm 2024
  • Ở phần đầu có hướng dẫn xem How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? - Take 2 để biết kết quả mới nhất
  • Chương trình test tạo N tác vụ đồng thời nhận từ đối số dòng lệnh; mỗi tác vụ chờ trong 10 giây, sau đó chương trình kết thúc khi tất cả tác vụ hoàn tất
  • Trọng tâm so sánh không phải là nhiều thread mà là mô hình đồng thời kiểu coroutine
  • Toàn bộ mã benchmark được công bố tại async-runtimes-benchmarks-2024

Ngôn ngữ và runtime được so sánh

  • Rust được so sánh với hai runtime bất đồng bộ là tokioasync_std
    • Cả hai đều là runtime bất đồng bộ được dùng rộng rãi trong Rust
  • C# hỗ trợ trực tiếp async/await, chạy tác vụ bằng Task.DelayTask.WhenAll
    • NativeAOT, được cung cấp từ .NET 7, cũng được đưa vào so sánh
    • NativeAOT biên dịch trực tiếp mã quản lý thành binary cuối cùng để có thể chạy không cần VM
  • NodeJS bọc setTimeout bằng util.promisify rồi chờ bằng Promise.all
  • Python dùng asyncio.sleepasyncio.gather
  • Go dùng goroutine làm thành phần đồng thời, và chờ tất cả tác vụ hoàn tất bằng WaitGroup thay vì await từng tác vụ
  • Java dùng virtual thread, được cung cấp từ JDK 21
    • Native image của GraalVM cũng được đưa vào so sánh
    • GraalVM native image được đưa vào như một khái niệm tương tự .NET NativeAOT

Môi trường test

  • Phần cứng: 13th Gen Intel Core i7-13700K
  • Hệ điều hành: Debian GNU/Linux 12(bookworm)
  • Rust: 1.82.0
  • .NET: 9.0.100
  • Go: 1.23.3
  • Java: openjdk 23.0.1 build 23.0.1+11-39
  • Java(GraalVM): java 23.0.1 build 23.0.1+11-jvmci-b01
  • NodeJS: v23.2.0
  • Python: 3.13.0
  • Khi có thể, tất cả chương trình đều được chạy ở release mode
  • Do môi trường test không có libicu, hỗ trợ quốc tế hóa và toàn cầu hóa bị vô hiệu hóa

Thay đổi bộ nhớ khi số lượng tác vụ tăng

  • Footprint tối thiểu: 1 tác vụ

    • Để xem bộ nhớ mà bản thân runtime yêu cầu, trước tiên chỉ chạy 1 tác vụ
    • Rust, C# NativeAOT và Go được biên dịch tĩnh thành binary native nên dùng rất ít bộ nhớ và cho kết quả tương tự nhau
    • Java GraalVM native image cũng cho kết quả tốt, nhưng dùng nhiều bộ nhớ hơn một chút so với các đối tượng biên dịch tĩnh khác
    • Các chương trình chạy trên nền tảng quản lý hoặc interpreter tiêu thụ nhiều bộ nhớ hơn
    • Ở khoảng này, Go có footprint nhỏ nhất
    • Java GraalVM dùng nhiều bộ nhớ hơn đáng kể so với OpenJDK Java, và có thể điều chỉnh được bằng cấu hình
  • 10 nghìn tác vụ

    • Hai benchmark của Rust hầu như không tăng đáng kể mức sử dụng bộ nhớ so với footprint tối thiểu ngay cả ở 10 nghìn tác vụ, và vẫn duy trì mức bộ nhớ rất thấp
    • C# NativeAOT cũng chỉ dùng khoảng 10MB bộ nhớ, bám sát Rust
    • Mức sử dụng bộ nhớ của Go tăng mạnh ở khoảng này
    • Virtual thread của Java GraalVM native image có vẻ nhẹ hơn Go goroutine
    • Go và Java GraalVM native image đều được biên dịch tĩnh thành binary native, nhưng lại dùng nhiều RAM hơn C# chạy trên VM
  • 100 nghìn tác vụ

    • Khi số lượng tác vụ tăng lên 100 nghìn, mức tiêu thụ bộ nhớ của tất cả ngôn ngữ bắt đầu tăng mạnh
    • Rust và C# vẫn cho kết quả tốt ở khoảng này
    • C# NativeAOT dùng ít RAM hơn Rust và vượt lên trước tất cả ngôn ngữ
    • Tại thời điểm này, chương trình Go không chỉ thua Rust mà còn thua cả Java, C# và NodeJS
    • Ngoại lệ là Java chạy trên GraalVM không nằm trong nhóm đánh bại Go
  • 1 triệu tác vụ

    • Ở mức 1 triệu tác vụ, C# vượt rõ rệt tất cả ngôn ngữ khác
    • Rust tiếp tục cho kết quả tốt về hiệu quả bộ nhớ như kỳ vọng
    • Khoảng cách giữa Go và các runtime khác càng lớn hơn
    • Go dùng nhiều bộ nhớ hơn kết quả thắng cuộc trên 13 lần
    • Ngay cả khi so với Java, Go cũng dùng nhiều bộ nhớ hơn trên 2 lần, cho thấy kết quả trái với nhận thức phổ biến rằng JVM dùng nhiều bộ nhớ còn Go thì nhẹ

Quan sát cuối cùng

  • Khi số lượng tác vụ đồng thời rất lớn, chúng có thể dùng một lượng bộ nhớ đáng kể ngay cả khi từng tác vụ không thực hiện phép tính phức tạp
  • Mỗi runtime ngôn ngữ có các trade-off khác nhau
    • Với số tác vụ ít, chúng có thể nhẹ và hiệu quả
    • Khi mở rộng lên hàng trăm nghìn tác vụ, mức tăng bộ nhớ có thể trở nên lớn
  • Theo các compiler và runtime mới nhất, .NET cho thấy cải thiện lớn
  • .NET NativeAOT cho kết quả đủ sức cạnh tranh với Rust
  • Java GraalVM native image cũng cho kết quả tốt về hiệu quả bộ nhớ
  • Go goroutine tiếp tục cho kết quả kém hiệu quả về mức tiêu thụ tài nguyên

Chưa có bình luận nào.

Chưa có bình luận nào.