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 "menuntut lebih cepat" - untuk menganalisis perbedaan performa antara C++ dan JavaScript dengan cermat, serta menulis kode dengan memperhatikan cara kerja JavaScript. Ringkasan 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 apa pun ke dalam konteks. Saran performa membuat ketagihan, dan terkadang berfokus pada saran mendalam terlebih dahulu dapat mengalihkan perhatian dari masalah yang sebenarnya. Anda perlu melihat performa aplikasi web Anda 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 menyadari) masalah
  • Kemudian, identifikasi dan pahami inti masalah Anda
  • Terakhir, perbaiki hal yang penting

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

Jadi, mari kita bahas tips V8.

Kelas tersembunyi

JavaScript memiliki informasi jenis waktu kompilasi yang terbatas: jenis dapat diubah saat runtime, sehingga wajar jika kita menganggap bahwa penalaran jenis JS saat waktu kompilasi memerlukan biaya yang besar. Hal ini mungkin membuat Anda mempertanyakan 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 kemudian dapat menggunakan kode yang dihasilkan dan dioptimalkan yang 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 Anda dapat menghindari terjadinya perbedaan class tersembunyi, makin baik performa yang akan Anda peroleh.

Oleh karena itu,

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

Angka

V8 menggunakan pemberian tag untuk merepresentasikan nilai secara efisien saat jenis dapat berubah. V8 menyimpulkan dari nilai yang Anda gunakan, jenis angka yang Anda hadapi. Setelah membuat inferensi ini, V8 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 31-bit.

Array

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

  • Fast Elements: penyimpanan linear untuk set 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 berdekatan yang dimulai dari 0 untuk Array
  • Jangan mengalokasikan Array besar (misalnya > 64K elemen) ke ukuran maksimumnya, tetapi kembangkan seiring berjalannya waktu
  • Jangan menghapus elemen dalam array, terutama array numerik
  • Jangan memuat elemen yang belum 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 ganda lebih cepat - class tersembunyi array melacak jenis elemen, dan array yang hanya berisi ganda akan dibuka (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 dalam contoh pertama, penetapan individual dilakukan satu per satu, dan penetapan a[2] menyebabkan Array dikonversi menjadi Array double yang tidak di-unboxing, tetapi kemudian penetapan a[3] menyebabkannya dikonversi ulang kembali menjadi 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
  • Pra-alokasikan array kecil (<64k) ke ukuran yang benar 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

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

  • Compiler "Full", yang dapat menghasilkan kode yang baik untuk JavaScript apa pun
  • Compiler pengoptimalan, yang menghasilkan kode yang bagus untuk sebagian besar JavaScript, tetapi memerlukan waktu lebih lama untuk mengompilasi.

Compiler lengkap

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

Tujuan Inline Cache adalah 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 inline cache untuk mempersingkat operasi. Namun, ini berarti operasi yang menerima beberapa jenis akan kurang berperforma.

Oleh karena itu,

  • Penggunaan operasi monomorfik lebih disukai daripada operasi polimorfik

Operasi bersifat monomorfik jika class input tersembunyi selalu sama. Jika tidak, operasi bersifat polimorfik, yang berarti beberapa argumen dapat berubah 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```

Compiler pengoptimalan

Sejalan dengan compiler lengkap, V8 mengompilasi ulang fungsi "hot" (yaitu, fungsi yang dijalankan berkali-kali) dengan compiler pengoptimalan. Compiler ini menggunakan masukan jenis untuk membuat kode yang dikompilasi lebih cepat - bahkan, compiler ini menggunakan jenis yang diambil dari IC yang baru saja kita bahas.

Dalam compiler pengoptimalan, operasi di-inline secara spekulatif (ditempatkan langsung di tempat operasi tersebut dipanggil). Hal ini mempercepat eksekusi (dengan mengorbankan jejak memori), tetapi juga memungkinkan pengoptimalan lainnya. Fungsi dan konstruktor monomorfik dapat di-inline 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 pengoptimal berjalan pada fungsi tertentu ("bail-out"). Secara khusus, compiler pengoptimalan saat ini tidak dapat menangani fungsi dengan blok try {} catch {}.

Oleh karena itu,

  • Letakkan kode yang sensitif terhadap performa ke dalam fungsi bertingkat jika Anda memiliki blok try {} catch {}: ```js function perf_sensitive() { // Lakukan pekerjaan yang sensitif terhadap performa di sini }

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

Panduan ini mungkin akan berubah di masa mendatang, seiring kami mengaktifkan blok try/catch di compiler pengoptimalan. Anda dapat memeriksa cara compiler pengoptimalan membatalkan fungsi dengan menggunakan opsi "--trace-opt" dengan d8 seperti di atas, yang memberi Anda informasi lebih lanjut tentang fungsi mana yang dibatalkan:

d8 --trace-opt primes.js

De-pengoptimalan

Terakhir, pengoptimalan yang dilakukan oleh compiler ini bersifat spekulatif - terkadang tidak berhasil, dan kami mundur. Proses "de-pengoptimalan" membuang kode yang dioptimalkan, dan melanjutkan eksekusi di tempat yang tepat dalam kode compiler "penuh". Pengoptimalan ulang mungkin akan 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 pengoptimalan lainnya, Anda bisa mendapatkan log fungsi yang harus dideoptimalkan oleh V8 dengan flag logging:

d8 --trace-deopt primes.js

Alat V8 lainnya

Ngomong-ngomong, 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

Tindakan ini menggunakan profiler sampel bawaan, yang mengambil sampel setiap milidetik dan menulis v8.log.

Ringkasan

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

  • Bersiaplah sebelum Anda mengalami (atau menyadari) 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 mengurangi ke JavaScript murni (tanpa DOM) sebelum mengumpulkan metrik, lalu menggunakan metrik tersebut untuk menemukan bottleneck dan menghilangkan yang penting. Semoga pembahasan Daniel (dan artikel ini) akan membantu Anda lebih memahami cara V8 menjalankan JavaScript - tetapi pastikan untuk berfokus pada pengoptimalan algoritme Anda sendiri juga.

Referensi