2 puan yazan GN⁺ 2024-11-30 | Henüz yorum yok. | WhatsApp'ta paylaş
  • 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 tokio ve async_std, C# ve NativeAOT, NodeJS, Python asyncio, 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ığı N adet 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, tokio ve async_std olmak ü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/await desteği sunuyor; görevler Task.Delay ve Task.WhenAll ile ç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.promisify ile sarmalayıp Promise.all ile bekliyor
  • Python, asyncio.sleep ve asyncio.gather kullanıyor
  • Go, eşzamanlılık bileşeni olarak goroutine kullanıyor; tek tek await etmek yerine tüm görevlerin tamamlanmasını WaitGroup ile 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 libicu bulunmadığı 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.

Henüz yorum yok.