Tips performa untuk JavaScript di V8

Chris Wilson
Chris Wilson

Pengantar

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

Saran Paling Penting

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

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

  • Persiapkan diri sebelum Anda menghadapi (atau mengetahui) suatu masalah
  • Kemudian, identifikasi dan pahami inti masalah Anda
  • Terakhir, perbaiki hal yang penting

Untuk mencapai langkah-langkah ini, penting untuk memahami bagaimana V8 mengoptimalkan JS, sehingga Anda dapat menulis kode dengan memperhatikan desain runtime JS. Penting juga untuk mempelajari alat yang tersedia dan bagaimana alat tersebut dapat membantu Anda. Daniel memberikan penjelasan lebih lanjut tentang cara menggunakan {i>developer tools<i} dalam pembahasannya; dokumen ini hanya menangkap beberapa poin terpenting dari desain mesin V8.

Jadi, ke tips V8!

Kelas Tersembunyi

JavaScript memiliki informasi jenis waktu kompilasi yang terbatas: jenis dapat diubah saat runtime, jadi wajar untuk memperkirakan bahwa akan mahal untuk memikirkan jenis JS pada waktu kompilasi. Hal ini mungkin menyebabkan Anda mempertanyakan bagaimana kinerja JavaScript bisa mendekati C++. Akan tetapi, V8 memiliki tipe tersembunyi yang dibuat secara internal untuk objek pada waktu proses; objek dengan kelas tersembunyi yang sama bisa menggunakan kode sama yang dihasilkan.

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" menambahkan, p1 dan p2 secara internal memiliki kelas tersembunyi yang sama - sehingga V8 dapat menghasilkan satu versi perakitan yang dioptimalkan untuk kode JavaScript yang memanipulasi p1 atau p2. Semakin banyak yang dapat Anda hindari menyebabkan class tersembunyi berbeda, semakin baik performa yang akan Anda dapatkan.

Oleh karena itu

  • Lakukan inisialisasi semua anggota objek dalam fungsi konstruktor (sehingga instance tidak mengubah jenis di lain waktu)
  • Selalu 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 tipe angka yang sedang Anda hadapi. Setelah membuat inferensi ini, V8 menggunakan pemberian tag untuk menampilkan 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, cara yang paling optimal untuk menggunakan bilangan bulat 31-bit yang ditandatangani, 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 31-bit yang ditandai.

Array

Untuk menangani array 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 berdekatan yang dimulai dari 0 untuk Array
  • Jangan alokasikan Array besar di awal (mis. > elemen 64K) ke ukuran maksimumnya, kembangkan seiring berjalannya waktu
  • Jangan menghapus elemen dalam array, terutama array numerik
  • Jangan memuat elemen yang tidak diinisialisasi atau dihapus:
for (var b = 0; b &lt; 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b &lt; 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

Selain itu, Array ganda lebih cepat - kelas tersembunyi dari array melacak jenis elemen, dan array yang berisi hanya ganda tidak dibuka (yang menyebabkan perubahan kelas tersembunyi). Namun, manipulasi Array yang ceroboh dapat menyebabkan pekerjaan ekstra karena tinju 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 pada contoh pertama, penetapan individual dilakukan satu per satu, dan penetapan a[2] menyebabkan Array dikonversi ke 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
  • Mengalokasikan array kecil (<64k) terlebih dahulu untuk memperbaiki ukuran sebelum menggunakannya
  • Jangan simpan 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, sebenarnya:

  • "Penuh" compiler, yang dapat menghasilkan kode yang baik untuk semua JavaScript
  • Kompilator Pengoptimalan, yang menghasilkan kode yang bagus untuk sebagian besar JavaScript, tetapi membutuhkan waktu kompilasi yang lebih lama.

Kompilator Sepenuhnya

Di V8, compiler Penuh berjalan pada semua kode, dan mulai mengeksekusi kode sesegera mungkin, sehingga dengan cepat menghasilkan kode yang bagus tetapi tidak bagus. Compiler ini tidak mengasumsikan apa pun tentang jenis pada waktu kompilasi - ia 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 dari Inline Caches adalah untuk menangani jenis secara efisien, dengan meng-cache kode yang bergantung pada jenis untuk operasi; ketika 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 mengalami penurunan performa.

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 mengubah jenis di berbagai panggilan pada 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 "hot" fungsi (yaitu, fungsi yang dijalankan berkali-kali) dengan compiler pengoptimalan. Kompilator ini menggunakan umpan balik dari tipe untuk membuat kode yang dikompilasi lebih cepat - bahkan, ia menggunakan tipe yang diambil dari IC yang baru saja kita bicarakan!

Dalam compiler pengoptimalan, operasi dilakukan secara spekulatif secara spekulatif (ditempatkan langsung di tempat yang dipanggil). Tindakan 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 "d8" mandiri versi mesin V8:

d8 --trace-opt primes.js

(ini mencatat nama fungsi yang dioptimalkan ke dalam log ke {i>stdout<i}.)

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 blok {} tangkap {}: ```js function perf_sensitive() { // Lakukan pekerjaan yang sensitif terhadap performa di sini }

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 cara compiler pengoptimalan mengoptimalkan fungsi dengan menggunakan tanda "--trace-opt" dengan d8 seperti di atas, yang memberi Anda lebih banyak informasi tentang fungsi mana yang dibebaskan:

d8 --trace-opt primes.js

Penghilangan pengoptimalan

Terakhir, pengoptimalan yang dilakukan oleh compiler ini bersifat spekulatif - terkadang tidak berhasil, dan kami berhenti. Proses "depengoptimalan" membuang kode yang dioptimalkan, dan melanjutkan eksekusi di tempat yang tepat dengan "penuh" kode compiler. Pengoptimalan ulang mungkin dipicu lagi nanti, tetapi untuk jangka pendek, eksekusi melambat. Secara khusus, menyebabkan perubahan di 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 bisa mendapatkan log fungsi yang harus dibatalkan pengoptimalan V8 dengan flag logging:

d8 --trace-deopt primes.js

Alat V8 Lainnya

Ngomong-ngomong, Anda juga bisa meneruskan opsi pelacakan V8 ke Chrome saat browser mulai dijalankan:

"/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 melakukan pembuatan profil:

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

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

Ringkasan

Penting untuk mengenali dan memahami cara mesin V8 bekerja dengan kode Anda sebagai persiapan untuk membangun JavaScript yang berkinerja tinggi. Sekali lagi, saran dasarnya adalah:

  • Persiapkan diri sebelum Anda menghadapi (atau mengetahui) suatu masalah
  • Kemudian, identifikasi dan pahami inti masalah Anda
  • Terakhir, perbaiki hal yang penting

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

Referensi