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 kita untuk "meminta lebih cepat" - untuk menganalisis perbedaan performa antara C++ dan JavaScript dengan cermat, dan menulis kode dengan mempertimbangkan 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 performa itu membuat ketagihan, dan terkadang berfokus pada saran mendalam terlebih dahulu dapat cukup mengganggu dari masalah 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 yang terlalu 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 akan mahal untuk memahami jenis JS pada waktu kompilasi. Hal ini mungkin membuat 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 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 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 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 mengalokasikan Array besar (misalnya, > 64K elemen) ke ukuran maksimumnya, tetapi tumbuhlah 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 dalam contoh pertama, setiap penetapan dilakukan satu per satu, dan penetapan a[2] menyebabkan Array dikonversi menjadi Array dari double yang tidak di-box, 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

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, 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.

Compiler Lengkap

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 compiler Lengkap menggunakan Cache Inline (IC) untuk meningkatkan pengetahuan tentang jenis saat program berjalan, sehingga meningkatkan efisiensi secara langsung.

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. 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 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 (itu adalah alasan lain mengapa monomorfisme adalah ide yang bagus di V8).

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

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 tertentu ("bail-out"). Secara khusus, compiler pengoptimal saat ini keluar dari fungsi dengan blok try {} catch {}.

Oleh karena itu

  • Masukkan 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) { // Handle exceptions here } ```

Panduan ini mungkin akan berubah pada masa mendatang, karena kami mengaktifkan blok try/catch di compiler pengoptimal. Anda dapat memeriksa cara pengoptimalan kompilator yang keluar dari fungsi menggunakan opsi "--trace-opt" dengan d8 seperti di atas, yang memberi Anda informasi selengkapnya tentang fungsi yang keluar:

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

Anda dapat, seperti pengoptimalan lainnya, mendapatkan log fungsi yang harus dideoptimalkan 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 sampling 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