Java Yığın Tahsisi C ++ 'tan Daha Hızlı


13

Bu soruyu zaten SO'ya gönderdim ve tamam. Maalesef kapatıldı (yeniden açmak için sadece bir oy gerekiyor), ancak biri buraya daha iyi bir uyum olduğu için buraya gönderdiğimi önerdi, bu yüzden aşağıdakiler kelimenin tam anlamıyla sorunun bir kopyasını


Bu cevap üzerine yorumları okuyordum ve bu alıntıyı gördüm.

Nesne örnekleme ve nesne yönelimli özelliklerin kullanımı hızlıdır (çoğu durumda C ++ 'dan daha hızlıdır) çünkü başlangıçtan itibaren tasarlanmıştır. ve Koleksiyonlar hızlı. Standart Java, en optimize C kodu için bile bu alanda standart C / C ++ 'ı geçer.

Bir kullanıcı (gerçekten yüksek rep ile ekleyebilirim) bu iddiayı cesurca savundu.

  1. Java'da yığın ayırma C ++ 's daha iyidir

  2. ve java'daki koleksiyonları savunan bu ifadeyi ekledi

    Java koleksiyonları, büyük ölçüde farklı bellek alt sistemleri nedeniyle C ++ koleksiyonlarına kıyasla hızlıdır.

Benim sorum şu, bunların herhangi biri gerçekten doğru olabilir mi ve eğer öyleyse java'nın yığın dağılımı neden bu kadar hızlı?


Sen bulabilirsiniz SO üzerinde üzerinde benzer bir soruya cevabım yararlı / alakalı.
Daniel Pryden

1
Önemsizdir: Java (veya başka bir yönetilen, kısıtlanmış ortam) ile nesneleri taşıyabilir ve işaretçileri onlara güncelleyebilirsiniz - yani, daha iyi bir önbellek konumu için dinamik olarak optimize edin. C ++ ve kontrolsüz bit yayınları olan işaretçi aritmetiği ile tüm nesneler sonsuza dek konumlarına sabitlenir.
SK-logic

3
Birisinin Java bellek yönetiminin daha hızlı olduğunu söylediğini duyduğumu hiç düşünmemiştim çünkü her zaman belleği kopyalıyor. iç çekmek.
gbjbaanb

1
@gbjbaanb, hiç bellek hiyerarşisini duydunuz mu? Önbellek kaçırma cezası? Birinci nesil tahsisat sadece tek bir ekleme operasyonu iken, genel amaçlı bir ayırıcı pahalı olduğunu biliyor musunuz?
SK-logic

1
Bu bazı durumlarda biraz doğru olsa da, java'da yığıntaki her şeyi tahsis ettiğiniz noktayı ve c ++ 'da yığın üzerinde çok daha hızlı olabilen çok fazla nesne tahsis ettiğiniz noktasını kaçırır.
JohnB

Yanıtlar:


23

Bu ilginç bir soru ve cevap karmaşık.

Genel olarak, JVM çöp toplayıcının çok iyi tasarlanmış ve son derece verimli olduğunu söylemenin adil olduğunu düşünüyorum. Muhtemelen en iyi genel amaçlı bellek yönetim sistemidir.

C ++, JVM GC'yi belirli amaçlar için tasarlanmış özel bellek ayırıcılar ile yenebilir . Örnekler şunlar olabilir:

  • Tüm bellek alanını periyodik aralıklarla silen çerçeve başına bellek ayırıcılar. Bunlar C ++ oyunlarında sıklıkla kullanılır, örneğin, geçici bellek alanı her kare için bir kez kullanılır ve hemen atılır.
  • Sabit boyutlu nesneler havuzunu yöneten özel ayırıcılar
  • Yığına dayalı ayırma (JVM'nin bunu çeşitli durumlarda, örneğin kaçış analizi yoluyla yaptığını unutmayın )

Özel bellek ayırıcılar elbette tanımla sınırlıdır. Genellikle nesne yaşam döngüsü ve / veya yönetilebilen nesne türü üzerinde kısıtlamalar vardır. Çöp toplama çok daha esnektir.

Çöp toplama ayrıca performans açısından bazı önemli avantajlar sağlar :

  • Nesne örneklemesi gerçekten son derece hızlıdır. Yeni nesnelerin bellekte sırayla tahsis edilmesinden dolayı, genellikle tipik C ++ yığın ayırma algoritmalarından daha hızlı olan birden fazla işaretçi eklemesi gerektirir.
  • Sen yaşam döngüsü yönetim maliyetleri gereksinimini ortadan (tipik çok daha GC yerine) sık artmasını beri performans açısından son derece kötü ve referans sayılarının azaltılarak performans yükü bir sürü ekler (bazen GC alternatif olarak kullanılır), örneğin referans sayma - .
  • Değişmez nesneler kullanıyorsanız, bellek tasarrufu yapmak ve önbellek verimliliğini artırmak için yapısal paylaşımdan yararlanabilirsiniz. Bu, SCA ve Clojure gibi JVM'deki fonksiyonel diller tarafından yoğun olarak kullanılmaktadır. Bunu GC olmadan yapmak çok zordur, çünkü paylaşılan nesnelerin yaşamlarını yönetmek son derece zordur. (Benim yaptığım gibi) değişmezliğin ve yapısal paylaşımın büyük eşzamanlı uygulamalar oluşturmanın anahtarı olduğuna inanıyorsanız, bu muhtemelen GC'nin en büyük performans avantajıdır.
  • Sen edebilirsiniz kopyalamayı önlemek her nesnenin türleri ve kendi kullanım ömrü aynı çöp toplama sistemi tarafından yönetildiği takdirde. Hedef farklı bir bellek yönetimi yaklaşımı gerektirdiği veya farklı bir nesne yaşam döngüsüne sahip olduğu için genellikle verilerin tam kopyalarını almanız gereken C ++ ile kontrast oluşturun.

Java GC'nin büyük bir dezavantajı vardır: çöp toplama işi ertelendiğinden ve periyodik aralıklarla iş parçalarında yapıldığından, gecikme süresini etkileyebilecek zaman zaman GC duraklamalarının çöp toplamasına neden olur . Bu genellikle tipik uygulamalar için bir sorun değildir, ancak zor gerçek zamanın gerekli olduğu durumlarda Java'yı ekarte edebilir (örn. Robotik kontrol). Yumuşak gerçek zamanlı (örneğin oyunlar, multimedya) genellikle sorun değildir.


c ++ alanında bu soruna yönelik özel kütüphaneler bulunmaktadır. Bunun en ünlü örneği SmartHeap.
Tobias Langner

5
Yumuşak-gerçek zamanlı genellikle durmak için uygun olduğunuz anlamına gelmez . Bu , dur / çökme / arıza yerine, gerçekte kötü durumda - genellikle beklenmedik - duraklatabileceğiniz / yeniden deneyebileceğiniz anlamına gelir . Kimse genellikle müzik çaları duraklatmak istemez. GC duraklaması sorunu genellikle ve öngörülemeyen bir şekilde gerçekleşmesidir . Bu şekilde, GC duraklaması yumuşak gerçek zamanlı uygulama için bile kabul edilemez. GC duraklaması yalnızca kullanıcılar uygulama kalitesini önemsemediğinde kabul edilebilir. Ve günümüzde insanlar artık o kadar saf değiller.
Eonil

1
Lütfen taleplerinizi desteklemek için bazı performans ölçümleri gönderin, aksi takdirde elma ve portakalları karşılaştırıyoruz.
JBRWilkinson

1
@Demetri Ancak gerçekte, eğer bazı pratik kısıtlamaları karşılayamazsanız , sadece durum çok fazla olursa (ve yine öngörülemez bir şekilde!). Başka bir deyişle, C ++ herhangi bir gerçek zamanlı durum için çok daha kolaydır.
Eonil

1
Tamlık için: GC performans açısından başka bir dezavantajı vardır: mevcut GC'lerin çoğunda boş bellek, farklı bir çekirdek üzerinde çalışması muhtemel olan başka bir iş parçacığında olduğu gibi, GC'lerin senkronizasyon için ciddi önbellek geçersizleştirme maliyetleri oluşturduğu anlamına gelir Farklı çekirdekler arasında L1 / L2 önbellekleri; ayrıca, ağırlıklı olarak NUMA olan sunucularda, L3 önbellekleri de senkronize edilmelidir (ve Hypertransport / QPI, ouch (!) üzerinden).
Hata Yok Tavşan

3

Bu bilimsel bir iddia değil. Ben sadece bu konuda düşünce için yiyecek veriyorum.

Görsel bir benzetme şudur: size halı kaplı bir daire (konut birimi) verilir. Halı kirli. Dairenin zeminini pırıl pırıl hale getirmenin en hızlı yolu (saat cinsinden) nedir?

Cevap: sadece eski halı rulo; fırlatmak; ve yeni bir halı serdim.

Burada neyi ihmal ediyoruz?

  • Mevcut kişisel eşyalarınızı taşımanın ve sonra taşınmanın maliyeti.
    • Bu, çöp toplamanın "dünyayı durdur" maliyeti olarak bilinir.
  • Yeni halının maliyeti.
    • Hangi, rastlantısal olarak RAM için, ücretsizdir.

Çöp toplama büyük bir konudur ve hem Programcılar'da hem de StackOverflow'da birçok soru vardır.

Bir yan konuda, nesne referans sayımı ile birlikte TCMalloc olarak bilinen bir C / C ++ tahsis yöneticisi teorik olarak herhangi bir GC sisteminin en iyi performans taleplerini karşılayabilir.


aslında c ++ 11 bile çöp toplama ABI var , bu SO var bazı cevaplar oldukça benzer
aaronman

C ++ 'da dil inovasyonunun ilerlemesini engelleyen mevcut C / C ++ programlarını (Linux çekirdeği gibi kod tabanları ve libtiff gibi archaic_but_still_economically_imortant kütüphaneleri) kırma korkusudur.
rwong

Mantıklı, c ++ 17 ile daha eksiksiz olacağını tahmin ediyorum, ama gerçekte gerçekten c ++ 'da nasıl programlanacağını öğrendikten sonra artık istemiyorsunuz, belki iki deyimi birleştirmenin bir yolunu bulabilirler güzel
aaronman

Dünyayı durdurmayan çöp toplayıcılarının olduğunu biliyor musunuz? Kompaktlaştırma (GC tarafında) ve yığın parçalanması (genel C ++ ayırıcıları için) performans sonuçlarını düşündünüz mü?
SK-logic

2
Sanırım bu benzetmedeki ana kusur, GC'nin gerçekte yaptığı şey, kirli bitleri bulmak, onları kesmek ve daha sonra yeni bir halı oluşturmak için geri kalan bitleri tekrar bir araya getirmektir.
svick

3

Bunun ana nedeni, Java'dan yeni bir bellek yığını istediğinde, doğrudan yığının sonuna gider ve size bir blok verir. Bu şekilde, bellek ayırma yığını üzerinde tahsis kadar hızlı (bu çoğu zaman C / C ++, ama bunun dışında nasıl yaparsınız ..)

Yani tahsisler her şey kadar hızlı ama ... bu, hafızayı boşaltmanın maliyetini saymaz. Daha sonraya kadar hiçbir şeyi serbest bırakmamanız çok pahalıya mal olmadığı anlamına gelmez ve GC sistemi için maliyet 'normal' yığın tahsislerinden çok daha fazladır - sadece GC, hayatta olup olmadıklarını görmek için tüm nesnelerin üzerinden geçmeli, daha sonra onları serbest bırakmalı ve (büyük maliyet) yığını sıkıştırmak için belleği kopyalamalıdır - böylece sonunda hızlı tahsis edersiniz (ya da hafızanız tükenirse, örneğin C / C ++, nesneye uyabilecek bir sonraki boş alan bloğunu arayan her ayırmada öbek üzerinde yürür).

Java / .NET testlerinin bu kadar iyi performans göstermesinin bir nedeni budur, ancak gerçek dünyadaki uygulamalar bu kadar kötü performans gösterir. Sadece telefonumdaki uygulamalara bakmam gerekiyor - gerçekten hızlı, duyarlı olanların hepsi NDK kullanılarak yazılıyor, o kadar çok şaşırdım ki.

Günümüzde koleksiyonlar, tüm nesneler yerel olarak tahsis edilirse, örneğin tek bir bitişik blokta hızlı olabilir. Java'da, nesnelerin yığının serbest ucundan birer birer tahsis edildiği için bitişik bloklar elde edemezsiniz. Onlarla mutlu bir şekilde bitişik olabilir, ancak sadece şansla (yani GC sıkıştırma rutinlerinin hevesine ve nesneleri nasıl kopyaladığına) kadar. C / C ++ ise bitişik tahsisleri açıkça destekler (açıkça, yığın üzerinden). Genellikle C / C ++ 'daki yığın nesneleri Java'nın BTW'sinden farklı değildir.

Şimdi C / C ++ ile belleği kaydetmek ve verimli kullanmak için tasarlanmış varsayılan ayırıcılardan daha iyi alabilirsiniz. Ayırıcıyı bir dizi sabit blok havuzuyla değiştirebilirsiniz, böylece her zaman ayırdığınız nesne için tam olarak doğru boyutta bir blok bulabilirsiniz. Yığın yürümek, serbest bir bloğun nerede olduğunu görmek için bir bitmap araması meselesi haline gelir ve ayırma işlemi bu bitmapte biraz yeniden ayarlanıyor. Maliyet, sabit boyutlu bloklarda ayırırken daha fazla bellek kullanmanızdır, böylece 4 baytlık bir yığın, 16 baytlık bloklar için başka bir yığınınız vardır.


2
GC'leri hiç anlamadığınız anlaşılıyor. En tipik senaryoyu düşünün - yüzlerce küçük nesne sürekli olarak tahsis edilir, ancak sadece bir düzine bir saniyeden fazla hayatta kalır. Bu şekilde, hafızayı boşaltmanın kesinlikle bir maliyeti yoktur - bu düzine genç nesilden kopyalanır (ve ek bir fayda olarak sıkıştırılır) ve geri kalanı ücretsiz olarak atılır. Ve bu arada, acıklı Dalvik GC'nin uygun JVM uygulamalarında bulacağınız modern, modern GC'lerle hiçbir ilgisi yoktur.
SK-logic

1
Bu serbest bırakılmış nesnelerden biri yığının ortasındaysa, yığının geri kalanı alanı geri kazanmak için sıkıştırılır. Veya GC sıkıştırmasının, tarif ettiğiniz en iyi durum olmadığı sürece gerçekleşmediğini mi söylüyorsunuz? Gelecek nesillerin ortasında bir nesne bırakmazsanız, kuşak GC'lerin burada çok daha iyi olduğunu biliyorum, bu durumda etki nispeten büyük olabilir. Bir Microsoftie tarafından GC üzerinde çalışan bir şey vardı, okuduğum bir kuşak GC oluştururken GC tradeoff'larını açıkladı. Onu tekrar bulabilir miyim göreceğim.
gbjbaanb

1
Hangi "yığın" dan bahsediyorsun? Çöplerin çoğu genç nesil aşamasında geri kazanılıyor ve performans avantajlarının çoğu tam olarak bu kompaktlaştırmadan geliyor. Tabii ki, çoğunlukla fonksiyonel programlama için tipik bir bellek ayırma profilinde (birçok kısa ömürlü küçük nesne) görülebilir. Ve elbette, henüz tam olarak keşfedilmemiş sayısız optimizasyon fırsatı var - örneğin, belirli bir yoldaki yığın ayırmalarını otomatik olarak yığın veya havuz ayırmalarına dönüştürebilen dinamik bir bölge analizi.
SK-logic

3
Yığın ayırmanın 'yığın kadar hızlı' olduğu iddiasına katılmıyorum - yığın ayırma iş parçacığı eşitlemesi gerektiriyor ve yığın (tanım gereği) gerektirmiyor
JBRWilkinson

1
Sanırım öyle, ama Java ve .net ile benim fikrimi görüyorsunuz - bir sonraki serbest bloğu bulmak için öbek yürümek zorunda değilsiniz, bu yüzden bu konuda önemli ölçüde daha hızlı, ama evet - haklısınız, Kilitli uygulamalara zarar verecek kilitli.
gbjbaanb

2

Eden Space

Benim sorum şu, bunların herhangi biri gerçekten doğru olabilir mi ve eğer öyleyse java'nın yığın dağılımı neden bu kadar hızlı?

Benim için çok ilginç olduğu için Java GC'nin nasıl çalıştığı hakkında biraz çalışıyorum. Her zaman C ve C ++ bellek ayırma stratejileri koleksiyonumu genişletmeye çalışıyorum (C'de benzer bir şey uygulamaya çalışmakla ilgileniyorum) ve çok sayıda nesneyi patlama biçiminde ayırmanın çok, çok hızlı bir yoludur. pratik bakış açısı, ancak esas olarak çoklu iş parçacığı nedeniyle.

Java GC tahsisinin çalışma şekli, nesneleri başlangıçta "Eden" alanına tahsis etmek için son derece ucuz bir tahsis stratejisi kullanmaktır. Söyleyebileceğim kadarıyla, sıralı bir havuz ayırıcı kullanıyor.

Bu sadece algoritma ve genel amaçlı daha zorunlu sayfa hataları azaltmak açısından bir sürü daha hızlı mallocatma, C veya varsayılan operator newC ++.

Ancak sıralı ayırıcıların göze çarpan bir zayıflığı vardır: değişken boyutlu parçalar tahsis edebilirler, ancak tek tek parçaları serbest bırakamazlar. Sadece hizalama için dolgu ile düz sıralı bir şekilde tahsis ederler ve sadece bir kerede tahsis ettikleri tüm belleği temizleyebilirler. Genellikle C ve C ++ 'da, yalnızca bir program başlatıldığında ve daha sonra tekrar arandığında veya yalnızca yeni anahtarlar eklendikten sonra yalnızca bir kez oluşturulması gereken bir arama ağacı gibi, öğelerin yalnızca eklenmesi ve kaldırılması gerekmeyen veri yapılarını oluşturmak için kullanışlıdırlar ( anahtar kaldırılmadı).

Ayrıca, öğelerin kaldırılmasına izin veren veri yapıları için bile kullanılabilirler, ancak bu öğeler gerçekte bellekten kurtarılmayacaktır, çünkü bunları ayrı ayrı ayıramayız. Sıralı bir ayırıcı kullanan böyle bir yapı , verilerin ayrı bir sıralı ayırıcı kullanılarak taze, sıkıştırılmış bir kopyaya kopyalandığı bazı ertelenmiş geçişleri olmadıkça (ve sabit bir ayırıcı kazandığında bazen çok etkili bir teknik olmadıkça) daha fazla bellek tüketirdi. nedense yapmayın - sadece sırayla veri yapısının yeni bir kopyasını ayırın ve eskisinin hafızasını boşaltın).

Toplamak

Yukarıdaki veri yapısı / sıralı havuz örneğinde olduğu gibi, Java GC'nin birçok ayrı parçanın çoğaltılması için süper hızlı olmasına rağmen sadece bu şekilde ayrılması büyük bir sorun olacaktır. Yazılım kapatılana kadar hiçbir şey boşaltamaz, bu noktada tüm bellek havuzlarını aynı anda serbest bırakabilir (temizleyebilir).

Bu nedenle, bunun yerine, tek bir GC döngüsünden sonra, "Eden" uzayındaki (ardışık olarak tahsis edilen) mevcut nesnelerden bir geçiş yapılır ve daha sonra referansta bulunulanlar, ayrı ayrı parçaları serbest bırakabilen daha genel amaçlı bir ayırıcı kullanılarak tahsis edilir. Artık başvurulmayanlar tasfiye sürecinde kolayca yer değiştirecektir. Yani, temelde "hala referans alınıyorsa nesneleri Eden alanından kopyalayıp temizler".

Bu normalde oldukça pahalıdır, bu nedenle başlangıçta tüm belleği tahsis eden ipliğin önemli ölçüde durmasını önlemek için ayrı bir arka plan iş parçacığında yapılır.

Bellek Eden alanından kopyalandıktan ve ilk GC döngüsünden sonra ayrı ayrı parçaları serbest bırakabilen bu daha pahalı şema kullanılarak ayrıldıktan sonra, nesneler daha kalıcı bir bellek bölgesine taşınır. Bu münferit parçalar daha sonra referans gösterilmemesi durumunda sonraki GC döngülerinde serbest bırakılır.

hız

Bu nedenle, Java GC'nin düz yığın tahsisinde C veya C ++ 'dan çok daha iyi performans göstermesinin nedeni, bellek ayırmak isteyen iş parçacığında en ucuz, tamamen dejenere edilmiş tahsis stratejisini kullanmasıdır. Daha sonra malloc, başka bir iş parçacığı için düzleştirme gibi daha genel bir ayırıcı kullanırken normalde yapmamız gereken daha pahalı işi kaydeder .

Dolayısıyla kavramsal olarak GC aslında daha fazla iş yapmak zorundadır, ancak tam maliyetin tek bir iş parçacığı tarafından önceden ödenmemesi için bunu iş parçacıkları arasında dağıtır. Bellek ayırma iş parçacığının süper ucuz yapmasına izin verir ve daha sonra işleri düzgün yapmak için gereken gerçek masrafı erteleyerek tek tek nesnelerin aslında başka bir iş parçacığına serbest kalmasını sağlar. C veya C ++ 'da biz mallocveya aradığımızda operator new, tam maliyetini aynı iş parçacığı içinde önceden ödemek zorundayız.

Bu ana farktır ve Java neden sadece saf çağrılar kullanarak mallocveya operator newbir grup ufacık parçaya ayrı ayrı tahsis ederek C veya C ++ 'dan çok iyi performans gösterebilir . Tabii ki, GC döngüsü devreye girdiğinde tipik olarak bazı atom işlemleri ve bazı potansiyel kilitleme olacaktır, ancak muhtemelen biraz optimize edilmiştir.

Temel olarak basit açıklama, tek bir iş parçacığında mallocdaha pahalı bir maliyet ödemekle ( ) tek bir iş parçacığında daha ucuz bir maliyet ödemek ve daha sonra paralel olarak çalışabilen başka bir işyerinde daha ağır maliyet ödemek anlamına gelir GC. İşleri bu şekilde yapmanın bir dezavantajı olarak, ayırıcının mevcut nesne referanslarını geçersiz kılmadan belleği kopyalamasına / hareket etmesine izin vermek için gereken şekilde nesne referansından nesneye almak için iki dolaylamaya ihtiyaç duyduğunuz anlamına gelir ve ayrıca nesne hafızası olduğunda uzamsal konumu kaybedebilirsiniz "Eden" alanından taşındı.

Son olarak, C ++ kodu normalde yığın üzerinde tek tek bir nesne yükü ayırmadığından karşılaştırma biraz haksızdır. İyi C ++ kodu, bitişik bloklardaki veya yığındaki birçok öğe için bellek ayırma eğilimindedir. Ücretsiz mağazada birer birer küçük nesneler yükü tahsis ederse, kod boktan olur.


0

Her şey hızı kimin ölçtüğüne, hangi uygulamanın hızını ölçtüğüne ve neyi kanıtlamak istediklerine bağlıdır. Ve karşılaştırdıklarını.

Sadece tahsis / dağıtmaya bakarsanız, C ++ 'da malloc'a 1.000.000 çağrı ve ücretsiz () 1.000.000 çağrı alabilirsiniz. Java'da, 1.000.000 new () çağrınız ve bir döngüde çalışan ve ücretsiz olarak kullanabileceğiniz 1.000.000 nesne bulan bir çöp toplayıcı olacaktır. Döngü, free () çağrısından daha hızlı olabilir.

Öte yandan, malloc / free başka bir zamanda gelişmiştir ve tipik olarak malloc / free sadece ayrı bir veri yapısında bir bit ayarlar ve aynı iş parçacığında malloc / free gerçekleşmesi için optimize edilir, bu nedenle çok iş parçacıklı bir ortamda paylaşılan bellek değişkenleri yoktur birçok durumda kullanılır (ve kilitleme veya paylaşılan bellek değişkenleri çok pahalıdır).

Üçüncü tarafta, çöp toplama olmadan ihtiyaç duyabileceğiniz referans sayımı gibi şeyler vardır ve bu ücretsiz değildir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.