2 điểm bởi GN⁺ 2024-11-30 | 1 bình luận | Chia sẻ qua WhatsApp

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 tokio và chương trình dùng async_std.
    • Cả hai đều là các async runtime được dùng phổ biến trong Rust.
  • C#

    • C# hỗ trợ async/await tươ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.
  • NodeJS

    • Sử dụng Promise.all cho các tác vụ bất đồng bộ.
  • Python

    • Sử dụng mô-đun asyncio để thực hiện tác vụ bất đồng bộ.
  • Go

    • Dùng goroutine để hiện thực tính đồng thời và dùng WaitGroup để chờ tác vụ.
  • 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ặp for thay cho join_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

 
GN⁺ 2024-11-30
Ý 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.all cò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 CPU

  • Giả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ết

  • Giả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