Giriş
Daniel Clifford, Google I/O'da V8'de JavaScript performansını iyileştirmeye yönelik ipuçları ve püf noktaları hakkında mükemmel bir konuşma yaptı. Daniel, bizi "daha hızlı talep etmeye", C++ ile JavaScript arasındaki performans farklarını dikkatlice analiz etmeye ve JavaScript'in nasıl çalıştığını göz önünde bulundurarak kod yazmaya teşvik etti. Daniel'ın konuşmasındaki en önemli noktaların özeti bu makalede yer almaktadır. Performansla ilgili yönergeler değiştikçe bu makaleyi de güncelleyeceğiz.
En önemli tavsiye
Performansla ilgili tavsiyeleri bağlam içinde değerlendirmek önemlidir. Performans tavsiyeleri bağımlılık yapar ve bazen derinlemesine tavsiyelere odaklanmak, asıl sorunlardan uzaklaşmanıza neden olabilir. Web uygulamanızın performansına bütünsel bir bakış açısıyla yaklaşmanız gerekir. Bu performans ipucuna odaklanmadan önce kodunuzu PageSpeed gibi araçlarla analiz etmeniz ve puanınızı yükseltmeniz önerilir. Bu sayede erken optimizasyondan kaçınabilirsiniz.
Web uygulamalarında iyi performans elde etmek için en iyi temel tavsiye şudur:
- Sorun yaşamadan (veya fark etmeden) önce hazırlıklı olun
- Ardından, sorununuzun temelini belirleyip anlayın.
- Son olarak, önemli olan sorunları düzeltin
Bu adımları gerçekleştirmek için V8'in JS'yi nasıl optimize ettiğini anlamak önemlidir. Böylece JS çalışma zamanı tasarımını dikkate alarak kod yazabilirsiniz. Kullanabileceğiniz araçlar ve bu araçların size nasıl yardımcı olabileceği hakkında bilgi edinmek de önemlidir. Daniel, konuşmasında geliştirici araçlarının nasıl kullanılacağını daha ayrıntılı bir şekilde açıklıyor. Bu dokümanda, V8 motoru tasarımının en önemli noktalarından bazıları ele alınmaktadır.
Şimdi V8 ipuçlarına geçelim.
Gizli sınıflar
JavaScript'te derleme zamanı tür bilgileri sınırlıdır: Türler çalışma zamanında değiştirilebilir. Bu nedenle, derleme zamanında JS türleri hakkında akıl yürütmenin maliyetli olmasını beklemek doğaldır. Bu durum, JavaScript performansının C++'a nasıl yaklaşabileceğini sorgulamanıza neden olabilir. Ancak V8, çalışma zamanında nesneler için dahili olarak oluşturulan gizli türlere sahiptir. Aynı gizli sınıfa sahip nesneler, aynı optimize edilmiş oluşturulan kodu kullanabilir.
Örneğin:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```
p2 nesne örneğine ek üye ".z" eklenene kadar p1 ve p2 dahili olarak aynı gizli sınıfa sahiptir. Bu sayede V8, p1 veya p2'yi değiştiren JavaScript kodu için optimize edilmiş derlemenin tek bir sürümünü oluşturabilir. Gizli sınıfların farklılaşmasını ne kadar önleyebilirseniz o kadar iyi performans elde edersiniz.
Bu nedenle
- Tüm nesne üyelerini oluşturucu işlevlerde ilk kullanıma hazırlayın (böylece örnekler daha sonra tür değiştirmez).
- Nesne üyelerini her zaman aynı sırayla başlatın.
Numbers
V8, türler değişebildiğinde değerleri verimli bir şekilde temsil etmek için etiketlemeyi kullanır. V8, kullandığınız değerlerden hangi sayı türüyle çalıştığınızı tahmin eder. V8 bu çıkarımı yaptıktan sonra, bu türler dinamik olarak değişebileceğinden değerleri verimli bir şekilde temsil etmek için etiketlemeyi kullanır. Ancak bazen bu tür etiketlerini değiştirmenin bir maliyeti olur. Bu nedenle, sayı türlerini tutarlı bir şekilde kullanmak en iyisidir ve genel olarak, uygun olduğunda 31 bitlik işaretli tam sayıları kullanmak en idealidir.
Örneğin:
var i = 42; // this is a 31-bit signed integer
var j = 4.2; // this is a double-precision floating point number```
Bu nedenle
- 31 bit işaretli tam sayı olarak gösterilebilen sayısal değerleri tercih edin.
Diziler
Büyük ve seyrek dizileri işlemek için dahili olarak iki tür dizi depolama alanı vardır:
- Fast Elements: Kompakt anahtar kümeleri için doğrusal depolama
- Sözlük Öğeleri: Diğer durumlarda karma tablo depolama alanı
Dizi depolama alanının bir türden diğerine geçmesine neden olmamanız önerilir.
Bu nedenle
- Diziler için 0'dan başlayan bitişik anahtarlar kullanın.
- Büyük dizileri (ör. > 64 bin öğe) maksimum boyutlarına önceden tahsis etmeyin, bunun yerine ilerledikçe büyütün.
- Dizilerdeki, özellikle de sayısal dizilerdeki öğeleri silmeyin.
- Başlatılmamış veya silinmiş öğeleri yüklemeyin:
for (var b = 0; b < 10; b++) {
a[0] |= b; // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // Much better! 2x faster.
}
Ayrıca, double dizileri daha hızlıdır. Dizinin gizli sınıfı, öğe türlerini izler ve yalnızca double içeren diziler kutudan çıkarılır (bu da gizli bir sınıf değişikliğine neden olur). Ancak dizilerin dikkatsizce değiştirilmesi, kutulama ve kutudan çıkarma nedeniyle fazladan işe neden olabilir. Örneğin:
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts```
Şundan daha az verimli:
var a = [77, 88, 0.5, true];
Çünkü ilk örnekte, tek tek atamalar birbiri ardına gerçekleştirilir ve a[2] ataması, dizinin kutulanmamış double değerlerden oluşan bir diziye dönüştürülmesine neden olur. Ancak daha sonra a[3] ataması, dizinin herhangi bir değer (sayı veya nesne) içerebilecek bir diziye yeniden dönüştürülmesine neden olur. İkinci durumda, derleyici, değişmez değerdeki tüm öğelerin türlerini bilir ve gizli sınıf önceden belirlenebilir.
- Küçük sabit boyutlu diziler için düz dizileri kullanarak başlatma
- Küçük dizileri (<64k) kullanmadan önce doğru boyuta önceden ayırın.
- Sayısal olmayan değerleri (nesneleri) sayısal dizilerde depolamayın.
- Değişmez değerler olmadan ilk kullanıma hazırlama yaparsanız küçük dizilerin yeniden dönüştürülmesine neden olmamaya dikkat edin.
JavaScript derlemesi
JavaScript çok dinamik bir dil olmasına ve orijinal uygulamaları yorumlayıcı olmasına rağmen modern JavaScript çalışma zamanı motorları derleme kullanır. V8 (Chrome'un JavaScript'i) aslında iki farklı tam zamanında (JIT) derleyiciye sahiptir:
- Herhangi bir JavaScript için iyi kod oluşturabilen "Tam" derleyici
- Çoğu JavaScript için mükemmel kod üreten ancak derlenmesi daha uzun süren optimizasyon derleyicisi.
Tam derleyici
V8'de, tam derleyici tüm kodlarda çalışır ve kodu mümkün olan en kısa sürede yürütmeye başlar. Bu sayede, iyi ancak mükemmel olmayan kodlar hızlı bir şekilde oluşturulur. Bu derleyici, derleme zamanında türler hakkında neredeyse hiçbir varsayımda bulunmaz. Değişken türlerinin çalışma zamanında değişebileceğini ve değişeceğini varsayar. Tam derleyici tarafından oluşturulan kod, program çalışırken türler hakkındaki bilgileri hassaslaştırmak için satır içi önbellekleri (IC'ler) kullanarak verimliliği anında artırır.
Satır içi önbelleklerin amacı, işlemlere yönelik türe bağlı kodu önbelleğe alarak türleri verimli bir şekilde işlemektir. Kod çalıştırıldığında önce tür varsayımlarını doğrular, ardından işlemi kısaltmak için satır içi önbelleği kullanır. Ancak bu, birden fazla türü kabul eden işlemlerin daha düşük performans göstereceği anlamına gelir.
Bu nedenle
- İşlemlerin monomorfik kullanımı, polimorfik işlemlere tercih edilir.
Girişlerin gizli sınıfları her zaman aynıysa işlemler monomorfiktir. Aksi takdirde polimorfiktirler. Yani bazı bağımsız değişkenler, işleme yapılan farklı çağrılarda tür değiştirebilir. Örneğin, bu örnekteki ikinci add() çağrısı polimorfizme neden olur:
function add(x, y) {
return x + y;
}
add(1, 2); // + in add is monomorphic
add("a", "b"); // + in add becomes polymorphic```
Optimize edici derleyici
V8, tam derleyiciye paralel olarak "hot" işlevleri (yani birçok kez çalıştırılan işlevler) optimize edici bir derleyiciyle yeniden derler. Bu derleyici, derlenen kodu daha hızlı hale getirmek için tür geri bildirimini kullanır. Hatta az önce bahsettiğimiz IC'lerden alınan türleri kullanır.
Optimizasyon derleyicisinde işlemler, tahmini olarak satır içine alınır (doğrudan çağrıldıkları yere yerleştirilir). Bu, yürütmeyi hızlandırır (bellekte kaplanan yer pahasına) ancak diğer optimizasyonları da etkinleştirir. Monomorfik işlevler ve yapıcılar tamamen satır içine alınabilir (bu, V8'de monomorfizmin iyi bir fikir olmasının bir başka nedenidir).
V8 motorunun bağımsız "d8" sürümünü kullanarak nelerin optimize edildiğini günlüğe kaydedebilirsiniz:
d8 --trace-opt primes.js
(Bu, optimize edilmiş işlevlerin adlarını stdout'a kaydeder.)
Ancak tüm işlevler optimize edilemez. Bazı özellikler, optimizasyon derleyicisinin belirli bir işlevde çalışmasını engeller ("vazgeçme"). Özellikle, optimizasyon derleyicisi şu anda try {} catch {} blokları içeren işlevlerde başarısız oluyor.
Bu nedenle
- try {} catch {} bloklarınız varsa performansa duyarlı kodu iç içe yerleştirilmiş bir işleve yerleştirin: ```js function perf_sensitive() { // Do performance-sensitive work here }
try { perf_sensitive() } catch (e) { // Handle exceptions here } ```
Optimizasyon derleyicisinde try/catch bloklarını etkinleştirdiğimiz için bu kılavuz gelecekte değişebilir. Optimizasyon derleyicisinin işlevleri nasıl iptal ettiğini incelemek için d8 ile "--trace-opt" seçeneğini yukarıdaki gibi kullanabilirsiniz. Bu seçenek, hangi işlevlerin iptal edildiği hakkında daha fazla bilgi verir:
d8 --trace-opt primes.js
Optimizasyonu kaldırma
Son olarak, bu derleyicinin gerçekleştirdiği optimizasyon tahmine dayalıdır. Bazen işe yaramaz ve geri adım atarız. "Optimizasyonu kaldırma" işlemi, optimize edilmiş kodu atar ve "tam" derleyici kodunda doğru yerde yürütmeye devam eder. Yeniden optimizasyon daha sonra tekrar tetiklenebilir ancak kısa vadede yürütme yavaşlar. Özellikle, işlevler optimize edildikten sonra değişkenlerin gizli sınıflarında değişikliklere neden olmak bu optimizasyonun kaldırılmasına neden olur.
Bu nedenle
- İşlevler optimize edildikten sonra gizli sınıf değişikliklerinden kaçınma
Diğer optimizasyonlarda olduğu gibi, V8'in optimizasyonunu kaldırmak zorunda kaldığı işlevlerin günlüğünü bir günlük kaydı işaretiyle alabilirsiniz:
d8 --trace-deopt primes.js
Diğer V8 araçları
Bu arada, başlangıçta Chrome'a V8 izleme seçeneklerini de iletebilirsiniz:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```
Geliştirici araçları profil oluşturma özelliğini kullanmanın yanı sıra d8'i kullanarak da profil oluşturabilirsiniz:
% out/ia32.release/d8 primes.js --prof
Bu işlem, her milisaniyede bir örnek alan ve v8.log dosyasını yazan yerleşik örnekleme profil oluşturucuyu kullanır.
Özet
Performanslı JavaScript oluşturmaya hazırlanmak için V8 motorunun kodunuzla nasıl çalıştığını belirlemek ve anlamak önemlidir. Temel tavsiyemizi bir kez daha hatırlatalım:
- Sorun yaşamadan (veya fark etmeden) önce hazırlıklı olun
- Ardından, sorununuzun temelini belirleyip anlayın.
- Son olarak, önemli olan sorunları düzeltin
Yani önce PageSpeed gibi diğer araçları kullanarak sorunun JavaScript'inizde olduğundan emin olmanız, metrikleri toplamadan önce muhtemelen saf JavaScript'e (DOM olmadan) indirgemeniz ve ardından bu metrikleri kullanarak darboğazları bulup önemli olanları ortadan kaldırmanız gerekir. Daniel'ın konuşmasının (ve bu makalenin), V8'in JavaScript'i nasıl çalıştırdığını daha iyi anlamanıza yardımcı olacağını umuyoruz. Ancak kendi algoritmalarınızı optimize etmeye de odaklanmayı unutmayın.