Dasar-dasar mesin JavaScript: optimasi prototipe. Bagian 2

Selamat sore teman! Kursus "Keamanan sistem informasi" telah diluncurkan, sehubungan dengan ini kami berbagi dengan Anda bagian akhir dari artikel "Dasar-dasar mesin JavaScript: optimalisasi prototipe", bagian pertama yang dapat dibaca di sini .

Kami juga mengingatkan Anda bahwa publikasi saat ini adalah kelanjutan dari dua artikel ini: β€œDasar-dasar mesin JavaScript: formulir umum dan caching Inline. Bagian 1 " , " Dasar-dasar mesin JavaScript: formulir umum dan caching Inline. Bagian 2 " .



Kelas dan pemrograman prototipe

Sekarang setelah kita tahu cara mendapatkan akses cepat ke properti objek JavaScript, kita dapat melihat struktur kelas JavaScript yang lebih kompleks. Ini adalah apa yang tampak seperti sintaksis kelas dalam JavaScript:

class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } 

Meskipun ini tampaknya seperti konsep yang relatif baru untuk JavaScript, itu hanya "gula sintaksis" untuk pemrograman prototipe yang selalu digunakan dalam JavaScript:

 function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; }; 

Di sini kita menetapkan properti getX ke objek getX . Ini akan berfungsi seperti halnya dengan objek lain, karena prototipe dalam JavaScript adalah objek yang sama. Dalam bahasa pemrograman prototipe seperti JavaScript, metode diakses melalui prototipe, sementara bidang disimpan dalam kasus tertentu.

Mari kita lihat lebih dekat apa yang terjadi ketika kita membuat instance baru dari Bar , yang akan kita sebut foo .

 const foo = new Bar(true); 

Sebuah instance yang dibuat menggunakan kode ini memiliki formulir dengan properti 'x' tunggal. Prototipe foo adalah Bar.prototype , yang termasuk dalam kelas Bar .



Bar.prototype ini memiliki bentuk sendiri, berisi satu-satunya properti 'getX' , yang nilainya ditentukan oleh fungsi 'getX' , yang ketika dipanggil mengembalikan this.x Prototipe Bar.prototype adalah Object.prototype , yang merupakan bagian dari bahasa JavaScript. Object.prototype adalah akar dari pohon prototipe, sedangkan prototipenya adalah null .



Saat Anda membuat instance baru dari kelas yang sama, kedua instance memiliki bentuk yang sama, seperti yang sudah kita pahami. Kedua instance akan menunjuk ke objek Bar.prototype sama.

Akses properti prototipe

Nah, sekarang kita tahu apa yang terjadi ketika kita mendefinisikan kelas dan membuat instance baru. Tetapi apa yang terjadi jika kita memanggil metode pada contoh, seperti yang kita lakukan pada contoh berikut?

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

Anda dapat mempertimbangkan pemanggilan metode apa pun sebagai dua langkah terpisah:

 const x = foo.getX(); // is actually two steps: const $getX = foo.getX; const x = $getX.call(foo); 

Langkah pertama adalah memuat metode, yang sebenarnya merupakan properti dari prototipe (yang nilainya adalah fungsi). Langkah kedua adalah memanggil fungsi dengan instance, misalnya, nilai this . Mari kita lihat lebih dekat pada langkah pertama di mana metode getX dari instance foo .



Mesin memulai turunan foo dan menyadari bahwa form foo tidak memiliki 'getX' , sehingga harus melalui rantai prototipe untuk menemukannya. Kita sampai ke Bar.prototype , lihat pada bentuk prototipe, lihat bahwa ia memiliki properti 'getX' dengan nol offset Kami mencari nilai pada offset ini di Bar.prototype dan menemukan JSFunction getX yang kami cari.

Fleksibilitas JavaScript memungkinkan tautan rantai prototipe berubah, misalnya:

 const foo = new Bar(true); foo.getX(); // β†’ true Object.setPrototypeOf(foo, null); foo.getX(); // β†’ Uncaught TypeError: foo.getX is not a function 

Dalam contoh ini, kami menelepon
 foo.getX() 
dua kali, tetapi setiap kali memiliki arti dan hasil yang sama sekali berbeda. Itulah sebabnya, meskipun fakta bahwa prototipe hanyalah objek dalam JavaScript, mempercepat akses ke properti prototipe adalah tugas yang bahkan lebih penting untuk mesin JavaScript daripada mempercepat akses mereka sendiri ke properti pada objek biasa.

Dalam praktik sehari-hari, memuat properti prototipe adalah operasi yang cukup umum: ini terjadi setiap kali Anda memanggil metode!

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

Sebelumnya, kami berbicara tentang bagaimana mesin mengoptimalkan pemuatan properti reguler dengan menggunakan formulir dan cache inline. Bagaimana saya bisa mengoptimalkan pemuatan properti prototipe untuk objek dengan bentuk yang sama? Dari atas kita melihat bagaimana properti dimuat.



Untuk melakukan ini dengan cepat dengan unduhan berulang dalam kasus khusus ini, Anda perlu mengetahui tiga hal berikut:

  • Bentuk foo tidak mengandung 'getX' dan tidak berubah. Ini berarti bahwa tidak ada seorang pun yang mengubah objek foo dengan menambahkan atau menghapus properti atau mengubah salah satu atribut properti.
  • Prototipe foo masih merupakan Bar.prototype asli. Jadi tidak ada yang mengubah prototipe foo menggunakan Object.setPrototypeOf() atau menugaskannya ke properti _proto_ khusus.
  • Formulir Bar.prototype berisi 'getX' dan belum berubah. Ini berarti bahwa tidak ada yang mengubah Bar.prototype dengan menambahkan atau menghapus properti atau mengubah salah satu atribut properti.

Dalam kasus umum, ini berarti bahwa Anda perlu membuat satu pemeriksaan instance itu sendiri dan dua pemeriksaan lagi untuk setiap prototipe hingga prototipe yang berisi properti yang diinginkan. 1 + 2N memeriksa, di mana N adalah jumlah prototipe yang digunakan, tidak terdengar begitu buruk dalam kasus ini, karena rantai prototipe relatif dangkal. Namun, mesin sering harus berurusan dengan rantai prototipe yang lebih lama, seperti halnya dengan kelas DOM reguler. Sebagai contoh:

 const anchor = document.createElement('a'); // β†’ HTMLAnchorElement const title = anchor.getAttribute('title'); 

Kami memiliki HTMLAnchorElement dan kami memanggil metode getAttribute() . Rantai untuk elemen sederhana ini sudah termasuk 6 prototipe! Sebagian besar metode DOM yang menarik minat kita bukan pada prototipe HTMLAnchorElement , tetapi di suatu tempat di rantai.



Metode getAttribute() ada di Element.prototype . Ini berarti bahwa setiap kali kita memanggil anchor.getAttribute() , mesin JavaScript perlu:

  1. Periksa bahwa 'getAttribute' bukan objek anchor per se;
  2. Verifikasi bahwa prototipe terakhir adalah HTMLAnchorElement.prototype ;
  3. Konfirmasikan tidak adanya 'getAttribute' sana;
  4. Verifikasi bahwa prototipe berikutnya adalah HTMLElement.prototype ;
  5. Konfirmasikan tidak adanya 'getAttribute' ;
  6. Verifikasi bahwa prototipe berikutnya adalah Element.prototype ;
  7. Periksa apakah 'getAttribute' ada di dalamnya.

Sebanyak 7 cek. Karena jenis kode ini cukup umum di web, mesin menggunakan berbagai trik untuk mengurangi jumlah pemeriksaan yang diperlukan untuk memuat properti prototipe.

Kembali ke contoh sebelumnya di mana kami hanya melakukan tiga pemeriksaan ketika meminta 'getX' untuk foo :

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

Untuk setiap objek yang terjadi sebelum prototipe yang berisi properti yang diinginkan, perlu untuk memeriksa formulir untuk tidak adanya properti ini. Alangkah baiknya jika kita bisa mengurangi jumlah cek dengan menghadirkan cek prototipe sebagai cek untuk tidak adanya properti. Intinya, inilah yang dilakukan mesin dengan trik sederhana: alih-alih menyimpan tautan prototipe ke instance itu sendiri, engine menyimpannya dalam bentuk.



Setiap bentuk menunjukkan prototipe. Ini berarti bahwa setiap kali prototipe foo berubah, mesin pindah ke bentuk baru. Sekarang kita perlu memeriksa hanya bentuk objek untuk mengkonfirmasi tidak adanya properti tertentu, serta melindungi tautan prototipe (menjaga tautan prototipe).

Dengan pendekatan ini, kami dapat mengurangi jumlah cek yang diperlukan dari 2N + 1 hingga 1 + N untuk mempercepat akses. Ini masih merupakan operasi yang cukup mahal, karena masih merupakan fungsi linier dari jumlah prototipe dalam rantai. Mesin menggunakan berbagai trik untuk lebih mengurangi jumlah cek ke nilai konstan tertentu, terutama dalam kasus pemuatan berurutan dari properti yang sama.

Sel validitas

V8 memproses formulir prototipe khusus untuk tujuan ini. Setiap prototipe memiliki bentuk unik yang tidak dibagi dengan objek lain (khususnya, dengan prototipe lain), dan masing-masing bentuk prototipe ini memiliki ValidityCell khusus yang terkait dengannya.



ValidityCell dinonaktifkan setiap kali seseorang mengubah prototipe yang terkait dengannya atau prototipe lain di atasnya. Mari kita lihat cara kerjanya.
Untuk mempercepat unduhan prototipe berikutnya, V8 menempatkan cache Inline di lokasi empat bidang:



Ketika cache inline dipanaskan saat pertama kali kode dijalankan, V8 mengingat offset di mana properti ditemukan dalam prototipe, prototipe ini (misalnya, Bar.prototype ), bentuk instance (dalam kasus kami, form foo ), dan juga mengikat ValidityCell saat ini ke prototipe yang diterima dari contoh formulir (dalam kasus kami, Bar.prototype diambil).

Lain kali Anda menggunakan cache Inline, mesin perlu memeriksa formulir instance dan ValidityCell . Jika masih valid, mesin langsung menggunakan offset pada prototipe, melewatkan langkah-langkah pencarian tambahan.



Saat Anda mengubah prototipe, formulir baru disorot, dan sel ValidityCell sebelumnya dinonaktifkan. Karena itu, cache Inline dilewati saat berikutnya dimulai, yang berujung pada kinerja yang buruk.

Mari kita kembali ke contoh dengan elemen DOM. Setiap perubahan Object.prototype tidak hanya membatalkan cache Inline untuk Object.prototype , tetapi juga untuk setiap prototipe dalam rantai di bawahnya, termasuk EventTarget.prototype , Node.prototype , Element.prototype , dll., HTMLAnchorElement.prototype itu sendiri.



Bahkan, memodifikasi Object.prototype saat kode sedang dieksekusi adalah kehilangan kinerja yang mengerikan. Jangan lakukan ini!

Mari kita lihat contoh spesifik untuk lebih memahami bagaimana ini bekerja. Katakanlah kita memiliki kelas Bar dan fungsi loadX yang memanggil metode pada objek bertipe Bar . Kami memanggil fungsi loadX beberapa kali dengan instance dari kelas yang sama.

 class Bar { /* … */ } function loadX(bar) { return bar.getX(); // IC for 'getX' on `Bar` instances. } loadX(new Bar(true)); loadX(new Bar(false)); // IC in `loadX` now links the `ValidityCell` for // `Bar.prototype`. Object.prototype.newMethod = y => y; // The `ValidityCell` in the `loadX` IC is invalid // now, because `Object.prototype` changed. 

Cache sebaris di loadX sekarang menunjuk ke ValidityCell untuk Bar.prototype . Jika Anda kemudian memodifikasi (mutate) Object.prototype , yang merupakan akar dari semua prototipe dalam JavaScript, ValidityCell menjadi tidak valid dan cache Inline yang ada tidak akan digunakan lain kali, yang mengakibatkan kinerja buruk.

Mengubah Object.prototype selalu merupakan ide yang buruk, karena membatalkan semua cache Inline untuk prototipe yang dimuat pada saat perubahan. Ini adalah contoh bagaimana TIDAK harus dilakukan:

 Object.prototype.foo = function() { /* … */ }; // Run critical code: someObject.foo(); // End of critical code. delete Object.prototype.foo; 

Kami sedang memperluas Object.prototype , yang membatalkan semua cache prototipe Inline yang dimuat oleh mesin pada saat ini. Kemudian kita akan menjalankan beberapa kode yang menggunakan metode yang dijelaskan oleh kita. Mesin harus mulai dari awal dan mengkonfigurasi cache Inline untuk setiap akses ke properti prototipe. Dan akhirnya, "bersihkan" dan hapus metode prototipe yang kami tambahkan sebelumnya.

Anda pikir membersihkan adalah ide yang bagus, bukan? Nah, dalam hal ini, ini akan semakin memperburuk situasi! Menghapus properti mengubah Object.prototype , sehingga semua cache Inline dinonaktifkan lagi, dan mesin harus mulai bekerja dari awal lagi.

Untuk meringkas . Terlepas dari kenyataan bahwa prototipe hanyalah objek, mereka secara khusus diproses oleh mesin JavaScript untuk mengoptimalkan kinerja pencarian metode oleh prototipe. Biarkan prototipe sendiri! Atau jika Anda benar-benar harus berurusan dengan mereka, lakukan sebelum menjalankan kode, sehingga Anda setidaknya tidak akan membatalkan semua upaya untuk mengoptimalkan kode Anda selama eksekusi!

Ringkaslah

Kami mempelajari bagaimana JavaScript menyimpan objek dan kelas, dan bagaimana formulir, cache inline, dan sel validitas membantu mengoptimalkan operasi prototipe. Berdasarkan pengetahuan ini, kami memahami cara meningkatkan kinerja dari sudut pandang praktis: jangan menyentuh prototipe! (atau jika Anda benar-benar membutuhkannya, lakukan sebelum menjalankan kode).

← Bagian pertama

Apakah seri publikasi ini bermanfaat bagi Anda? Tulis di komentar.

Source: https://habr.com/ru/post/id449144/


All Articles