Giriş
Daniel Clifford, V8'de JavaScript performansını iyileştirmeyle ilgili ipuçları ve püf noktaları hakkında Google I/O'da mükemmel bir konuşma yaptı. Daniel, C++ ile JavaScript arasındaki performans farklılıklarını dikkatlice analiz etmemizi ve JavaScript'in işleyiş şeklini göz önünde bulundurarak kod yazmamızı "daha hızlı olmamızı" istedi. Daniel'in konuşmasının en önemli noktalarının özeti bu makalede yer almaktadır. Ayrıca, performansla ilgili yönergeler değiştikçe bu makaleyi de güncelleyeceğiz.
En Önemli Tavsiye
Performansla ilgili tüm tavsiyeleri bağlama yerleştirmek önemlidir. Performansla ilgili tavsiyeler bağımlılık yapar ve bazen önce derin tavsiyelere odaklanmak, asıl sorunlardan oldukça fazla dikkati dağıtabilir. Web uygulamanızın performansını bütünsel bir şekilde değerlendirmeniz gerekir. Bu performans ipuçlarına odaklanmadan önce, kodunuzu PageSpeed gibi araçlarla analiz edip puanınızı yükseltmeniz gerekir. 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 temel nedenini belirleyip anlayın.
- Son olarak, önemli olan sorunları düzeltin
Bu adımları tamamlamak için V8'in JS'yi nasıl optimize ettiğini anlamak önemlidir. Böylece JS çalışma zamanı tasarımını göz önünde bulundurarak kod yazabilirsiniz. Kullanabileceğiniz araçlar ve bu araçların size nasıl yardımcı olabileceği hakkında bilgi edinmeniz de önemlidir. Daniel, konuşmasında geliştirici araçlarının nasıl kullanılacağı hakkında daha fazla bilgi veriyor. Bu dokümanda, V8 motor tasarımının en önemli noktalarından bazıları ele alınmaktadır.
V8 ile ilgili ipuçlarına geçelim.
Gizli sınıflar
JavaScript'in 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 pahalı olmasını beklemek doğaldır. Bu durum, JavaScript performansının nasıl C++'ya 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şturulmuş 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!```
Nesne örneği p2'ye ek ".z" üyesi eklenene kadar p1 ve p2 dahili olarak aynı gizli sınıfa sahiptir. Bu nedenle 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 birbirinden ayrılmasına ne kadar az neden olursanız o kadar iyi performans elde edersiniz.
Bu nedenle
- Tüm nesne üyelerini oluşturucu işlevlerde başlatın (böylece örneklerin türü daha sonra değişmez)
- Nesne üyelerini her zaman aynı sırayla başlatın
Numbers
V8, türler değişebileceğinde değerleri verimli bir şekilde temsil etmek için etiketleme kullanır. V8, kullandığınız değerlerden hangi sayı türüyle uğraştığınızı anlar. V8 bu çıkarım yapıldıktan sonra, bu türler dinamik olarak değişebileceğinden değerleri verimli bir şekilde temsil etmek için etiketlemeyi kullanır. Ancak bu tür etiketlerin değiştirilmesinin bazen maliyeti olduğundan sayı türlerini tutarlı bir şekilde kullanmak en iyisidir. Genel olarak, uygun olduğunda 31 bitlik işaretli tam sayılar kullanmak en uygun seçenektir.
Ö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 bitlik işaretli tam sayı olarak temsil edilebilecek sayısal değerlere öncelik verin.
Diziler
Büyük ve seyrek dizileri işlemek için dahili olarak iki tür dizi depolama alanı vardır:
- Hızlı Öğeler: Kompakt anahtar kümeleri için doğrusal depolama
- Sözlük Öğeleri: Aksi takdirde karma tablo depolama alanı
Dizi depolama alanının bir türden diğerine geçmesine neden olmamak en iyisidir.
Bu nedenle
- Diziler için 0'dan başlayan bitişik anahtarlar kullanın
- Büyük dizileri (ör. > 64K öğe) maksimum boyutlarına göre önceden ayırmayın. Bunun yerine, ilerledikçe boyutlarını artırın.
- Dizilerdeki öğeleri (özellikle de sayısal dizileri) silmeyin
- İlkleştirilmemiş 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, çiftler içeren diziler daha hızlıdır. Dizilerin gizli sınıfı, öğe türlerini izler ve yalnızca çiftler içeren dizilerin kutusu açılır (bu da gizli sınıf değişikliğine neden olur). Ancak, dizilerin dikkatsizce değiştirilmesi, kutuya alma ve kutudan çıkarma işlemleri nedeniyle ek işe neden olabilir.
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts```
Aşağıdakilerden daha az verimlidir:
var a = [77, 88, 0.5, true];
çünkü ilk örnekte bağımsız atamalar birbiri ardına gerçekleştirilir ve a[2]
ataması, dizinin kutusuz çiftler dizisine dönüştürülmesine neden olur ancak ardından a[3]
ataması, dizinin herhangi bir değeri (sayı veya nesne) içerebilen bir diziye yeniden dönüştürülmesine neden olur. İkinci durumda, derleyici, değişmez ifadedeki tüm öğelerin türlerini bilir ve gizli sınıf önceden belirlenebilir.
- Küçük sabit boyutlu diziler için dizi değişmezlerini kullanarak başlatma
- Küçük dizileri (64 KB'tan küçük) kullanmadan önce doğru boyuta önceden ayırın
- Sayısal olmayan değerleri (nesneler) sayısal dizilerde saklamayın
- Başlangıç değerini değişmez değer olmadan atarsanı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ında yorumlayıcılar kullanılmasına rağmen modern JavaScript çalışma zamanı motorları derleme kullanır. V8'in (Chrome'un JavaScript'i) aslında iki farklı Tam Zamanında (JIT) derleyicisi vardır:
- Herhangi bir JavaScript için iyi kod oluşturabilen "Tam" derleyici
- Çoğu JavaScript için mükemmel kodlar üreten ancak derlemesi daha uzun süren Optimize edici derleyici.
Tam Derleyici
V8'de tam derleyici tüm kodda çalışır ve mümkün olan en kısa sürede kodu yürütmeye başlar. Böylece, hızlı bir şekilde iyi ancak mükemmel olmayan kodlar oluşturur. Bu derleyici, derleme sırasında türlerle ilgili neredeyse hiçbir şey varsayar. 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ürlerle ilgili bilgileri hassaslaştırmak için satır içi önbellekleri (IC'ler) kullanır ve bu sayede verimliliği anında artırır.
Satır içi önbelleğe alma işlemlerinin amacı, işlemler için türe bağlı kodu önbelleğe alarak türleri verimli bir şekilde ele almaktır. 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
- Polimorfik işlemlere kıyasla işlemlerin tek biçimli kullanımı tercih edilir.
Girişlerin gizli sınıfları her zaman aynıysa işlemler tek biçimlidir. Aksi takdirde, işlemler çok biçimlidir. Yani bağımsız değişkenlerden bazıları, işleme yapılan farklı çağrılarda türünü 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 Eden Derleyici
V8, tam derleyiciye paralel olarak "sıcak" işlevleri (yani birçok kez çalıştırılan işlevleri) optimize edici bir derleyiciyle yeniden derler. Bu derleyici, derlenmiş kodu hızlandırmak için tür geri bildirimini kullanır. Aslında, daha önce bahsettiğimiz IC'lerden alınan türleri kullanır.
Optimizasyon yapan derleyicide işlemler spekülatif 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 (bellek kullanımı pahasına) ancak diğer optimizasyonları da sağlar. Tek biçimli işlevler ve kurucular tamamen satır içi olarak yerleştirilebilir (tek biçimliliğin V8'de iyi bir fikir olmasının bir diğer nedeni de budur).
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, optimize edici derleyicinin belirli bir işlevde çalışmasını engeller ("vazgeçme"). Özellikle de optimize edici derleyici ş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 koyun: ```js function perf_sensitive() { // Performansa duyarlı işlemleri burada yapın }
try { perf_sensitive() } catch (e) { // İstisnaları burada yönetin } ```
Optimizasyon yapan derleyicide try/catch bloklarını etkinleştirdiğimiz için bu kılavuz gelecekte muhtemelen değişecektir. Yukarıdaki gibi d8 ile "--trace-opt" seçeneğini kullanarak optimize edici derleyicinin işlevlerde nasıl vazgeçtiğini inceleyebilirsiniz. Bu seçenek, hangi işlevlerin vazgeçildiği hakkında daha fazla bilgi verir:
d8 --trace-opt primes.js
De-optimizasyon
Son olarak, bu derleyici tarafından gerçekleştirilen optimizasyon tahminidir. Bazen işe yaramaz ve geri çekiliriz. "Optimizasyondan vazgeçme" işlemi, optimize edilmiş kodu atar ve yürütmeyi "tam" derleyici kodunda doğru yerden devam ettirir. Yeniden optimizasyon daha sonra tekrar tetiklenebilir ancak kısa vadede yürütme yavaşlar. Özellikle, işlevler optimize edildikten sonra gizli değişken sınıflarında değişiklik yapılması bu tür bir optimizasyondan vazgeçmeye neden olur.
Bu nedenle
- Optimize edildikten sonra işlevlerde gizli sınıf değişikliklerinden kaçının
Diğer optimizasyonlarda olduğu gibi, V8'in bir günlük işaretiyle optimize etmeyi bırakması gereken işlevlerin günlüğünü alabilirsiniz:
d8 --trace-deopt primes.js
Diğer V8 Araçları
Ayrıca, V8 izleme seçeneklerini Chrome'a başlangıçta da iletebilirsiniz:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```
Geliştirici araçları profil oluşturma özelliğinin yanı sıra d8'i de kullanarak profil oluşturabilirsiniz:
% out/ia32.release/d8 primes.js --prof
Bu işlemde, her milisaniyede bir örnek alan ve v8.log dosyasını yazan yerleşik örnekleme profilleyici kullanılır.
Özet
Performanslı JavaScript oluşturmaya hazırlanmak için V8 motorunun kodunuzla nasıl çalıştığını belirlemeniz ve anlamanız önemlidir. Tekrar belirtmek gerekirse temel tavsiye şudur:
- Sorun yaşamadan (veya fark etmeden) önce hazırlıklı olun
- Ardından, sorununuzun temel nedenini 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 olmalısınız. Daha sonra metrikleri toplarken saf JavaScript'e (DOM yok) geçebilir ve ardından bu metrikleri kullanarak darboğazları tespit edip önemli olanları ortadan kaldırabilirsiniz. Daniel'in 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.