Tips performa untuk JavaScript di V8

Chris Wilson
Chris Wilson

Pengantar

Daniel Clifford memberikan ceramah yang sangat baik di Google I/O tentang tips dan trik untuk meningkatkan performa JavaScript di V8. Daniel mendorong kami untuk "meminta 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 pembahasan Daniel tercantum dalam artikel ini, dan kami juga akan terus memperbarui artikel ini saat panduan performa berubah.

Saran Paling Penting

Saran performa apa pun harus disertakan dalam konteks. Saran kinerja bersifat ketagihan, dan terkadang berfokus pada saran mendalam terlebih dahulu bisa sangat mengalihkan perhatian dari masalah yang sebenarnya. Anda perlu memahami performa aplikasi web secara menyeluruh - sebelum berfokus pada tips performa ini, sebaiknya analisis kode dengan alat seperti PageSpeed dan tingkatkan skor Anda. Tindakan ini akan membantu Anda menghindari pengoptimalan prematur.

Saran dasar terbaik untuk mendapatkan kinerja yang baik dalam aplikasi Web adalah:

  • Bersiaplah sebelum Anda mengalami (atau melihat) masalah
  • Kemudian, identifikasi dan pahami inti dari 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. Anda juga perlu mempelajari alat yang tersedia dan bagaimana 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, lanjut ke tips V8!

Class Tersembunyi

JavaScript memiliki informasi jenis waktu kompilasi yang terbatas: jenis dapat diubah saat runtime, jadi wajar jika diperkirakan akan mahal untuk menjelaskan jenis JS pada waktu kompilasi. 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 dapat menggunakan kode hasil pengoptimalan 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!```

Sampai 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 penyimpangan class tersembunyi, makin baik performa yang akan Anda peroleh.

Oleh karena itu

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

Numbers

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 V8 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 akan lebih optimal menggunakan bilangan bulat dengan tanda tangan 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 dengan tanda tangan 31-bit.

Array

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

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

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

Oleh karena itu

  • Menggunakan kunci berurutan mulai dari 0 untuk Array
  • Jangan mengalokasikan Array besar (mis. > 64K) ke ukuran maksimumnya terlebih dahulu, tumbuh seiring perkembangan Anda
  • Jangan hapus elemen dalam array, terutama array numerik
  • Jangan muat 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 yang terdiri dari double lebih cepat - jenis elemen pelacakan class tersembunyi dari array, dan array yang hanya berisi double akan dibuka (yang menyebabkan perubahan class tersembunyi). Namun, manipulasi Array yang tidak hati-hati dapat menyebabkan pekerjaan tambahan karena proses 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 dibandingkan:

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

karena dalam contoh pertama, masing-masing penetapan dilakukan satu per satu, dan penetapan a[2] menyebabkan Array dikonversi menjadi Array double yang tidak ditempatkan, tetapi penetapan a[3] menyebabkannya dikonversi 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
  • Mengalokasikan array kecil (<64k) untuk memperbaiki ukuran sebelum menggunakannya
  • Jangan menyimpan nilai non-numerik (objek) dalam array numerik
  • Hati-hati 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, sebenarnya:

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

Compiler Lengkap

Di V8, compiler Lengkap berjalan pada semua kode, dan mulai mengeksekusi kode sesegera mungkin, dengan cepat menghasilkan kode yang bagus tetapi tidak bagus. Compiler ini 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 Full menggunakan Inline Cache (IC) untuk menyempurnakan 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 tersebut akan memvalidasi asumsi jenis terlebih dahulu, lalu menggunakan cache inline untuk pintasan operasi. Namun, hal ini berarti operasi yang menerima beberapa jenis akan berperforma kurang baik.

Oleh karena itu

  • Penggunaan operasi monomorfik lebih disukai daripada operasi polimorfik

Operasi bersifat monomorfis jika class input tersembunyi selalu sama - jika tidak, operasi tersebut polimorfik, artinya beberapa argumen dapat berubah jenis di berbagai panggilan 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```

Optimization Compiler

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

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

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

d8 --trace-opt primes.js

(ini mencatat nama fungsi yang dioptimalkan ke stdout.)

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

Oleh karena itu

  • Masukkan kode yang sensitif 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 kita mengaktifkan blok try/catch di compiler pengoptimalan. Anda dapat memeriksa bagaimana compiler pengoptimalan menghemat fungsi dengan menggunakan opsi "--trace-opt" dengan d8 seperti di atas, yang memberi Anda informasi lebih lanjut tentang fungsi mana yang diselamatkan:

d8 --trace-opt primes.js

De-pengoptimalan

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

Oleh karena itu

  • Menghindari perubahan class tersembunyi dalam fungsi setelah dioptimalkan

Seperti halnya pengoptimalan lainnya, Anda dapat memperoleh log fungsi yang harus dihilangkan oleh V8 dengan flag logging:

d8 --trace-deopt primes.js

Alat V8 Lainnya

Sekadar informasi, Anda juga dapat meneruskan opsi pelacakan V8 ke Chrome saat perangkat dinyalakan:

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

Selain menggunakan profiling alat developer, Anda juga dapat menggunakan d8 untuk melakukan profiling:

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

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

Ringkasan

Penting untuk mengidentifikasi dan memahami cara mesin V8 bekerja dengan kode Anda untuk mempersiapkan build JavaScript berperforma tinggi. Sekali lagi, saran dasarnya adalah:

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

Artinya, Anda harus memastikan bahwa masalah ada di JavaScript, dengan terlebih dahulu menggunakan alat lain seperti PageSpeed; mungkin kurangi ke JavaScript murni (tanpa DOM) sebelum mengumpulkan metrik, lalu gunakan metrik tersebut untuk menemukan bottleneck dan menghilangkan yang penting. Semoga pembicaraan Daniel (dan artikel ini) akan membantu Anda lebih memahami bagaimana V8 menjalankan JavaScript - tetapi pastikan juga untuk fokus pada pengoptimalan algoritma Anda sendiri!

Referensi