Javascript'te Çöp Toplayıcı etkinliğini azaltmak için en iyi uygulamalar


96

Saniyede 60 kez adlandırılan bir ana döngüye sahip oldukça karmaşık bir Javascript uygulamam var. Devam eden çok fazla çöp toplama var gibi görünüyor (Chrome geliştirici araçlarındaki Bellek zaman çizelgesindeki 'testere dişi' çıktısına göre) ve bu genellikle uygulamanın performansını etkiler.

Bu yüzden, çöp toplayıcının yapması gereken iş miktarını azaltmak için en iyi uygulamaları araştırmaya çalışıyorum. (Web'de bulabildiğim bilgilerin çoğu bellek sızıntılarından kaçınmayla ilgili, bu biraz farklı bir soru - hafızam boşalıyor, sadece çok fazla çöp toplama sürüyor.) bu çoğunlukla nesneleri olabildiğince yeniden kullanmakla ilgilidir, ancak elbette şeytan ayrıntıda gizlidir.

Uygulama, John Resig'in Basit JavaScript Kalıtımına göre 'sınıflar' şeklinde yapılandırılmıştır .

Sanırım bir sorun, bazı işlevlerin saniyede binlerce kez çağrılabilmesidir (ana döngünün her yinelemesinde yüzlerce kez kullanıldıklarından) ve belki de bu işlevlerdeki yerel çalışma değişkenleri (dizeler, diziler vb.) sorun olabilir.

Daha büyük / daha ağır nesneler için nesne havuzlamasının farkındayım (ve bunu bir dereceye kadar kullanıyoruz), ancak özellikle sıkı döngülerde çok kez çağrılan işlevlerle ilgili olmak üzere, pano boyunca uygulanabilecek teknikler arıyorum. .

Çöp toplayıcının yapması gereken iş miktarını azaltmak için hangi teknikleri kullanabilirim?

Ve belki de - en çok hangi nesnelerin çöp toplandığını belirlemek için hangi teknikler kullanılabilir? (Bu çok büyük bir kod tabanı, bu nedenle yığının anlık görüntülerini karşılaştırmak pek verimli olmadı)


2
Bize gösterebileceğiniz bir kod örneği var mı? O zaman soruyu yanıtlamak daha kolay olacak (ancak potansiyel olarak daha az genel, bu yüzden burada emin değilim)
John Dvorak

2
Saniyede binlerce kez çalışan işlevleri durdurmaya ne dersiniz? Gerçekten buna yaklaşmanın tek yolu bu mu? Bu soru bir XY problemi gibi görünüyor. X'i tanımlıyorsunuz ama gerçekte aradığınız şey Y'ye bir çözüm.
Travis J

2
@TravisJ: Saniyede yalnızca 60 kez çalıştırıyor, bu oldukça yaygın bir animasyon hızı. Daha az iş yapmayı istemiyor, ancak daha fazla çöp toplama açısından nasıl yapılacağını soruyor.
Bergi

1
@Bergi - "bazı işlevler saniyede binlerce kez çağrılabilir". Bu milisaniyede birdir (muhtemelen daha kötüsü!). Bu hiç de yaygın değil. Saniyede 60 kez sorun olmamalı. Bu soru fazlasıyla belirsizdir ve yalnızca fikir veya tahmin üretecektir.
Travis J

4
@TravisJ - Oyun çerçevelerinde hiç de nadir değil.
UpTheCreek

Yanıtlar:


131

GC karmaşasını en aza indirmek için yapmanız gereken şeylerin çoğu, diğer senaryoların çoğunda deyimsel JS olarak kabul edilenlere aykırıdır, bu nedenle verdiğim tavsiyeleri değerlendirirken lütfen bağlamı aklınızda bulundurun.

Tahsis, modern tercümanlarda birkaç yerde gerçekleşir:

  1. newDeğişmez sözdizimi aracılığıyla veya aracılığıyla bir nesne oluşturduğunuzda [...]veya {}.
  2. Dizeleri birleştirdiğinizde.
  3. İşlev bildirimlerini içeren bir kapsam girdiğinizde.
  4. Bir istisnayı tetikleyen bir eylem gerçekleştirdiğinizde.
  5. Bir işlev ifadesi değerlendirdiğimizde: (function (...) { ... }).
  6. Gibi Nesneye zorlayan bir işlem gerçekleştirdiğinizde Object(myNumber)veyaNumber.prototype.toString.call(42)
  7. Kaputun altında bunlardan herhangi birini yapan bir yerleşik çağırdığınızda, mesela Array.prototype.slice.
  8. argumentsParametre listesi üzerinde düşünmek için kullandığınızda .
  9. Bir dizeyi böldüğünüzde veya normal bir ifadeyle eşleştiğinizde.

Bunları yapmaktan kaçının ve mümkün olduğunda nesneleri toplayın ve yeniden kullanın.

Özellikle, aşağıdaki fırsatlara dikkat edin:

  1. Kapalı duruma hiç bağımlı olmayan veya çok az bağımlılığı olan iç işlevleri daha yüksek, daha uzun ömürlü bir kapsama çekin. ( Closure derleyicisi gibi bazı kod küçültme araçları iç işlevleri satır içi yapabilir ve GC performansınızı artırabilir.)
  2. Yapılandırılmış verileri temsil etmek veya dinamik adresleme için dizeler kullanmaktan kaçının. splitHer biri birden çok nesne ayırma gerektirdiğinden, özellikle düzenli ifade eşleşmelerini kullanarak tekrar tekrar ayrıştırmaktan kaçının . Bu genellikle arama tablolarındaki anahtarlar ve dinamik DOM düğüm kimlikleri ile olur. Örneğin, lookupTable['foo-' + x]ve document.getElementById('foo-' + x)bir dize birleştirme olduğundan hem bir ayırma gerektirir. Genellikle, anahtarları yeniden birleştirmek yerine uzun ömürlü nesnelere ekleyebilirsiniz. Desteklemeniz gereken tarayıcılara bağlı olarak, Mapnesneleri doğrudan anahtar olarak kullanabilirsiniz.
  3. Normal kod yollarında istisnaları yakalamaktan kaçının. Bunun yerine try { op(x) } catch (e) { ... }yap if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Dizeler oluşturmaktan kaçınamadığınızda, örneğin bir sunucuya bir mesaj iletmek için, JSON.stringifybirden çok nesne ayırmak yerine içeriği biriktirmek için dahili bir yerel tampon kullanan bir yerleşik gibi kullanın .
  5. Yüksek frekanslı olaylar için geri arama kullanmaktan kaçının ve yapabildiğiniz yerlerde, mesaj içeriğinden durumu yeniden oluşturan uzun ömürlü bir işlevi (bkz. 1) geri arama olarak iletin.
  6. Çağrıldığında argumentsdizi benzeri bir nesne yaratmak zorunda olan işlevleri kullanmaktan kaçının .

JSON.stringifyGiden ağ mesajları oluşturmak için kullanmayı önerdim. Giriş mesajlarını kullanarak ayrıştırmak JSON.parseaçık bir şekilde ayırmayı ve büyük mesajlar için çoğunu içerir. Gelen mesajlarınızı ilkel diziler olarak gösterebiliyorsanız, çok sayıda ayırma kaydedebilirsiniz. Tahsis etmeyen bir ayrıştırıcı oluşturabileceğiniz diğer tek yerleşik yapıdır String.prototype.charCodeAt. Sadece okumak için cehennem olacak olanı kullanan karmaşık bir biçim için ayrıştırıcı.


JSON.parseD nesnelerinin mesaj dizesinden daha az (veya eşit) alan ayırdığını düşünmüyor musunuz ?
Bergi

@Bergi, Bu, özellik adlarının ayrı tahsisler gerektirip gerektirmediğine bağlıdır, ancak bir ayrıştırma ağacı yerine olay üreten bir ayrıştırıcı, gereksiz tahsisler yapmaz.
Mike Samuel

Harika cevap, teşekkürler!
Ödülün süresi dolduğu

Ödülle kötü zamanlamamı telafi etmek için, doldurmak için bir tane daha ekledim (verebileceğim en az 200'dü;) - Bazı nedenlerden dolayı, ödül vermeden önce 24 saat beklememi gerektirse de ( 'Mevcut cevabı ödüllendir' seçeneğini seçtim). Yarın senin olacak ...
UpTheCreek

@UpTheCreek, endişelenme. Yararlı bulduğuna sevindim.
Mike Samuel

12

Chrome geliştirici araçları bellek ayırmayı izleme için çok güzel bir özelliği vardır. Buna Hafıza Zaman Çizelgesi denir. Bu makale bazı ayrıntıları açıklamaktadır. Sanırım bahsettiğin şey bu "testere dişi" midir? Bu, çoğu GC'lenmiş çalışma zamanları için normal bir davranıştır. Tahsis, bir koleksiyonu tetikleyen bir kullanım eşiğine ulaşılana kadar devam eder. Normalde, farklı eşik değerlerinde farklı koleksiyon türleri vardır.

Chrome'da Bellek Zaman Çizelgesi

Çöp koleksiyonları, izleme süresiyle birlikte olay listesine dahil edilir. Oldukça eski defterimde, geçici koleksiyonlar yaklaşık 4Mb'de gerçekleşiyor ve 30ms sürüyor. Bu, 60Hz döngü yinelemelerinizden 2'sidir. Bu bir animasyonsa, 30 ms'lik koleksiyonlar muhtemelen takılmaya neden oluyor. Çevrenizde neler olup bittiğini görmek için buradan başlamalısınız: koleksiyon eşiğinin nerede olduğunu ve koleksiyonlarınızın ne kadar sürdüğünü. Bu size optimizasyonları değerlendirmek için bir referans noktası sağlar. Ancak, tahsis oranını yavaşlatarak ve koleksiyonlar arasındaki aralığı uzatarak kekemeliğin sıklığını azaltmaktan daha iyisini muhtemelen yapmayacaksınız.

Sonraki adım, Profiller | Kayıt türüne göre bir ayırma kataloğu oluşturmak için Kayıt Yığını Tahsisleri özelliği. Bu, izleme süresi boyunca hangi nesne türlerinin en fazla belleği tükettiğini hızlı bir şekilde gösterecektir; bu, ayırma hızına eşdeğerdir. Azalan oran sırasına göre bunlara odaklanın.

Teknikler roket bilimi değildir. Kutusuz bir nesne ile yapabildiğiniz zaman kutulu nesnelerden kaçının. Her yinelemede yenilerini ayırmak yerine tek kutulu nesneleri tutmak ve yeniden kullanmak için global değişkenler kullanın. Yaygın nesne türlerini terk etmek yerine ücretsiz listelerde bir araya getirin. Önbellek dizesi birleştirme sonuçları, gelecekteki yinelemelerde büyük olasılıkla yeniden kullanılabilir. Bunun yerine değişkenleri kapsayan bir kapsamda ayarlayarak işlev sonuçlarını döndürmek için ayırmadan kaçının. En iyi stratejiyi bulmak için her nesne türünü kendi bağlamında düşünmeniz gerekecek. Spesifik konularda yardıma ihtiyacınız varsa, baktığınız zorluğun ayrıntılarını açıklayan bir düzenleme yayınlayın.

Daha az çöp üretmek için bir av tüfeği girişiminde bir uygulama boyunca normal kodlama stilinizi değiştirmemenizi tavsiye ederim. Bu aynı nedenden dolayı hız için zamanından önce optimize etmemeniz gerektiğidir. Çabanızın çoğu artı ek karmaşıklık ve kod belirsizliği anlamsız olacaktır.


Doğru, testere dişinden kastettiğim bu. Her zaman bir tür testere dişi deseni olacağını biliyorum, ancak benim endişem, uygulamamla testere dişi frekansının ve 'uçurumların' oldukça yüksek olması. İlginçtir, GC olaylar benim zaman çizelgesi üzerinde görünmüyor - 'kayıtları' bölmesinde (ortadaki) görünen sadece olaylar şunlardır: request animation frame, animation frame firedve composite layers. Neden senin GC Eventgibi görmediğime dair hiçbir fikrim yok (bu Chrome'un en son sürümünde ve ayrıca canary'de).
UpTheCreek

4
Profil oluşturucuyu 'kayıt yığını ayırmaları' ile kullanmayı denedim, ancak şimdiye kadar çok kullanışlı bulmadım. Belki de bunun nedeni, onu nasıl doğru kullanacağımı bilmediğim içindir. Benim için hiçbir şey ifade etmeyen referanslarla dolu gibi görünüyor, @342342ve gibi code relocation info.
UpTheCreek

"Erken optimizasyon tüm kötülüklerin köküdür" ile ilgili olarak: Sadece körü körüne takip etmeyin. Oyun ve multimedya programlama gibi belirli senaryolarda, performans çok önemlidir ve çok sayıda "etkin" kodunuz olacaktır. Yani evet, programlama tarzınızı ayarlamanız gerekecek.
snarf

9

Genel bir ilke olarak, olabildiğince çok önbelleğe almak ve döngünüzün her çalışması için çok az şey oluşturmak ve yok etmek istersiniz.

Aklıma gelen ilk şey, ana döngünüzde anonim işlevlerin (varsa) kullanımını azaltmaktır. Ayrıca diğer işlevlere aktarılan nesneleri yaratma ve yok etme tuzağına düşmek de kolay olacaktır. Ben kesinlikle bir javascript uzmanı değilim, ancak şunu hayal edebilirim:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

bundan çok daha hızlı koşardı:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Programınız için herhangi bir kesinti var mı? Belki bir veya iki saniye sorunsuz çalışması için (örneğin bir animasyon için) ihtiyacınız var ve sonra işlemek için daha fazla zamanı var? Eğer durum buysa, animasyon boyunca normalde çöp olarak toplanan nesneleri aldığımı ve bazı genel nesnelerde bunlara bir referans tuttuğunu görebilirdim. Ardından animasyon bittiğinde tüm referansları temizleyebilir ve çöp toplayıcının çalışmasını sağlayabilirsiniz.

Özür dilerim, zaten denediğiniz ve düşündüğünüz şeyle karşılaştırıldığında bunların hepsi biraz önemsizse.


Bu. Ayrıca, diğer işlevler içinde (IIFE olmayanlar) bahsedilen işlevler de çok fazla bellek yakan ve gözden kaçması kolay yaygın kötüye kullanımdır.
Esailija

Teşekkürler Chris! Maalesef herhangi bir aksama
sürem yok

4

global scope(Çöp toplayıcının bunlara dokunmasına izin verilmediğinden emin olduğum yerde) bir veya birkaç nesne yapardım, sonra yerel değişkenler kullanmak yerine bu nesneleri kullanmak için çözümümü yeniden düzenlemeye çalışırdım. .

Elbette kodun her yerinde yapılamaz, ancak genellikle çöp toplayıcıdan kaçınmanın yolu budur.

Not: Kodun belirli bir bölümünü biraz daha az bakım yapılabilir hale getirebilir.


GC, genel kapsam değişkenlerimi tutarlı bir şekilde çıkarır.
VectorVortec
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.