- Rust, Go, Java, C#, Python, Node.js ve Elixir dillerinde asenkron ile çoklu iş parçacığı arasındaki bellek kullanımını karşılaştırıyor
- 10 saniye bekleyen N adet görevi çalıştıran programlar her dilde yazılmış (ChatGPT yardımıyla)
- Xeon E3 + Ubuntu 22.04 üzerinde karşılaştırılmış
Sonuçlar
- En düşük ayak izi (yalnızca 1 görev test edildi): Go ve Rust 3MB'den az gerektiriyor, Python 17MB, Java/Node.js yaklaşık 40MB, C# 131MB
- 10 bin görev: Rust Tokio 4.6MB, Rust async-std 8MB, Go 28.6MB, Python 40MB, Rust Threads 48MB, Node.js 48MB, Java Virtual Thread 78MB, Elixir 99MB, C# 131MB, Java Threads 244MB
- 100 bin görev (iş parçacıkları hariç): Rust tokio 23MB, Rust Async-std 54MB, Node.js 112MB, C# 130MB, Java virtual threads 223 MB, Python 240MB, Go 269MB, Elixir 445MB
- 1 milyon görev: Rust Tokio 213MB, C# 461MB, Node.js 494MB, Rust async-std 527MB, Java virtual thread 1154MB, Python 2232MB, Go 2658MB, Elixir 4009MB
Sonuç
- Rust tokio açık ara önde
- C# ayak izi büyük olsa da oldukça rekabetçi (hatta bazen Rust'ı geçiyor)
- Go, 1 milyona çıkıldığında Java virtual threads ile arasındaki farkı açıyor (Go'nun JVM'e kıyasla daha hafif olduğu yönündeki yaygın düşünceyi tersine çeviriyor)
- Yalnızca bellek kullanımı incelendiği için diğer faktörler dikkate alınmadı
- 1 milyon görevde işleri başlatma ek yükü artıyor ve kodların çoğunun tamamlanması 12 saniyeden uzun sürüyor
- Başka benchmark'lar da çalıştırılması planlanıyor
9 yorum
Go kullanıp bir yandan da sürekli Rust’a göz atarken, bu kadar sıkı söz dizimine gerçekten alışmaya değip değmeyeceğini düşündüğünüz durumlarda, bu oldukça anlamlı bir benchmark gibi görünüyor. Go’nun OOM yüzünden çökeceği bir durumda Rust iyi dayanıyorsa... yatırım yapmaya fazlasıyla değer demektir.
Elbette Rust geliştiricisi bulmanın hâlâ çok daha zor olması da ayrı bir sorun olmaya devam ediyor ama...
Go’da her bir goroutine için ayrı bir stack (2KB) tahsis edildiğinden kullanım doğal olarak O(n) kadar artıyor; bu yüzden thread sayısı arttıkça dezavantajlı hale geldiği doğru....
Ufak bir merak konusu da şu: 10 bin thread’i aşan durumlar ne kadar sık yaşanıyor acaba? Gerçek kodun çalışmasından ziyade context switching’in daha sık gerçekleşecekmiş gibi....
Kotlin coroutine'lerinin nasıl olacağını merak ediyorum.
Elixir’in sonucu en şaşırtıcı olanı; Erlang’ın Go’dan bile daha hafif, sadece birkaç yüz kelime düzeyinde bellek kullandığını biliyordum ama...
Erlang resmi belgelerine baktığımda, tek bir Erlang süreci spawn etmek için 338 word gerektiği söyleniyor. Ayrıca 64 bit bir sistemde 1 word 8 bayt olduğuna göre, bir Erlang süreci yaklaşık 2.7KB (
338 × 8 = 2,704) bellek kaplıyor demektir. Go dilinde tek bir goroutine stack boyutunun yaklaşık 2.0KB olduğu belirtiliyor; bu durumda Erlang tarafının daha fazla bellek tükettiğini söylemek gerekir gibi görünüyor.O hâlde basit bir hesapla 1 milyon Erlang sürecinin 2.7GB bellek kullanması gerekir; ancak yukarıda tanıtılan Elixir benchmark’ında yaklaşık 4.0GB tepe bellek kullanımı gözlemlendiğine göre fazladan 1.3GB bellek kullanılmış oluyor. Basitçe hesaplarsak, bu senaryoda Erlang süreci başına yaklaşık 1.3KB ek bellek kullanıldığı anlamına geliyor. Emin değilim ama Erlang süreçlerinin sayısı belli bir eşiğin üzerine çıktığında runtime tarafında ek bellek alanı kullanımına ihtiyaç duyuluyor olabilir diye düşünüyorum.
Muhtemelen supervision tree haritasını ya da mesaj kuyruğu kapasitesini önceden ayırmaktan kaynaklanıyordur diye düşünüyorum.
Rust’ın, paradigmasından performansına kadar gerçekten harika bir dil olduğunu düşünüyorum.
Asenkron ve thread yaklaşımının karşılaştırılması; buna bir de dil çalışma zamanlarını içeren benchmark'lar eklenince, bakış açısına göre sonuçlar farklı yorumlanabilir, bu yüzden bunu referans olarak görün.
HN yorumlarına da birlikte göz atın. https://news.ycombinator.com/item?id=36024209