Giriş
Son yıllarda web uygulamalarının hızı önemli ölçüde arttı. Artık birçok uygulama o kadar hızlı çalışıyor ki bazı geliştiricilerin "Web yeterince hızlı mı?" diye sorduğunu duyuyorum. Bazı uygulamalar için bu yeterli olabilir ancak yüksek performanslı uygulamalar üzerinde çalışan geliştiriciler için yeterince hızlı olmadığını biliyoruz. JavaScript sanal makine teknolojisindeki şaşırtıcı ilerlemelere rağmen, yakın zamanda yapılan bir çalışma Google uygulamalarının zamanlarının% 50 ila% 70'ini V8 içinde geçirdiğini gösterdi. Uygulamanızın sınırlı bir süresi vardır. Bir sistemden döngüleri azaltmak, başka bir sistemin daha fazla iş yapabileceği anlamına gelir. 60 fps'de çalışan uygulamaların kare başına yalnızca 16 ms'si olduğunu unutmayın. Aksi takdirde takılma yaşanır. JavaScript'i optimize etme ve JavaScript uygulamalarını profilleme hakkında bilgi edinmek için okumaya devam edin. Bu makalede, Oz Büyücüsü filminde belirsiz bir performans sorununu takip eden V8 ekibindeki performans dedektiflerinin hikayesi anlatılmaktadır.
Google I/O 2013 Oturum
Bu materyali Google I/O 2013'te sunduk. Aşağıdaki videoya göz atın:
Performans neden önemlidir?
CPU döngüleri sıfır toplamlı bir oyundur. Sisteminizin bir bölümünün daha az kullanmasını sağlamak, başka bir bölümde daha fazla kullanmanıza veya genel olarak daha sorunsuz çalışmanıza olanak tanır. Daha hızlı çalışma ve daha fazla işlem yapma genellikle birbirine zıt hedeflerdir. Kullanıcılar yeni özellikler talep ederken uygulamanızın daha sorunsuz çalışmasını da bekler. JavaScript sanal makineleri giderek daha hızlı hale geliyor ancak web uygulamalarındaki performans sorunlarıyla uğraşan birçok geliştiricinin bildiği gibi, bu durum şu anda düzeltebileceğiniz performans sorunlarını göz ardı etmeniz için bir neden değildir. Gerçek zamanlı, yüksek kare hızına sahip uygulamalarda takılma olmaması çok önemlidir. Insomniac Games, istikrarlı ve yüksek kare hızının bir oyunun başarısı için önemli olduğunu gösteren bir çalışma yayınladı: "İstikrarlı kare hızı, profesyonel ve iyi tasarlanmış bir ürünün göstergesidir." Web geliştiricileri dikkate alabilir.
Performans Sorunlarını Çözme
Performans sorunlarını çözmek, bir suçu çözmeye benzer. Kanıtları dikkatlice incelemeniz, şüpheli nedenleri kontrol etmeniz ve farklı çözümlerle denemeler yapmanız gerekir. Sorunu gerçekten çözdüğünüzden emin olmak için tüm süreçte ölçümlerinizi belgelemeniz gerekir. Bu yöntem ile suç dedektiflerinin bir olayı çözme şekli arasında çok az fark vardır. Dedektifler, kanıtları inceler, şüphelileri sorgular ve suçu kanıtlayacak bir şey bulmak için denemeler yapar.
V8 CSI: Oz
Oz Büyücüsü'nü geliştiren muhteşem sihirbazlar, kendi başlarına çözemedikleri bir performans sorunuyla V8 ekibine başvurdu. Bazen Oz donup takılmalara neden oluyordu. Oz geliştiricileri, Chrome Geliştirici Araçları'ndaki zaman çizelgesi panelini kullanarak bazı başlangıç incelemeleri yapmıştı. Bellek kullanımına baktıklarında korkunç testere dişi grafiğiyle karşılaştılar. Çöp toplayıcı saniyede bir kez 10 MB çöp topluyordu ve çöp toplama duraklamaları, takılmayla aynı zamana denk geliyordu. Chrome Geliştirici Araçları'ndaki Zaman Çizelgesi'ndeki aşağıdaki ekran görüntüsüne benzer:
V8 dedektifleri Jakob ve Yang bu konuyu ele aldı. V8 ekibinden Jakob ile Oz ekibinden Yang arasında uzun bir tartışma yaşandı. Bu görüşmeyi, sorunun tespit edilmesine yardımcı olan önemli etkinliklere göre özetledim.
Kanıt
İlk adım, ilk kanıtları toplayıp incelemektir.
Ne tür bir uygulamaya bakıyoruz?
Oz demosu, etkileşimli bir 3D uygulamadır. Bu nedenle, çöp toplama işlemlerinden kaynaklanan duraklamalar konusunda çok hassastır. 60 fps hızında çalışan etkileşimli bir uygulamanın tüm JavaScript işlemlerini 16 ms içinde tamamlaması gerektiğini ve bu sürenin bir kısmının Chrome'un grafik çağrılarını işleyip ekranı çizmesi için ayrılması gerektiğini unutmayın.
Oz, çift değerler üzerinde çok sayıda aritmetik hesaplama yapar ve WebAudio ile WebGL'ye sık sık çağrı yapar.
Ne tür bir performans sorunuyla karşılaşıyoruz?
Duraklamalar (kare düşmesi veya takılma) görüyoruz. Bu duraklamalar, çöp toplama işlemlerine bağlıdır.
Geliştiriciler en iyi uygulamaları uyguluyor mu?
Evet, Oz geliştiricileri JavaScript sanal makinesi performansı ve optimizasyon teknikleri konusunda bilgilidir. Oz geliştiricilerinin kaynak dil olarak CoffeeScript'i kullandığını ve CoffeeScript derleyicisi aracılığıyla JavaScript kodu ürettiğini belirtmek gerekir. Oz geliştiricileri tarafından yazılan kod ile V8 tarafından kullanılan kod arasındaki bağlantısızlık nedeniyle, incelemenin bazı bölümleri daha karmaşık hale geldi. Chrome Geliştirici Araçları artık bu işlemi kolaylaştıracak kaynak eşlemelerini destekliyor.
Çöp toplayıcı neden çalışır?
JavaScript'deki bellek, geliştirici için sanal makine tarafından otomatik olarak yönetilir. V8, belleğin iki (veya daha fazla) nesle bölündüğü ortak bir çöp toplama sistemi kullanır. Genç nesil, kısa süre önce ayrılmış nesneleri barındırır. Yeterince uzun süre hayatta kalan nesneler eski nesle taşınır.
Genç nesil, eski nesle kıyasla çok daha yüksek bir sıklıkta toplanır. Genç nesil koleksiyonları çok daha ucuz olduğu için bu durum planlı bir işlemdir. Sık GC duraklamaları genellikle genç nesil toplamadan kaynaklanır.
V8'de genç bellek alanı, eşit boyutta iki bitişik bellek bloğuna bölünür. Belirli bir zamanda bu iki bellek bloğundan yalnızca biri kullanılır ve bu alana hedef alan adı verilir. Hedef alanda kalan bellek varsa yeni bir nesne ayırmak ucuzdur. "To" alanındaki imleç, yeni nesne için gereken bayt sayısı kadar ileri taşınır. Bu işlem, hedef alan dolana kadar devam eder. Bu noktada program durdurulur ve veri toplama işlemi başlar.
Bu noktada, kaynak alan ve hedef alan değiştirilir. "to" alanı olan ve artık "from" alanı olan alan baştan sona taranır ve hâlâ etkin olan nesneler "to" alanına kopyalanır veya eski nesil yığına yükseltilir. Ayrıntılı bilgi için Cheney algoritması hakkındaki makaleyi okumanızı öneririz.
Bir nesne her atandığında (new, [], veya {} çağrısı aracılığıyla) dolaylı veya açık bir şekilde, uygulamanızın çöp toplama işlemine ve korkutucu uygulama duraklatma durumuna yaklaştığını sezgisel olarak anlayabilirsiniz.
Bu uygulama için saniyede 10 MB'lık gereksiz veri bekleniyor mu?
Kısacası, hayır. Geliştirici, saniyede 10 MB'lık gereksiz veri göndermek için hiçbir şey yapmıyor.
Şüpheliler
İncelemenin bir sonraki aşaması, potansiyel şüphelileri belirlemek ve ardından bu şüphelileri azaltmaktır.
Şüpheli #1
Kare sırasında yeni arama. Ayrılan her nesnenin, GC duraklatılmasına daha da yaklaşmanızı sağladığını unutmayın. Özellikle yüksek kare hızlarında çalışan uygulamalar, kare başına sıfır tahsis etmeye çalışmalıdır. Bu genellikle, dikkatlice düşünülmüş, uygulamaya özel bir nesne geri dönüşüm sistemi gerektirir. V8 dedektifleri Oz ekibiyle görüştüler ve yeni bir arama olmadığını söylediler. Aslında Oz ekibi bu şartın farkındaydı ve "Bu utanç verici olur" demişti. Bu seçeneği listeden çıkarın.
Şüpheli #2
Bir nesnenin "şeklini", kurucu dışında değiştirme Bu durum, bir nesneye kurucu dışında yeni bir özellik eklendiğinde her zaman ortaya çıkar. Bu işlem, nesne için yeni bir gizli sınıf oluşturur. Optimize edilmiş kod bu yeni gizli sınıfı gördüğünde koddan kaldırma işlemi tetiklenir. Kod, sıcak olarak sınıflandırılana ve tekrar optimize edilene kadar optimize edilmemiş kod yürütülür. Bu optimizasyondan kaldırma ve yeniden optimizasyon işlemi,takılmalara neden olur ancak aşırı miktarda gereksiz kod oluşturmayla kesinlikle ilişkili değildir. Kodda yapılan dikkatli bir denetimin ardından, nesne şekillerinin statik olduğu doğrulandı ve bu nedenle 2. şüpheli reddedildi.
Şüpheli #3
Optimize edilmemiş kodda aritmetik. Optimize edilmemiş kodda tüm hesaplamalar, gerçek nesnelerin ayrılmasıyla sonuçlanır. Örneğin, şu snippet:
var a = p * d;
var b = c + 3;
var c = 3.3 * dt;
point.x = a * b * c;
Bu işlem sonucunda 5 HeapNumber nesnesi oluşturulur. İlk üçü a, b ve c değişkenleri içindir. 4. değer anonim değer (a * b) içindir ve 5. değer #4 * c'den gelir. 5. değer sonunda point.x'e atanır.
Oz, her karede bu tür binlerce işlem yapar. Bu hesaplamalardan herhangi biri, hiçbir zaman optimize edilmeyen işlevlerde gerçekleşirse gereksiz veri oluşmasına neden olabilir. Çünkü optimize edilmemiş hesaplamalar geçici sonuçlar için bile bellek ayırır.
Şüpheli #4
Bir mülke çift hassasiyetli bir sayı depolama Numarayı depolamak için bir HeapNumber nesnesi oluşturulmalı ve mülk bu yeni nesneyi işaret edecek şekilde değiştirilmelidir. Mülkün, HeapNumber'ı işaret edecek şekilde değiştirilmesi çöp oluşturmaz. Ancak, nesne özellikleri olarak depolanan çok sayıda çift hassasiyetli sayı olabilir. Kod aşağıdaki gibi ifadelerle doludur:
sprite.position.x += 0.5 * (dt);
Optimize edilmiş kodda, x her yeni hesaplanan değer atandığında (görünüşte zararsız bir ifade) yeni bir HeapNumber nesnesi dolaylı olarak ayrılır. Bu da bizi bir çöp toplama duraklatmasına yaklaştırır.
Çift hassasiyetli sayı için depolama alanı yalnızca bir kez ayrıldığı ve değerin tekrar tekrar değiştirilmesi için yeni depolama alanı ayrılması gerekmediğinden, tanımlanmış bir dizi (veya yalnızca çift hassasiyetli sayılar içeren normal bir dizi) kullanarak bu sorunu tamamen önleyebileceğinizi unutmayın.
4. şüpheli olası bir şüphelidir.
Adli tıp
Bu noktada dedektiflerin iki olası şüphelisi vardır: yığın sayılarının nesne özellikleri olarak depolanması ve optimize edilmemiş işlevlerde gerçekleşen aritmetik hesaplama. Artık laboratuvara gidip hangi şüphelinin suçlu olduğunu kesin olarak belirleme zamanıydı. NOT: Bu bölümde, asıl Oz kaynak kodunda bulunan sorunun bir kopyasını kullanacağım. Bu yeniden oluşturma, orijinal koddan çok daha küçüktür ve bu nedenle daha kolay anlaşılır.
Deneme #1
3. şüpheliyi (optimizasyon yapılmamış işlevler içindeki aritmetik hesaplama) kontrol etme. V8 JavaScript motorunda, arka planda neler olup bittiğine dair mükemmel bilgiler sağlayabilecek yerleşik bir günlük kaydı sistemi bulunur.
Chrome hiç çalışmıyorsa Chrome'u aşağıdaki işaretlerle başlatın:
--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"
ve ardından Chrome'dan tamamen çıktığınızda mevcut dizinde bir v8.log dosyası oluşturulur.
v8.log dosyasının içeriğini yorumlayabilmek için Chrome'unuzun kullandığı v8 sürümünü (about:version adresini kontrol edin) indirmeniz ve derlemeniz gerekir.
v8'i başarıyla oluşturduktan sonra, tik işlemcisini kullanarak günlüğü işleyebilirsiniz:
$ tools/linux-tick-processor /path/to/v8.log
(Platformunuza bağlı olarak linux yerine mac veya windows yazın.) (Bu araç, v8'deki üst düzey kaynak dizininden çalıştırılmalıdır.)
Onay işleyici, en çok onay alan JavaScript işlevlerinin metin tabanlı bir tablosunu gösterir:
[JavaScript]:
ticks total nonlib name
167 61.2% 61.2% LazyCompile: *opt demo.js:12
40 14.7% 14.7% LazyCompile: unopt demo.js:20
15 5.5% 5.5% Stub: KeyedLoadElementStub
13 4.8% 4.8% Stub: BinaryOpStub_MUL_Alloc_Number+Smi
6 2.2% 2.2% Stub: BinaryOpStub_ADD_OverwriteRight_Number+Number
4 1.5% 1.5% Stub: KeyedStoreElementStub
4 1.5% 1.5% KeyedLoadIC: {12}
2 0.7% 0.7% KeyedStoreIC: {13}
1 0.4% 0.4% LazyCompile: ~main demo.js:30
demo.js dosyasının üç işlevi olduğunu görebilirsiniz: opt, unopt ve main. Optimize edilmiş işlevlerin adlarının yanında yıldız işareti (*) bulunur. opt işlevinin optimize edildiğini ve unopt işlevinin optimize edilmediğini gözlemleyin.
V8 dedektifinin araç çantasındaki bir diğer önemli araç da plot-timer-event'tir. Bu komut şu şekilde yürütülebilir:
$ tools/plot-timer-event /path/to/v8.log
Çalıştırıldıktan sonra geçerli dizinde timer-events.png adlı bir png dosyası oluşturulur. Dosyayı açtığınızda aşağıdakine benzer bir görünüm görürsünüz:
Alt kısımdaki grafiğin dışında veriler satırlarda gösterilir. X ekseni zamandır (ms). Sol tarafta her satırın etiketleri bulunur:
V8.Execute satırında, V8'in JavaScript kodunu yürüttüğü her profil işaretinde siyah dikey bir çizgi çizilir. V8.GCScavenger, V8'in yeni nesil koleksiyon gerçekleştirdiği her profil onay kutusunda mavi dikey bir çizgi çizer. V8 durumlarının geri kalanı için de benzer şekilde.
En önemli satırlardan biri "yürütülen kod türü"dür. Optimize edilmiş kod yürütülürken yeşil, optimize edilmemiş kod yürütülürken ise kırmızı ve mavi karışımı renkte olur. Aşağıdaki ekran görüntüsünde, optimize edilmişten optimize edilmeyene ve ardından tekrar optimize edilmiş koda geçiş gösterilmektedir:
İdeal olarak bu satır sabit yeşil renkte olur ancak bu durum her zaman hemen gerçekleşmez. Bu, programınızın optimize edilmiş bir kararlı duruma geçtiği anlamına gelir. Optimize edilmemiş kodlar her zaman optimize edilmiş kodlardan daha yavaş çalışır.
Bu kadar ayrıntıya girdiğiniz takdirde, uygulamanızı v8 hata ayıklama kabuğunda (d8) çalışacak şekilde yeniden düzenleyerek çok daha hızlı çalışabileceğinizi hatırlatmak isteriz. d8'i kullanmak, tik işlemcisi ve çizim zamanlayıcısı etkinliği araçlarıyla daha hızlı iterasyon süreleri elde etmenizi sağlar. d8 kullanmanın bir başka yan etkisi de, gerçek sorunun tespit edilmesini kolaylaştırarak verilerdeki gürültü miktarını azaltmasıdır.
Oz kaynak kodundaki zamanlayıcı etkinlikleri grafiğine bakıldığında, optimize edilmiş koddan optimize edilmemiş koda geçiş olduğu ve optimize edilmemiş kod yürütülürken aşağıdaki ekran görüntüsüne benzer şekilde birçok yeni nesil koleksiyonun tetiklendiği görülüyor (ortada zamanın kaldırıldığına dikkat edin):
Yakından bakarsanız V8'in JavaScript kodunu ne zaman yürüttüğünü gösteren siyah çizgilerin, yeni nesil koleksiyonlarla (mavi çizgiler) tam olarak aynı profil tikleme zamanlarında eksik olduğunu görebilirsiniz. Bu, çöp toplanırken komut dosyasının duraklatıldığını açıkça gösterir.
Oz kaynak kodundaki tıklama işleyici çıkışına bakıldığında, en üst işlev (updateSprites) optimize edilmemiştir. Diğer bir deyişle, programın en çok zaman harcadığı işlev de optimize edilmemiştir. Bu, suçlunun 3. numaralı şüpheli olduğunu güçlü bir şekilde gösteriyor. updateSprites kaynağı aşağıdaki gibi döngüler içeriyordu:
function updateSprites(dt) {
for (var sprite in sprites) {
sprite.position.x += 0.5 * dt;
// 20 more lines of arithmetic computation.
}
}
V8'i çok iyi bildiklerinden, for-i-in döngü yapısının bazen V8 tarafından optimize edilmediğini hemen fark ettiler. Diğer bir deyişle, bir işlev for-i-in döngü yapısı içeriyorsa optimize edilmeyebilir. Bu, şu anda özel bir durumdur ve gelecekte değişebilir. Yani V8 bir gün bu döngü yapısını optimize edebilir. V8 dedektifi olmadığımızdan ve V8'i avucumuzun içi gibi bilmediğimizden updateSprites'ın neden optimize edilmediğini nasıl belirleyebiliriz?
Deneme #2
Chrome'u şu işaretle çalıştırın:
--js-flags="--trace-deopt --trace-opt-verbose"
optimizasyon ve optimizasyondan kaldırma verilerinin ayrıntılı bir günlüğünü gösterir. Verileri updateSprites için aradığımızda şunları görürüz:
[updateSprites için optimizasyon devre dışı bırakıldı, neden: ForInStatement hızlı durum değil]
Dedektiflerin tahmin ettiği gibi, bunun nedeni for-i-in döngü yapısıydı.
Destek Kaydı Kapatıldı
updateSprites işlevinin optimize edilmemesinin nedenini öğrendikten sonra, düzeltme basitti. Hesaplamayı kendi işlevine taşımanız yeterliydi. Yani:
function updateSprite(sprite, dt) {
sprite.position.x += 0.5 * dt;
// 20 more lines of arithmetic computation.
}
function updateSprites(dt) {
for (var sprite in sprites) {
updateSprite(sprite, dt);
}
}
updateSprite optimize edilerek çok daha az HeapNumber nesnesi oluşturulur ve GC duraklamaları daha seyrek hale gelir. Aynı denemeleri yeni kodla gerçekleştirerek bunu doğrulamanız kolay olacaktır. Dikkatli okuyucular, çift sayıların hâlâ mülk olarak depolandığını fark edecektir. Profil oluşturma işlemi buna değdiğini gösteriyorsa konumu çiftler dizisi veya yazılmış veri dizisi olarak değiştirerek oluşturulan nesne sayısını daha da azaltabilirsiniz.
Son söz
Oz geliştiricileri bununla yetinmedi. V8 dedektifleri tarafından paylaşılan araç ve tekniklerle donanmış olarak, optimizasyondan çıkarılma cehenneminde takılı kalan birkaç işlev daha buldular ve hesaplama kodunu optimize edilmiş alt işlevlere dahil ettiler. Bu da daha da iyi performans elde etmelerini sağladı.
Hemen dışarı çıkıp performans suçları çözmeye başlayın.