64 öğeli birden çok diziyi 65 öğelik diziyi bildirmekten 1000 kat daha hızlı bildirme


91

Son zamanlarda, 64 öğe içeren bir diziyi bildirmenin, 65 öğeli aynı tür diziyi bildirmekten çok daha hızlı (> 1000 kat) olduğunu fark ettim.

İşte bunu test etmek için kullandığım kod:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

Değiştirirsem yaklaşık 7 saniye sürer new double[64], new double[65]bu yaklaşık 6 ms içinde çalışır . İş gittikçe daha fazla iş parçacığına yayılırsa bu sorun katlanarak daha şiddetli hale geliyor, benim sorunum da buradan kaynaklanıyor.

Bu sorun, int[65]veya gibi farklı dizi türlerinde de ortaya çıkar String[65]. Bu sorun büyük dizelerde ortaya çıkmaz: String test = "many characters";ancak bu değiştirildiğinde ortaya çıkmaya başlar.String test = i + "";

Neden böyle olduğunu ve bu sorunu aşmanın mümkün olup olmadığını merak ediyordum.


3
Not dışı: Kıyaslama System.nanoTime()için tercih edilmelidir System.currentTimeMillis().
rocketboy

4
Sadece merak ediyorum ? Linux altında mısınız? Davranış işletim sistemi ile değişiyor mu?
bsd

9
Bu soru nasıl olumsuz oy aldı ??
Rohit Jain

2
FWIW, bu kodu byteyerine ile çalıştırırsam benzer performans tutarsızlıkları görüyorum double.
Oliver Charlesworth

3
@ThomasJungblut: Peki OP'nin deneyindeki tutarsızlığı ne açıklıyor?
Oliver Charlesworth

Yanıtlar:


88

Java sanal makinenizin JIT derleyicisi tarafından yapılan optimizasyonların neden olduğu bir davranış gözlemliyorsunuz . Bu davranış, 64 öğeye kadar skaler dizilerle tetiklenir ve 64'ten büyük dizilerle tetiklenmez.

Ayrıntılara girmeden önce, döngünün gövdesine daha yakından bakalım:

double[] test = new double[64];

Vücudun etkisi yoktur (gözlemlenebilir davranış) . Bu, bu ifadenin yürütülüp yürütülmemesinin program yürütme dışında hiçbir fark yaratmadığı anlamına gelir. Aynısı tüm döngü için de geçerlidir. Dolayısıyla, kod iyileştirici döngüyü aynı işlevsel ve farklı zamanlama davranışına sahip bir şeye (veya hiçbir şeye) çevirebilir .

Kıyaslamalar için en azından aşağıdaki iki kurala uymalısınız. Bunu yapsaydınız, fark önemli ölçüde daha küçük olurdu.

  • JIT derleyicisini (ve optimize ediciyi) birkaç kez kıyaslama yaparak ısıtın.
  • Her ifadenin sonucunu kullanın ve kıyaslamanın sonunda yazdırın.

Şimdi ayrıntılara girelim. Şaşırtıcı olmayan bir şekilde 64 öğeden büyük olmayan skaler diziler için tetiklenen bir optimizasyon vardır. Optimizasyon, Escape analizinin bir parçasıdır . Küçük nesneleri ve küçük dizileri yığına ayırmak yerine yığının üzerine koyar - veya daha da iyisi onları tamamen optimize eder. Bununla ilgili bazı bilgileri Brian Goetz'in 2005 yılında yazdığı şu makalede bulabilirsiniz:

Optimizasyon, komut satırı seçeneği ile devre dışı bırakılabilir -XX:-DoEscapeAnalysis. Skaler diziler için 64 sihirli değeri de komut satırından değiştirilebilir. Programınızı aşağıdaki gibi çalıştırırsanız, 64 ve 65 elemanlı diziler arasında fark olmayacaktır:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

Bunu söyledikten sonra, bu tür komut satırı seçeneklerini kullanmaktan kesinlikle vazgeçiyorum. Gerçekçi bir uygulamada çok büyük bir fark yarattığından şüpheliyim. Bunu yalnızca, gerekliliğe kesinlikle ikna olursam kullanırdım - ve bazı sözde kıyaslamaların sonuçlarına dayanmaz.


9
Ancak optimize eden kişi neden 64 boyutundaki dizinin çıkarılabilir olduğunu ancak 65 olmadığını tespit ediyor
ug_

10
@nosid: OP'nin kodu gerçekçi olmasa da, JVM'de başka durumlarda etkileri olabilecek ilginç / beklenmedik bir davranışı tetikliyor. Bunun neden olduğunu sormanın geçerli olduğunu düşünüyorum.
Oliver Charlesworth

1
@ThomasJungblut Döngünün kaldırıldığını sanmıyorum. Döngünün dışına "toplam" ekleyebilir ve "toplam + = test [0];" yukarıdaki örneğe. Ardından, sonucu yazdırdığınızda, toplamın = 100 milyon olduğunu göreceksiniz ve bir saniyeden daha kısa bir sürede durur.
Sipko

1
Yığın üzerinde değiştirme, yığın tahsisini yığın tahsisi ile değiştirmek yerine, yorumlanan kodu anında derlenen kodla değiştirmektir. EliminateAllocationArraySizeLimit, kaçış analizinde skaler değiştirilebilir olarak kabul edilen dizilerin sınır boyutudur. Dolayısıyla, etkinin derleyici optimizasyonundan kaynaklandığı ana nokta doğrudur, ancak bunun nedeni yığın tahsisi değildir, kaçış analizi aşamasının tahsisi fark edememesi nedeniyle gerekli değildir.
kiheru

2
@Sipko: Uygulamanın konu sayısı ile ölçeklenmediğini yazıyorsunuz. Bu, sorunun sorduğunuz mikro optimizasyonlarla ilgili olmadığının bir göstergesi. Küçük parçalar yerine büyük resme bakmanızı öneririm.
nosid

2

Bir nesnenin boyutuna bağlı olarak bir fark olmanın birçok yolu vardır.

Nosid'in belirttiği gibi, JITC yığın üzerinde küçük "yerel" nesneler tahsis ediyor olabilir (büyük olasılıkla) ve "küçük" diziler için boyut sınırı 64 eleman olabilir.

Yığın üzerinde tahsis etmek, yığın ayırmaktan önemli ölçüde daha hızlıdır ve daha da önemlisi, yığının çöp olarak toplanması gerekmez, bu nedenle GC ek yükü büyük ölçüde azalır. (Ve bu test senaryosu için GC ek yükü, muhtemelen toplam yürütme süresinin% 80-90'ıdır.)

Ayrıca, değer yığın tahsis edildikten sonra, JITC "ölü kod eleme" gerçekleştirebilir, sonucunun newhiçbir yerde kullanılmadığını belirleyebilir ve kaybolacak yan etkilerin olmadığından emin olduktan sonra tüm newoperasyonu ortadan kaldırabilir , ve sonra (artık boş) döngünün kendisi.

JITC yığın tahsisi yapmasa bile, belirli bir boyuttan daha küçük nesnelerin bir yığın içinde büyük nesnelerden farklı bir şekilde (örneğin, farklı bir "boşluktan") ayrılması tamamen mümkündür. (Normalde bu, çok dramatik zamanlama farkları üretmez.)


Bu konuya geç. Neden yığın üzerinde ayırma, yığın ayırmaktan daha hızlıdır? Birkaç makaleye göre, yığın üzerine tahsis etmek ~ 12 talimat alır. İyileştirme için fazla yer yok.
Girdap

@Vortex - Yığına atamak için 1-2 talimat gerekir. Ancak bu, tüm bir yığın çerçevesini ayırmak içindir. Yığın çerçevesi, rutin için bir kayıt kaydetme alanına sahip olmak için yine de tahsis edilmelidir, böylece aynı zamanda tahsis edilen diğer değişkenler "serbesttir". Ve dediğim gibi, yığın GC gerektirmez. Bir yığın öğesinin GC ek yükü, yığın ayırma işleminin maliyetinden çok daha büyüktür.
Hot Licks
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.