หน่วยความจำที่ต้องใช้เพื่อรันงานพร้อมกัน 1 ล้านงานในปี 2024
(hez2010.github.io)- เป็นเบนช์มาร์กที่เปรียบเทียบการใช้หน่วยความจำของ งานพร้อมกันตั้งแต่ 1 ถึง 1 ล้านงาน โดยอิงจากภาษาและรันไทม์ล่าสุด ณ สิ้นปี 2024 และแจ้งให้ดูผลล่าสุดได้ที่หน้า Take 2 แยกต่างหาก
- การทดสอบทั้งหมดปรับให้มีโครงสร้างเดียวกัน คือแต่ละงาน รอ 10 วินาที แล้วรอให้งานทั้งหมดเสร็จสิ้น โดยเปรียบเทียบลักษณะการใช้หน่วยความจำของ coroutine, งาน asynchronous, goroutine และ virtual thread มากกว่าการใช้หลายเธรด
- รายการที่เปรียบเทียบได้แก่ Rust
tokio·async_std, C# และ NativeAOT, NodeJS, Pythonasyncio, 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 ยังให้ผลลัพธ์ที่ไม่มีประสิทธิภาพในแง่การใช้ทรัพยากรอย่างต่อเนื่อง
ยังไม่มีความคิดเห็น