Tips performa untuk JavaScript di V8

Chris Wilson
Chris Wilson

Pengantar

Daniel Clifford memberikan presentasi yang luar biasa di Google I/O tentang tips dan trik untuk meningkatkan performa JavaScript di V8. Daniel mendorong kami untuk "meminta solusi lebih cepat" - untuk menganalisis perbedaan performa antara C++ dan JavaScript dengan cermat, dan menulis kode dengan penuh pertimbangan tentang cara kerja JavaScript. Ringkasan poin-poin terpenting dari presentasi Daniel tercantum dalam artikel ini, dan kami juga akan terus memperbarui artikel ini seiring perubahan panduan performa.

Saran yang Paling Penting

Penting untuk menempatkan saran performa dalam konteks. Saran kinerja membuat ketagihan, dan terkadang berfokus pada saran mendalam terlebih dahulu bisa sangat mengalihkan masalah dari masalah yang sebenarnya. Anda perlu melihat performa aplikasi web secara menyeluruh. Sebelum berfokus pada tips performa ini, sebaiknya analisis kode Anda dengan alat seperti PageSpeed dan tingkatkan skor Anda. Hal ini akan membantu Anda menghindari pengoptimalan dini.

Saran dasar terbaik untuk mendapatkan performa yang baik di aplikasi Web adalah:

  • Bersiaplah sebelum Anda mengalami (atau melihat) masalah
  • Kemudian, identifikasi dan pahami inti masalah Anda
  • Terakhir, perbaiki hal yang penting

Untuk menyelesaikan langkah-langkah ini, Anda perlu memahami cara V8 mengoptimalkan JS, sehingga Anda dapat menulis kode dengan mempertimbangkan desain runtime JS. Penting juga untuk mempelajari alat yang tersedia dan cara alat tersebut dapat membantu Anda. Daniel menjelaskan lebih lanjut cara menggunakan alat developer dalam presentasinya; dokumen ini hanya membahas beberapa poin terpenting dari desain mesin V8.

Jadi, mari kita lanjutkan ke tips V8.

Class Tersembunyi

JavaScript memiliki informasi jenis waktu kompilasi yang terbatas: jenis dapat diubah saat runtime, sehingga wajar jika Anda mengharapkan bahwa jenis JS akan mahal untuk dipertimbangkan pada waktu kompilasi. Hal ini mungkin menyebabkan Anda bertanya-tanya bagaimana performa JavaScript bisa mendekati C++. Namun, V8 memiliki jenis tersembunyi yang dibuat secara internal untuk objek saat runtime; objek dengan class tersembunyi yang sama nantinya bisa menggunakan kode yang dihasilkan dan dioptimalkan sama.

Contoh:

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!```

Hingga instance objek p2 memiliki anggota tambahan ".z" yang ditambahkan, p1 dan p2 secara internal memiliki class tersembunyi yang sama - sehingga V8 dapat menghasilkan satu versi assembly yang dioptimalkan untuk kode JavaScript yang memanipulasi p1 atau p2. Makin banyak Anda dapat menghindari penyebab divergensi class tersembunyi, makin baik performa yang akan Anda dapatkan.

Oleh karena itu

  • Lakukan inisialisasi semua anggota objek dalam fungsi konstruktor (sehingga instance tidak mengubah jenisnya nanti)
  • Selalu lakukan inisialisasi anggota objek dalam urutan yang sama

Angka

V8 menggunakan pemberian tag untuk mewakili nilai secara efisien saat jenis dapat berubah. V8 menyimpulkan dari nilai yang Anda gunakan jenis angka yang Anda tangani. Setelah V8 membuat inferensi ini, V8 akan menggunakan pemberian tag untuk merepresentasikan nilai secara efisien, karena jenis ini dapat berubah secara dinamis. Namun, terkadang ada biaya untuk mengubah tag jenis ini, jadi sebaiknya gunakan jenis angka secara konsisten, dan secara umum, sebaiknya gunakan bilangan bulat bertanda 31-bit jika sesuai.

Contoh:

var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number```

Oleh karena itu

  • Pilih nilai numerik yang dapat direpresentasikan sebagai bilangan bulat bertanda tangan 31-bit.

Array

Untuk menangani array yang besar dan jarang, ada dua jenis penyimpanan array secara internal:

  • Elemen Cepat: penyimpanan linear untuk kumpulan kunci yang ringkas
  • Elemen Kamus: penyimpanan tabel hash jika tidak

Sebaiknya jangan menyebabkan penyimpanan array beralih dari satu jenis ke jenis lainnya.

Oleh karena itu

  • Menggunakan kunci yang berurutan dimulai dari 0 untuk Array
  • Jangan alokasikan Array besar di awal (mis. > elemen 64K) ke ukuran maksimumnya, kembangkan seiring berjalannya waktu
  • Jangan hapus elemen dalam array, terutama array numerik
  • Jangan memuat elemen yang tidak diinisialisasi atau dihapus:
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.
}

Selain itu, Array dari double lebih cepat - class tersembunyi array melacak jenis elemen, dan array yang hanya berisi double akan di-unbox (yang menyebabkan perubahan class tersembunyi). Namun, manipulasi Array yang ceroboh dapat menyebabkan pekerjaan tambahan karena boxing dan unboxing - misalnya,

var a = new Array();
a[0] = 77;   // Allocates
a[1] = 88;
a[2] = 0.5;   // Allocates, converts
a[3] = true; // Allocates, converts```

kurang efisien daripada:

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

karena pada contoh pertama, penetapan individual dilakukan satu per satu, dan penetapan a[2] menyebabkan Array dikonversi menjadi Array dari ganda yang tidak diberi kotak, tetapi kemudian penetapan a[3] menyebabkannya dikonversi kembali ke Array yang dapat berisi nilai apa pun (Angka atau objek). Dalam kasus kedua, compiler mengetahui jenis semua elemen dalam literal, dan class tersembunyi dapat ditentukan di awal.

  • Melakukan inisialisasi menggunakan literal array untuk array berukuran tetap yang kecil
  • Melakukan pra-alokasi array kecil (<64k) untuk memperbaiki ukuran sebelum menggunakannya
  • Jangan menyimpan nilai non-numerik (objek) dalam array numerik
  • Berhati-hatilah agar tidak menyebabkan konversi ulang array kecil jika Anda melakukan inisialisasi tanpa literal.

Kompilasi JavaScript

Walaupun JavaScript adalah bahasa yang sangat dinamis, dan implementasi aslinya adalah penafsir, mesin waktu proses JavaScript modern menggunakan kompilasi. V8 (JavaScript Chrome) memiliki dua compiler Just-In-Time (JIT) yang berbeda, yaitu:

  • Compiler "Lengkap", yang dapat menghasilkan kode yang baik untuk JavaScript apa pun
  • Compiler Optimizing, yang menghasilkan kode yang bagus untuk sebagian besar JavaScript, tetapi membutuhkan waktu lebih lama untuk dikompilasi.

Kompilator Sepenuhnya

Di V8, compiler Lengkap berjalan di semua kode, dan mulai mengeksekusi kode sesegera mungkin, dengan cepat menghasilkan kode yang baik, tetapi tidak bagus. Compiler ini hampir tidak mengasumsikan apa pun tentang jenis pada waktu kompilasi - compiler ini mengharapkan bahwa jenis variabel dapat dan akan berubah saat runtime. Kode yang dihasilkan oleh Full compiler menggunakan Inline Caches (IC) untuk meningkatkan pengetahuan tentang jenis saat program berjalan, sehingga meningkatkan efisiensi dengan cepat.

Tujuan Cache Inline adalah untuk menangani jenis secara efisien, dengan meng-cache kode yang bergantung pada jenis untuk operasi; saat kode berjalan, kode akan memvalidasi asumsi jenis terlebih dahulu, lalu menggunakan cache inline untuk mempersingkat operasi. Namun, hal ini berarti operasi yang menerima beberapa jenis akan memiliki performa yang lebih rendah.

Oleh karena itu

  • Penggunaan operasi monomorf lebih disukai daripada operasi polimorf

Operasi bersifat monomorfik jika class input tersembunyi selalu sama. Jika tidak, operasi bersifat polimorfik, yang berarti beberapa argumen dapat mengubah jenis di berbagai panggilan ke operasi. Misalnya, panggilan add() kedua dalam contoh ini menyebabkan polimorfisme:

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

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

Optimizing Compiler

Secara paralel dengan compiler lengkap, V8 mengompilasi ulang fungsi "hot" (yaitu, fungsi yang dijalankan berkali-kali) dengan compiler pengoptimal. Kompilator ini menggunakan jenis respons untuk membuat kode yang dikompilasi lebih cepat - bahkan, ia menggunakan jenis yang diambil dari IC yang baru saja kita bicarakan!

Dalam compiler pengoptimal, operasi akan disisipkan secara spekulatif (langsung ditempatkan di tempat operasi dipanggil). Hal ini mempercepat eksekusi (dengan mengorbankan jejak memori), tetapi juga memungkinkan pengoptimalan lainnya. Fungsi dan konstruktor monomorfik dapat disisipkan sepenuhnya (itulah alasan lain mengapa monomorfisme adalah ide yang bagus di V8).

Anda dapat mencatat apa yang dioptimalkan menggunakan mesin V8 versi "d8" mandiri:

d8 --trace-opt primes.js

(tindakan ini akan mencatat nama fungsi yang dioptimalkan ke stdout.)

Namun, tidak semua fungsi dapat dioptimalkan - beberapa fitur mencegah compiler pengoptimalan berjalan pada fungsi yang diberikan ("bail-out"). Secara khusus, compiler pengoptimalan saat ini mengurangi fungsi dengan blok try {} catch {}!

Oleh karena itu

  • Masukkan kode yang sensitif terhadap perf ke dalam fungsi tersarang jika Anda telah mencoba {} catch {} block: ```js function perf_sensitive() { // Do performance-sensitive work here }

coba { perf_sensitive() } catch (e) { // Tangani pengecualian di sini } ```

Panduan ini mungkin akan berubah di masa mendatang, karena kami mengaktifkan blok coba/tangkap dalam compiler pengoptimalan. Anda dapat memeriksa bagaimana compiler pengoptimalan mengoptimalkan fungsi dengan menggunakan opsi "--trace-opt" dengan d8 seperti di atas, yang memberi Anda informasi lebih lanjut tentang fungsi mana yang dibebaskan:

d8 --trace-opt primes.js

De-pengoptimalan

Terakhir, pengoptimalan yang dilakukan oleh compiler ini bersifat spekulatif - terkadang tidak berhasil, dan kita mundur. Proses "de-pengoptimalan" akan menghapus kode yang dioptimalkan, dan melanjutkan eksekusi di tempat yang tepat dalam kode compiler "lengkap". Pengoptimalan ulang mungkin dipicu lagi nanti, tetapi untuk jangka pendek, eksekusi akan melambat. Secara khusus, menyebabkan perubahan pada class variabel tersembunyi setelah fungsi dioptimalkan akan menyebabkan de-pengoptimalan ini terjadi.

Oleh karena itu

  • Menghindari perubahan class tersembunyi dalam fungsi setelah dioptimalkan

Seperti halnya pengoptimalan lainnya, Anda bisa mendapatkan log fungsi yang harus dibatalkan pengoptimalan V8 dengan flag logging:

d8 --trace-deopt primes.js

Alat V8 Lainnya

Selain itu, Anda juga dapat meneruskan opsi pelacakan V8 ke Chrome saat memulai:

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

Selain menggunakan pembuatan profil alat developer, Anda juga dapat menggunakan d8 untuk membuat profil:

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

Ini menggunakan profiler pengambilan sampel bawaan, yang mengambil sampel setiap milidetik dan menulis v8.log.

Ringkasan

Anda harus mengidentifikasi dan memahami cara kerja mesin V8 dengan kode Anda untuk bersiap mem-build JavaScript yang berperforma tinggi. Sekali lagi, saran dasarnya adalah:

  • Bersiaplah sebelum Anda mengalami (atau melihat) masalah
  • Kemudian, identifikasi dan pahami inti masalah Anda
  • Terakhir, perbaiki hal yang penting

Artinya, Anda harus memastikan masalahnya ada di JavaScript, dengan menggunakan alat lain seperti PageSpeed terlebih dahulu; mungkin menguranginya menjadi JavaScript murni (tanpa DOM) sebelum mengumpulkan metrik, lalu menggunakan metrik tersebut untuk menemukan bottleneck dan menghilangkan bottleneck yang penting. Semoga presentasi Daniel (dan artikel ini) akan membantu Anda lebih memahami cara V8 menjalankan JavaScript - tetapi pastikan untuk berfokus juga pada pengoptimalan algoritme Anda sendiri.

Referensi