Dung lượng bộ nhớ cần thiết để chạy 1 triệu tác vụ đồng thời trong năm 2024
(hez2010.github.io)Benchmark
-
Coroutine là gì?
- Coroutine là một thành phần của chương trình máy tính có thể tạm dừng và tiếp tục việc thực thi của chương trình, là sự khái quát hóa của subroutine cho đa nhiệm hợp tác.
- Phù hợp để hiện thực các thành phần chương trình như tác vụ hợp tác, ngoại lệ, event loop, iterator, danh sách vô hạn và pipe.
-
Rust
- Viết hai chương trình: chương trình dùng
tokiovà chương trình dùngasync_std. - Cả hai đều là các async runtime được dùng phổ biến trong Rust.
- Viết hai chương trình: chương trình dùng
-
C#
- C# hỗ trợ
async/awaittương tự Rust. - Từ .NET 7, có hỗ trợ biên dịch NativeAOT, cho phép chạy managed code mà không cần VM.
- C# hỗ trợ
-
NodeJS
- Sử dụng
Promise.allcho các tác vụ bất đồng bộ.
- Sử dụng
-
Python
- Sử dụng mô-đun
asynciođể thực hiện tác vụ bất đồng bộ.
- Sử dụng mô-đun
-
Go
- Dùng goroutine để hiện thực tính đồng thời và dùng
WaitGroupđể chờ tác vụ.
- Dùng goroutine để hiện thực tính đồng thời và dùng
-
Java
- Từ JDK 21, Java cung cấp virtual thread, là một khái niệm tương tự goroutine.
- Có thể dùng GraalVM để tạo native image.
Môi trường thử nghiệm
- Phần cứng: Intel(R) Core(TM) i7-13700K thế hệ 13
- 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
- Java (GraalVM): java 23.0.1
- NodeJS: v23.2.0
- Python: 3.13.0
Kết quả
-
Mức sử dụng bộ nhớ tối thiểu
- Rust, C# (NativeAOT) và Go được biên dịch thành binary native nên dùng ít bộ nhớ.
- Java (native image GraalVM) cũng cho kết quả tốt, nhưng vẫn dùng nhiều bộ nhớ hơn các ngôn ngữ biên dịch tĩnh khác.
-
10K tác vụ
- Mức dùng bộ nhớ của Rust gần như không tăng.
- C# (NativeAOT) cũng dùng ít bộ nhớ.
- Go dùng nhiều bộ nhớ hơn dự kiến.
-
100K tác vụ
- Rust và C# cho kết quả tốt.
- C# (NativeAOT) dùng ít bộ nhớ hơn Rust.
-
1 triệu tác vụ
- C# vượt trội hơn tất cả các ngôn ngữ khác và dùng ít bộ nhớ nhất.
- Rust cũng có hiệu quả bộ nhớ rất cao.
- Go dùng nhiều bộ nhớ hơn so với các ngôn ngữ khác.
Kết luận
- Nhiều tác vụ đồng thời có thể tiêu tốn một lượng bộ nhớ đáng kể ngay cả khi không thực hiện công việc phức tạp.
- Những cải thiện của .NET và NativeAOT rất nổi bật, và Java native image được build bằng GraalVM cũng có hiệu quả bộ nhớ rất cao.
- Goroutine vẫn kém hiệu quả về mặt tiêu thụ tài nguyên.
Phụ lục
- Trong Rust (
tokio), việc dùng vòng lặpforthay chojoin_allđã giúp giảm một nửa mức dùng bộ nhớ. Rust là quán quân tuyệt đối trong benchmark lần này.
1 bình luận
Ý kiến trên Hacker News
Benchmark không phản ánh đúng khác biệt trong cách xử lý bất đồng bộ của Node và Go. Node dùng
Promise.allcòn Go dùng goroutine, nên có sự khác biệt. Sẽ thú vị hơn nếu so sánh mức sử dụng bộ nhớ giữa I/O bất đồng bộ và tác vụ bị ràng buộc bởi CPUGiải thích sự khác biệt giữa "tác vụ chờ trong 10 giây" và "tác vụ được đánh thức sau 10 giây". Mức sử dụng bộ nhớ của mã Go khác biệt đáng kể so với các mã khác
Đề xuất cách so sánh công bằng giữa Go và Node bằng cách dùng goroutine để lên lịch timer và goroutine để xử lý tín hiệu timer. Cũng nhắc rằng việc không đưa Bun và Deno vào cùng Node là điều khá lạ
Nhiều tác vụ đồng thời có thể tiêu thụ nhiều bộ nhớ, nhưng nếu dữ liệu mỗi tác vụ từ vài KB trở lên thì phần overhead bộ nhớ của scheduler gần như có thể bỏ qua
Mức sử dụng bộ nhớ có thể thay đổi tùy theo định nghĩa của "tác vụ đồng thời". Với cách triển khai hiệu quả, 1 triệu tác vụ đồng thời cần khoảng 200MB
Chỉ ra rằng Go thua Java hơn 2 lần về mức sử dụng bộ nhớ, đồng thời nói benchmark này không đại diện cho các chương trình thực tế
So sánh ngôn ngữ bằng đoạn mã đơn giản có thể không công bằng với lập trình viên, và khuyến nghị thêm tác vụ thực tế để đo mức sử dụng bộ nhớ cùng khác biệt trong lập lịch
Nói rằng benchmark thường đầy lỗi và họ không hiểu động cơ của những người đăng các benchmark như vậy
Có khả năng benchmark Java bị sai, vì không chỉ định kích thước khởi tạo cho
ArrayList, dẫn đến tạo ra nhiều đối tượng không cần thiếtGiải thích lý do mã bất đồng bộ của Rust hoàn thành nhanh hơn dự kiến. Vì
tokio::time::sleep()theo dõi thời điểm future được tạo ra