ปริมาณหน่วยความจำที่ต้องใช้สำหรับการรันงานพร้อมกัน 1 ล้านงานในปี 2024
(hez2010.github.io)เบนช์มาร์ก
-
คอร์รูทีนคืออะไร?
- คอร์รูทีนเป็นองค์ประกอบของโปรแกรมคอมพิวเตอร์ที่สามารถหยุดและกลับมาทำงานต่อได้ โดยเป็นการขยายแนวคิดของซับรูทีนสำหรับการทำงานหลายอย่างพร้อมกันแบบร่วมมือกัน
- เหมาะสำหรับการใช้งานในองค์ประกอบของโปรแกรม เช่น งานแบบร่วมมือกัน ข้อยกเว้น event loop iterator ลิสต์อนันต์ และ pipe
-
Rust
- เขียนโปรแกรม 2 แบบ: โปรแกรมที่ใช้
tokioและasync_std - ทั้งสองเป็น asynchronous runtime ที่นิยมใช้กันทั่วไปใน Rust
- เขียนโปรแกรม 2 แบบ: โปรแกรมที่ใช้
-
C#
- C# รองรับ
async/awaitเช่นเดียวกับ Rust - ตั้งแต่ .NET 7 เป็นต้นมา มีการคอมไพล์แบบ NativeAOT ทำให้สามารถรัน managed code ได้โดยไม่ต้องมี VM
- C# รองรับ
-
NodeJS
- ใช้
Promise.allสำหรับงานแบบอะซิงโครนัส
- ใช้
-
Python
- ใช้โมดูล
asyncioเพื่อทำงานแบบอะซิงโครนัส
- ใช้โมดูล
-
Go
- ใช้ goroutine ในการทำ concurrent และใช้
WaitGroupเพื่อรอให้งานเสร็จ
- ใช้ goroutine ในการทำ concurrent และใช้
-
Java
- ตั้งแต่ JDK 21 เป็นต้นมา มี virtual thread ซึ่งเป็นแนวคิดที่คล้ายกับ goroutine
- สามารถใช้ GraalVM เพื่อสร้าง native image ได้
สภาพแวดล้อมการทดสอบ
- ฮาร์ดแวร์: 13th Gen Intel(R) Core(TM) i7-13700K
- ระบบปฏิบัติการ: 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
ผลลัพธ์
-
การใช้หน่วยความจำต่ำสุด
- Rust, C# (NativeAOT) และ Go ถูกคอมไพล์เป็น native binary จึงใช้หน่วยความจำน้อย
- Java (GraalVM native image) ก็แสดงผลได้ดีเช่นกัน แต่ยังใช้หน่วยความจำมากกว่าภาษาแบบคอมไพล์ล่วงหน้าอื่น ๆ
-
10K งาน
- Rust แทบไม่มีการเพิ่มขึ้นของการใช้หน่วยความจำ
- C# (NativeAOT) ก็ใช้หน่วยความจำน้อยเช่นกัน
- Go ใช้หน่วยความจำมากกว่าที่คาดไว้
-
100K งาน
- Rust และ C# แสดงประสิทธิภาพที่ดี
- C# (NativeAOT) ใช้หน่วยความจำน้อยกว่า Rust
-
1 ล้านงาน
- C# ทิ้งห่างทุกภาษาและใช้หน่วยความจำน้อยที่สุด
- Rust ก็มีประสิทธิภาพด้านหน่วยความจำที่ยอดเยี่ยม
- Go ใช้หน่วยความจำมากเมื่อเทียบกับภาษาอื่น
บทสรุป
- งานพร้อมกันจำนวนมากอาจใช้หน่วยความจำอย่างมาก แม้จะไม่ได้ทำงานที่ซับซ้อนก็ตาม
- การพัฒนาของ .NET และ NativeAOT นั้นโดดเด่น และ Java native image ที่สร้างด้วย GraalVM ก็มีประสิทธิภาพด้านหน่วยความจำยอดเยี่ยมเช่นกัน
- goroutine ยังคงไม่มีประสิทธิภาพในแง่การใช้ทรัพยากร
ภาคผนวก
- ใน Rust (
tokio) การใช้ลูปforแทนjoin_allช่วยลดการใช้หน่วยความจำลงได้ครึ่งหนึ่ง ทำให้ Rust กลายเป็นผู้นำอย่างชัดเจนในเบนช์มาร์กครั้งนี้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เบนช์มาร์กไม่ได้สะท้อนความแตกต่างของวิธีประมวลผลแบบอะซิงโครนัสของ Node และ Go ได้อย่างเหมาะสม โดย Node ใช้
Promise.allส่วน Go ใช้ goroutine จึงมีความแตกต่างกัน น่าสนใจหากจะเปรียบเทียบการใช้หน่วยความจำระหว่างงาน async I/O กับงานที่เป็น CPU-boundอธิบายความแตกต่างระหว่าง “งานที่รอ 10 วินาที” กับ “งานที่จะถูกปลุกหลังจาก 10 วินาที” โดยการใช้หน่วยความจำของโค้ด Go แตกต่างจากโค้ดอื่นค่อนข้างมาก
เสนอวิธีเปรียบเทียบ Go กับ Node อย่างยุติธรรม โดยใช้ goroutine สำหรับจัดตารางเวลา timer และ goroutine สำหรับจัดการสัญญาณจาก timer พร้อมตั้งข้อสังเกตว่าที่ไม่มี Bun และ Deno รวมอยู่ใน Node นั้นดูแปลก
งานพร้อมกันจำนวนมากอาจใช้หน่วยความจำมาก แต่ถ้าข้อมูลต่องานมีขนาดมากกว่าหลาย KB โอเวอร์เฮดหน่วยความจำของตัวจัดตารางเวลาก็เล็กน้อยจนแทบมองข้ามได้
การใช้หน่วยความจำอาจแตกต่างกันได้ตามนิยามของ “งานพร้อมกัน” โดยในการติดตั้งใช้งานที่มีประสิทธิภาพ งานพร้อมกัน 1M ต้องใช้ประมาณ 200MB
ชี้ว่า Go ตามหลัง Java ในด้านการใช้หน่วยความจำมากกว่า 2 เท่า และกล่าวว่าเบนช์มาร์กนี้ไม่ได้เป็นตัวแทนของโปรแกรมจริง
การเปรียบเทียบภาษาด้วยโค้ดง่าย ๆ อาจไม่ยุติธรรมกับนักพัฒนา และแนะนำให้เพิ่มงานจริงเข้าไปเพื่อวัดทั้งการใช้หน่วยความจำและความแตกต่างด้านการจัดตารางเวลา
กล่าวว่าบ่อยครั้งเบนช์มาร์กล้วนเต็มไปด้วยข้อผิดพลาด และไม่เข้าใจแรงจูงใจของคนที่เผยแพร่เบนช์มาร์กแบบนี้
มีความเป็นไปได้ว่า Java เบนช์มาร์กทำผิด เพราะไม่ได้กำหนดขนาดเริ่มต้นของ
ArrayListทำให้มีการสร้างอ็อบเจ็กต์ที่ไม่จำเป็นจำนวนมากอธิบายว่าทำไมโค้ดอะซิงก์ของ Rust จึงเสร็จเร็วกว่าที่คาด โดย
tokio::time::sleep()ติดตามเวลาที่ future ถูกสร้างขึ้น