2024’te 1 milyon eşzamanlı görevi çalıştırmak için gereken bellek miktarı
(hez2010.github.io)- 2024 sonu itibarıyla güncel dil ve runtime’lar temel alınarak 1’den 1 milyona kadar eşzamanlı görev için bellek kullanımını karşılaştıran bir benchmark; en yeni sonuçlar için ayrı Take 2 sayfasına bakılması öneriliyor
- Tüm testler, her görevin 10 saniye beklediği ve ardından tümünün tamamlanmasının beklendiği aynı yapıya göre hazırlandı; birden çok thread yerine coroutine, asenkron görev, goroutine ve virtual thread’lerin bellek özellikleri karşılaştırıldı
- Karşılaştırmaya Rust
tokioveasync_std, C# ve NativeAOT, NodeJS, Pythonasyncio, Go goroutine, Java virtual thread ve Java GraalVM native image dahil edildi; tüm kod GitHub’da yayımlandı - Görev sayısı arttıkça runtime’lara göre bellek artış miktarı ciddi biçimde ayrıştı; 1 milyon görevde en düşük bellek kullanımını C# gösterdi, Rust da verimli sonuçlarını korudu
- Güncel .NET büyük iyileşme gösterdi ve NativeAOT Rust ile rekabet etti; ancak Go goroutine, 1 milyon görevde kazanan sonuca göre 13 kattan fazla, Java’ya göre ise 2 kattan fazla bellek kullandı
Benchmark yöntemi ve yayımlanan materyaller
- 2024 sonu itibarıyla güncel dil sürümleriyle, 2023’teki asenkron programlama bellek tüketimi karşılaştırmasının yeniden yapılmış sonuçlarıdır
- Üst bölümde en yeni sonuçları görmek için How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? - Take 2 sayfasına bakılması gerektiği belirtiliyor
- Test programı, komut satırı argümanı olarak aldığı
Nadet eşzamanlı görev oluşturuyor; her görev 10 saniye boyunca bekliyor ve tüm görevler bittiğinde program sonlanıyor - Karşılaştırmanın odağı birden çok thread değil, coroutine ailesindeki eşzamanlılık modelleri
- Benchmark’ın tüm kodu async-runtimes-benchmarks-2024 üzerinde yayımlandı
Karşılaştırılan diller ve runtime’lar
- Rust,
tokioveasync_stdolmak üzere iki farklı asenkron runtime ile karşılaştırıldı- İkisi de Rust’ta yaygın kullanılan asenkron runtime’lar
- C# doğrudan
async/awaitdesteği sunuyor; görevlerTask.DelayveTask.WhenAllile çalıştırıldı- .NET 7’den beri sunulan NativeAOT de karşılaştırmaya dahil edildi
- NativeAOT, managed code’u VM olmadan çalıştırılabilmesi için doğrudan nihai binary’ye derliyor
- NodeJS,
setTimeout’ıutil.promisifyile sarmalayıpPromise.allile bekliyor - Python,
asyncio.sleepveasyncio.gatherkullanıyor - Go, eşzamanlılık bileşeni olarak goroutine kullanıyor; tek tek await etmek yerine tüm görevlerin tamamlanmasını
WaitGroupile bekliyor - Java, JDK 21’den beri sunulan virtual thread kullanıyor
- GraalVM’in native image’i de karşılaştırmaya dahil edildi
- GraalVM native image, .NET NativeAOT’ye benzer bir kavram olarak dahil edildi
Test ortamı
- Donanım: 13th Gen Intel Core i7-13700K
- İşletim sistemi: 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
- Mümkün olduğunda tüm programlar release mode ile çalıştırıldı
- Test ortamında
libicubulunmadığı için uluslararasılaştırma ve globalleştirme desteği devre dışı bırakıldı
Görev sayısı arttığında bellekteki değişim
-
Minimum ayak izi: 1 görev
- Runtime’ın kendisinin gerektirdiği belleği görmek için önce yalnızca 1 görev çalıştırıldı
- Rust, C# NativeAOT ve Go statik olarak native binary’ye derlendiği için çok az bellek kullandı ve birbirine benzer sonuçlar gösterdi
- Java GraalVM native image de iyi sonuç verdi, ancak diğer statik derleme hedeflerine göre biraz daha fazla bellek kullandı
- Managed platformlar veya interpreter üzerinde çalışan programlar daha fazla bellek tüketti
- Bu aralıkta en küçük ayak izini Go gösterdi
- Java GraalVM, OpenJDK Java’dan çok daha fazla bellek kullandı; bunun ayarlarla değiştirilebilmesi mümkün olabilir
-
10 bin görev
- Rust’ın iki benchmark’ı, 10 bin görevde bile minimum ayak izine kıyasla bellek kullanımını çok artırmadı ve çok düşük bellek kullanımını korudu
- C# NativeAOT de yaklaşık 10MB bellek kullanarak Rust’ı yakından izledi
- Go’nun bellek kullanımı bu aralıkta ciddi biçimde arttı
- Java GraalVM native image’in virtual thread’leri, Go goroutine’den daha hafif görünüyor
- Go ve Java GraalVM native image statik olarak native binary’ye derlenmiş olsa da VM üzerinde çalışan C#’tan daha fazla RAM kullandı
-
100 bin görev
- Görev sayısı 100 bine çıktığında tüm dillerin bellek tüketimi belirgin biçimde artmaya başladı
- Rust ve C# bu aralıkta da iyi sonuçlar verdi
- C# NativeAOT, Rust’tan daha az RAM kullanarak tüm dillerin önüne geçti
- Go programı bu noktada yalnızca Rust’ın değil Java, C# ve NodeJS’in de gerisine düştü
- İstisna olarak GraalVM üzerinde çalıştırılan Java, Go’yu geçenler arasında yer almadı
-
1 milyon görev
- 1 milyon görevde C# diğer tüm dillerin açık ara önüne geçti
- Rust, beklendiği gibi bellek verimliliği açısından iyi sonuçlarını sürdürdü
- Go ile diğer runtime’lar arasındaki fark daha da büyüdü
- Go, kazanan sonuca göre 13 kattan fazla bellek kullandı
- Java ile karşılaştırıldığında da Go 2 kattan fazla bellek kullandı; bu da JVM’in çok bellek kullandığı, Go’nun ise hafif olduğu yönündeki genel algıdan farklı bir sonuç ortaya koydu
Son gözlemler
- Eşzamanlı görev sayısı çok yüksek olduğunda, her görev karmaşık hesaplama yapmasa bile kayda değer bellek kullanabilir
- Dil runtime’larına göre trade-off’lar farklı biçimde ortaya çıkıyor
- Az görev sayısında hafif ve verimli olabilirler
- Yüz binlerce göreve ölçeklendiğinde bellek artış miktarı büyüyebilir
- Güncel compiler ve runtime’lar temel alındığında .NET büyük bir iyileşme gösteriyor
- .NET NativeAOT, Rust ile rekabetçi sonuçlar veriyor
- Java’nın GraalVM native image’i de bellek verimliliğinde iyi sonuçlar veriyor
- Go goroutine, kaynak tüketimi açısından sürekli verimsiz sonuçlar gösteriyor
Henüz yorum yok.