V8'de JavaScript için performans ipuçları

Chris Wilson
Chris Wilson

Giriş

Daniel Clifford, Google I/O'da harika bir konuşma yaparak V8'de JavaScript performansını iyileştirmeye yönelik ipuçları ve püf noktaları hakkında bilgi verdi. Daniel, C++ ile JavaScript arasındaki performans farklılıklarını dikkatli bir şekilde analiz etmemiz ve JavaScript'in nasıl çalıştığı konusunda dikkatli bir şekilde kod yazmamız için bizi "daha hızlı talep etmeye" teşvik etti. Doğan'ın konuşmasının en önemli noktalarının özeti bu makalede ele alınmıştır. Ayrıca, performans kılavuzu değiştikçe bu makaleyi de güncelleyeceğiz.

En Önemli Tavsiye

Her performans önerisini bir bağlama oturtmak önemlidir. Performans tavsiyeleri bağımlılık yapar ve bazen ilk önce derin tavsiyelere odaklanmak, gerçek sorunlardan epey dikkat dağıtıcı olabilir. Web uygulamanızın performansına ilişkin bütünsel bir bakış açısı edinmeniz gerekir. Bu performans ipuçlarına odaklanmadan önce büyük olasılıkla kodunuzu PageSpeed gibi araçlarla analiz etmeniz ve puanınızı yükseltmeniz gerekir. Böylece, erken optimizasyon yapmaktan kaçınabilirsiniz.

Web uygulamalarında iyi bir performans elde etmek için en iyi temel öneri şudur:

  • Bir sorun yaşamadan (veya fark etmeden) önce hazırlıklı olun
  • Ardından, sorununuzun en önemli noktasını belirleyin ve anlayın
  • Son olarak, önemli noktaları

Bu adımları gerçekleştirmek için, V8'in JS'yi nasıl optimize ettiğini anlamak önemli olabilir. Böylece, JS çalışma zamanı tasarımına dikkat ederek kod yazabilirsiniz. Kullanabileceğiniz araçlar ve bunların size nasıl yardımcı olabilecekleri hakkında bilgi edinmeniz de önemlidir. Daniel, konuşmasında geliştirici araçlarının nasıl kullanılacağını daha açıklıyor; bu belgede yalnızca V8 motor tasarımının en önemli noktalarından bazıları yer alıyor.

O halde, V8 ipuçları!

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ürleriyle ilgili neden çok yüksek maliyetli olması normaldir. Bu durum, JavaScript performansının C++'ya nasıl yaklaşabileceğini sorgulamanıza neden olabilir. Ancak, V8'de çalışma zamanında nesneler için dahili olarak oluşturulmuş gizli türler vardır. Aynı gizli sınıfa sahip nesneler, optimize edilmiş aynı 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ğinin ek üyesi ".z" eklenene kadar, p1 ve p2 dahili olarak aynı gizli sınıfa sahip olur. Böylece V8, p1 veya p2'ye müdahale eden JavaScript kodu için optimize edilmiş derlemenin tek bir sürümünü oluşturabilir. Gizli sınıfların birbirinden ayrılmasına neden olan süreyi ne kadar önlerseniz aldığınız performans da o kadar iyi olur.

Bu nedenle

  • Oluşturucu işlevlerdeki tüm nesne üyelerini başlatır (böylece örneklerin türü daha sonra değişmez)
  • Nesne üyelerini her zaman aynı sırayla başlat

Numbers

V8, türler değişebildiğinde değerleri verimli bir şekilde temsil etmek için etiketlemeyi kullanır. V8, üzerinde çalıştığınız sayı türünü kullandığınız değerlerden çıkarım yapar. V8 bu çıkarımı yaptıktan sonra, değerleri verimli bir şekilde temsil etmek için etiketlemeden yararlanır, çünkü bu türler dinamik olarak değişebilmektedir. Bununla birlikte, bazen bu tür etiketlerini değiştirmenin bir maliyeti vardır. Dolayısıyla, sayı türlerini tutarlı bir şekilde kullanmak ve genel olarak, uygun olduğu durumlarda 31 bit imzalı tam sayıların kullanılması en iyi yöntemdir.

Ö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 imzalı tam sayılar olarak temsil edilebilen sayısal değerleri tercih edin.

Diziler

Büyük ve az miktarda dizileri işlemek için dahili olarak iki tür dizi depolaması vardır:

  • Hızlı Öğeler: Kompakt anahtar grupları için doğrusal depolama
  • Sözlük Öğeleri: aksi takdirde karma tablo depolaması

Dizi depolama alanının bir türden diğerine geçiş yapmaması en iyisidir.

Bu nedenle

  • Diziler için 0'dan başlayan bitişik anahtarlar kullanın
  • Büyük Dizileri (ör.64 binden fazla öğe) maksimum boyutlarına göre önceden tahsis etmeyin. Bunun yerine, dizi büyüdükçe büyürler
  • Dizilerdeki öğeleri, özellikle sayısal dizileri silmeyin
  • İlk kullanıma hazırlanmamış 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 dizileri daha hızlıdır. Dizinin gizli sınıf izleri, yalnızca çiftler içeren dizilerin kutudan çıkarılmasına neden olur (bu da gizli sınıf değişikliğine neden olur). Bununla birlikte, dizilerin dikkatsizce değiştirilmesi, kutulama ve kutu açılımı nedeniyle ek iş yüküne 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 etkilidir:

var a = [77, 88, 0.5, true];

Çünkü ilk örnekte bağımsız atamalar birbiri ardına yapılır ve a[2] ataması, Dizinin kutu açılmış çiftler dizisine dönüştürülmesine neden olur. Ancak, a[3] ataması, herhangi bir değer (Sayılar veya nesneler) içerebilen 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.

  • Sabit boyutlu küçük diziler için dizi değişmez değerlerini kullanarak başlatma
  • Küçük dizileri (<64 k) kullanmadan önce doğru boyutta olacak şekilde önceden ayırın
  • Sayısal olmayan değerleri (nesneler) 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ı olsa da, modern JavaScript çalışma zamanı motorları derlemeyi kullanır. V8'in (Chrome JavaScript'i) iki farklı Just-In-Time (JIT) derleyicisi vardır:

  • Tüm JavaScript kodları için iyi kod oluşturabilen "Tam" derleyici
  • Çoğu JavaScript için mükemmel kod üreten ancak derlenmesi daha uzun süren Optimize 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. Böylece, hızlı bir şekilde iyi ama mükemmel olmayan kod üretilir. Bu derleyici, derleme zamanında türler hakkında neredeyse hiçbir şey tahmin etmez; değişken türlerinin çalışma zamanında değişebileceğini ve değişmesini bekler. Tam derleyici tarafından oluşturulan kod, program çalışırken türler hakkındaki bilgiyi hassaslaştırarak çalışma sırasında verimliliği artırmak için Satır İçi Önbellekleri (IC'ler) kullanır.

Satır İçi Önbelleklerin amacı, işlemler için türe bağlı kodu önbelleğe alarak türleri verimli bir şekilde işlemektir. Kod çalıştığında önce tür varsayımlarını doğrular, ardından işlemi kısayol haline getirmek için satır içi önbelleği kullanır. Ancak bu, birden fazla türü kabul eden işlemlerin daha az performans göstereceği anlamına gelir.

Bu nedenle

  • İşlemlerin polimorfik işlemlere göre monomorfik kullanımı tercih edilir

Gizli giriş sınıfları her zaman aynıysa işlemler monomorfiktir. Aksi takdirde, polimorfiktirler. Diğer bir deyişle, bağımsız değişkenlerden bazılarının işleme yönelik farklı çağrılarda türü değişebilir. Örneğin, bu örnekteki ikinci add() çağrısı çok biçimliliğe neden olur:

function add(x, y) {
  return x + y;
}

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

Optimize Etme Derleyici

V8, tam derleyiciye paralel olarak "sıcak" işlevleri (yani, birçok kez çalıştırılan işlevleri) bir optimize edici derleyici ile yeniden derler. Bu derleyici, derlenen kodu daha hızlı hale getirmek için tür geri bildirimi kullanır. Hatta, az önce bahsettiğimiz IC'lerden alınan türleri kullanır.

Optimizasyon derleyicisinde, işlemler kurgusal olarak satır içi olur (doğrudan çağrıldıkları yere yerleştirilir). Bu, yürütmeyi hızlandırır (bellek ayak izi pahasına) ancak başka optimizasyonlar da sağlar. Monomorfik işlevler ve kurucular tamamen satır içine alınabilir (bu nedenle, V8'de monomorfizmin iyi bir fikir olmasının başka bir 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 işlem, optimize edilmiş işlevlerin adlarını stdout'a kaydeder.)

Tüm işlevler optimize edilemez. Ancak bazı özellikler, optimize edici derleyicinin belirli bir işlevde ("kurtarma") çalışmasını engeller. Özellikle, optimizasyon derleyicisi şu anda try {} capture {} bloklarıyla işlevlerden yararlanmaktadır.

Bu nedenle

  • {} catch {} bloklarını denemeniz durumunda performansa duyarlı bir kodu iç içe geçmiş bir işleve yerleştirin: ```js function perf_sensitive() { // Performans açısından hassas işlem yapın }

try { perf_sensitive() } capture (e) { // İstisnaları burada ele alın } ```

Optimizasyon derleyicisinde dene-yakala bloklarını etkinleştirdiğimiz için bu kılavuz muhtemelen ileride değişecektir. Yukarıda açıklandığı gibi d8 ile "--trace-opt" seçeneğini kullanarak optimize derleyicinin işlevlerden nasıl yararlandığını inceleyebilirsiniz. Bu seçenek, hangi işlevlerin kurtarıldığı hakkında size daha fazla bilgi verir:

d8 --trace-opt primes.js

Optimizasyonu iptal etme

Son olarak, bu derleyici tarafından gerçekleştirilen optimizasyon spekülatiftir; bazen işe yaramaz ve biz de bu işlemi durdururuz. "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 ama 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ı, optimizasyonun kaldırılmasına neden olur.

Bu nedenle

  • Optimize edilen işlevlerde gizli sınıf değişikliklerinden kaçınma

Diğer optimizasyonlarda olduğu gibi, bir günlük kaydı işaretiyle V8'in optimizasyondan kaldırılması gereken işlevlerin günlüğünü alabilirsiniz:

d8 --trace-deopt primes.js

Diğer V8 Araçları

Bu arada, V8 izleme seçeneklerini başlangıçta Chrome'a da geçirebilirsiniz:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```

d8'i, geliştirici araçlarının profilini çıkarmanın yanı sıra profil oluşturma işlemi için de kullanabilirsiniz:

% out/ia32.release/d8 primes.js --prof

Bu özellik, her milisaniyede bir örnek alıp v8.log yazan yerleşik örnekleme profil aracını kullanır.

Özet

Etkili JavaScript derlemeye hazırlanmak için V8 motorunun kodunuzla nasıl çalıştığını tanımlamanız ve anlamanız önemlidir. Bir kez daha temel öneri şu şekildedir:

  • Bir sorun yaşamadan (veya fark etmeden) önce hazırlıklı olun
  • Ardından, sorununuzun en önemli noktasını belirleyin ve anlayın
  • Son olarak, önemli noktaları

Yani, önce PageSpeed gibi başka araçları kullanarak sorunun JavaScript'inizde olduğundan emin olmanız gerekir. Metrikleri toplamadan önce sadece JavaScript'e (DOM olmadan) indirgemeli ve daha sonra, performans sorunlarını bulup önemli olanları ortadan kaldırmak için bu metrikleri kullanabilirsiniz. Umarız Daniel'ın konuşması (ve bu makale) V8'in JavaScript'i nasıl çalıştırdığını daha iyi anlamanıza yardımcı olur. Ancak, kendi algoritmalarınızı da optimize etmeye odaklandığınızdan emin olun!

Referanslar