• เป็นเบนช์มาร์กที่เปรียบเทียบการใช้หน่วยความจำของ งานพร้อมกันตั้งแต่ 1 ถึง 1 ล้านงาน โดยอิงจากภาษาและรันไทม์ล่าสุด ณ สิ้นปี 2024 และแจ้งให้ดูผลล่าสุดได้ที่หน้า Take 2 แยกต่างหาก
  • การทดสอบทั้งหมดปรับให้มีโครงสร้างเดียวกัน คือแต่ละงาน รอ 10 วินาที แล้วรอให้งานทั้งหมดเสร็จสิ้น โดยเปรียบเทียบลักษณะการใช้หน่วยความจำของ coroutine, งาน asynchronous, goroutine และ virtual thread มากกว่าการใช้หลายเธรด
  • รายการที่เปรียบเทียบได้แก่ Rust tokio·async_std, C# และ NativeAOT, NodeJS, Python asyncio, Go goroutine, Java virtual thread และ Java GraalVM native image โดยโค้ดทั้งหมดเปิดเผยบน GitHub
  • เมื่อจำนวนงานเพิ่มขึ้น ขนาดการเพิ่มขึ้นของหน่วยความจำแตกต่างกันมากในแต่ละรันไทม์ และที่ 1 ล้านงาน C# ใช้หน่วยความจำน้อยที่สุด ขณะที่ Rust ก็ยังคงให้ผลลัพธ์ที่มีประสิทธิภาพ
  • .NET รุ่นล่าสุดปรับปรุงขึ้นมาก และ NativeAOT แข่งขันกับ Rust ได้ แต่ Go goroutine ใช้หน่วยความจำมากกว่าผลลัพธ์ที่ชนะ มากกว่า 13 เท่า และมากกว่า Java มากกว่า 2 เท่า ที่ 1 ล้านงาน

วิธีเบนช์มาร์กและข้อมูลที่เปิดเผย

  • เป็นผลจากการทำการเปรียบเทียบการใช้หน่วยความจำของการเขียนโปรแกรมแบบ asynchronous ประจำปี 2023 ใหม่อีกครั้ง ด้วยเวอร์ชันภาษาล่าสุด ณ สิ้นปี 2024
  • ด้านบนมีข้อความแนะนำให้ดูผลล่าสุดที่ How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? - Take 2
  • โปรแกรมทดสอบจะสร้างงานพร้อมกันจำนวน N งานตามอาร์กิวเมนต์จาก command line และแต่ละงานจะ รอเป็นเวลา 10 วินาที จากนั้นเมื่อทุกงานเสร็จสิ้นจึงจบการทำงาน
  • จุดเน้นของการเปรียบเทียบอยู่ที่ โมเดล concurrency ตระกูล coroutine ไม่ใช่หลายเธรด
  • โค้ดเบนช์มาร์กทั้งหมดเปิดเผยไว้ที่ async-runtimes-benchmarks-2024

ภาษาและรันไทม์ที่เปรียบเทียบ

  • Rust เปรียบเทียบด้วยรันไทม์ asynchronous สองแบบคือ tokio และ async_std
    • ทั้งสองเป็นรันไทม์ asynchronous ที่ใช้กันอย่างแพร่หลายใน Rust
  • C# รองรับ async/await โดยตรง และรันงานด้วย Task.Delay กับ Task.WhenAll
    • เปรียบเทียบ NativeAOT ที่มีให้ตั้งแต่ .NET 7 ด้วย
    • NativeAOT คอมไพล์ managed code โดยตรงเป็นไบนารีสุดท้าย เพื่อให้รันได้โดยไม่ต้องใช้ VM
  • NodeJS ห่อ setTimeout ด้วย util.promisify แล้วรอด้วย Promise.all
  • Python ใช้ asyncio.sleep และ asyncio.gather
  • Go ใช้ goroutine เป็นองค์ประกอบของ concurrency และรอให้งานทั้งหมดเสร็จด้วย WaitGroup แทนการ await ทีละงาน
  • Java ใช้ virtual thread ที่มีให้ตั้งแต่ JDK 21
    • เปรียบเทียบ native image ของ GraalVM ด้วย
    • GraalVM native image ถูกนำมารวมในฐานะแนวคิดที่คล้ายกับ .NET NativeAOT

สภาพแวดล้อมการทดสอบ

  • ฮาร์ดแวร์: 13th Gen Intel Core i7-13700K
  • ระบบปฏิบัติการ: 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
  • ในกรณีที่เป็นไปได้ โปรแกรมทั้งหมดรันใน release mode
  • เนื่องจากสภาพแวดล้อมทดสอบไม่มี libicu จึงปิดการรองรับ internationalization และ globalization

การเปลี่ยนแปลงของหน่วยความจำเมื่อจำนวนงานเพิ่มขึ้น

  • ฟุตพรินต์ขั้นต่ำ: งาน 1 งาน

    • เพื่อดูหน่วยความจำที่ตัวรันไทม์เองต้องใช้ จึงเริ่มจากการรันเพียง 1 งาน ก่อน
    • Rust, C# NativeAOT และ Go ถูกคอมไพล์แบบ static เป็นไบนารี native จึงใช้หน่วยความจำน้อยมาก และให้ผลลัพธ์ใกล้เคียงกัน
    • Java GraalVM native image ก็ให้ผลลัพธ์ดีเช่นกัน แต่ใช้หน่วยความจำมากกว่าเป้าหมายที่คอมไพล์แบบ static อื่นเล็กน้อย
    • โปรแกรมที่รันบน managed platform หรือ interpreter ใช้หน่วยความจำมากกว่า
    • ในช่วงนี้ Go มีฟุตพรินต์เล็กที่สุด
    • Java GraalVM ใช้หน่วยความจำมากกว่า OpenJDK Java อย่างมาก และอาจปรับได้ด้วยการตั้งค่า
  • งาน 10,000 งาน

    • เบนช์มาร์กของ Rust ทั้งสองแบบ แม้ที่ 10,000 งาน การใช้หน่วยความจำก็ไม่ได้เพิ่มขึ้นมากจากฟุตพรินต์ขั้นต่ำ และยังคงใช้หน่วยความจำน้อยมาก
    • C# NativeAOT ก็ใช้หน่วยความจำเพียงประมาณ 10MB และตามหลัง Rust อย่างใกล้ชิด
    • การใช้หน่วยความจำของ Go เพิ่มขึ้นมากในช่วงนี้
    • virtual thread ของ Java GraalVM native image ดูเบากว่า Go goroutine
    • แม้ Go และ Java GraalVM native image จะถูกคอมไพล์แบบ static เป็นไบนารี native แต่กลับใช้ RAM มากกว่า C# ที่รันบน VM
  • งาน 100,000 งาน

    • เมื่อจำนวนงานเพิ่มเป็น 100,000 งาน การใช้หน่วยความจำของทุกภาษาเริ่มเพิ่มขึ้นอย่างมาก
    • Rust และ C# ยังให้ผลลัพธ์ดีในช่วงนี้
    • C# NativeAOT ใช้ RAM น้อยกว่า Rust และนำหน้าทุกภาษา
    • ณ จุดนี้ โปรแกรม Go ตามหลังไม่เพียงแต่ Rust แต่ยังตามหลัง Java, C# และ NodeJS ด้วย
    • ข้อยกเว้นคือ Java ที่รันบน GraalVM ไม่รวมอยู่ในกลุ่มที่ชนะ Go
  • งาน 1 ล้านงาน

    • ที่ 1 ล้านงาน C# นำหน้าภาษาอื่นทั้งหมดอย่างชัดเจน
    • Rust ยังคงให้ผลลัพธ์ที่ดีด้านประสิทธิภาพหน่วยความจำตามคาด
    • ช่องว่างระหว่าง Go กับรันไทม์อื่น ๆ ยิ่งกว้างขึ้น
    • Go ใช้หน่วยความจำมากกว่าผลลัพธ์ที่ชนะ มากกว่า 13 เท่า
    • เมื่อเทียบกับ Java แล้ว Go ก็ยังใช้หน่วยความจำ มากกว่า 2 เท่า แสดงผลลัพธ์ที่ต่างจากความเข้าใจทั่วไปว่า JVM ใช้หน่วยความจำมากและ Go เบา

ข้อสังเกตสุดท้าย

  • หากจำนวนงานพร้อมกันสูงมาก แม้แต่ละงานจะไม่ได้ทำการคำนวณที่ซับซ้อน ก็อาจใช้ หน่วยความจำจำนวนมาก ได้
  • แต่ละรันไทม์ภาษามี trade-off แตกต่างกัน
    • เมื่อจำนวนงานน้อย อาจเบาและมีประสิทธิภาพ
    • เมื่อขยายไปถึงงานหลักหลายแสนงาน ขนาดการเพิ่มขึ้นของหน่วยความจำอาจสูงได้
  • เมื่ออิงจากคอมไพเลอร์และรันไทม์ล่าสุด .NET แสดงการปรับปรุงครั้งใหญ่
  • .NET NativeAOT ให้ผลลัพธ์ที่แข่งขันกับ Rust ได้
  • GraalVM native image ของ Java ก็ให้ผลลัพธ์ดีด้านประสิทธิภาพหน่วยความจำ
  • Go goroutine ยังให้ผลลัพธ์ที่ไม่มีประสิทธิภาพในแง่การใช้ทรัพยากรอย่างต่อเนื่อง

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น