Sebuah kisah tentang V8, React dan penurunan kinerja. Bagian 2

Hari ini kami menerbitkan bagian kedua dari terjemahan materi yang ditujukan untuk mekanisme internal V8 dan penyelidikan masalah kinerja Bereaksi.



Bagian pertama

Usang dan migrasi bentuk objek


Bagaimana jika bidang awalnya berisi Smi , dan kemudian situasinya berubah dan perlu menyimpan nilai yang representasi Smi tidak cocok? Misalnya, seperti dalam contoh berikut, ketika dua objek direpresentasikan menggunakan bentuk yang sama dari objek di mana x awalnya disimpan sebagai Smi :

 const a = { x: 1 }; const b = { x: 2 }; //  `x`       `Smi` bx = 0.2; //  `bx`     `Double` y = ax; 

Pada awal contoh, kita memiliki dua objek, untuk representasi yang kita gunakan bentuk objek yang sama di mana format Smi digunakan untuk menyimpan x .


Bentuk yang sama digunakan untuk mewakili objek

Ketika properti bx berubah dan Anda harus menggunakan format Double untuk mewakilinya, V8 mengalokasikan ruang memori untuk bentuk objek yang baru, di mana x ditugaskan representasi Double , dan yang menunjukkan formulir kosong. V8 juga membuat entitas yang MutableHeapNumber , yang digunakan untuk menyimpan nilai 0,2 dari properti x . Kemudian kami memperbarui objek b sehingga merujuk ke formulir baru ini dan mengubah slot di objek sehingga MutableHeapNumber pada entitas MutableHeapNumber dibuat sebelumnya pada offset 0. Akhirnya, kami menandai bentuk objek yang lama sebagai usang dan melepaskannya dari pohon transisi. Ini dilakukan dengan membuat transisi baru untuk 'x' dari formulir kosong ke yang baru saja kita buat.


Konsekuensi Penugasan Nilai Baru ke Properti Obyek

Saat ini, kami tidak dapat sepenuhnya menghapus formulir lama, karena masih digunakan oleh objek a . Selain itu, akan sangat mahal untuk memotong semua memori dalam mencari semua objek yang merujuk ke bentuk lama, dan segera memperbarui keadaan objek-objek ini. Sebaliknya, V8 menggunakan pendekatan "malas" di sini. Yaitu, semua operasi membaca atau menulis properti objek a pertama kali ditransfer ke penggunaan bentuk baru. Gagasan di balik tindakan ini adalah untuk akhirnya membuat bentuk objek yang usang menjadi tidak dapat dicapai. Ini akan menyebabkan pengumpul sampah menanganinya.


Memori out-of-form membebaskan pengumpul sampah

Hal-hal yang lebih rumit dalam situasi di mana bidang yang mengubah tampilan bukanlah yang terakhir dalam rantai:

 const o = {  x: 1,  y: 2,  z: 3, }; oy = 0.1; 

Dalam hal ini, V8 perlu menemukan apa yang disebut bentuk split. Ini adalah bentuk terakhir dalam rantai, yang terletak sebelum formulir di mana properti terkait muncul. Di sini kita mengubah y , yaitu - kita perlu menemukan bentuk terakhir di mana tidak ada y . Dalam contoh kita, ini adalah bentuk di mana x muncul.


Cari formulir terakhir di mana tidak ada nilai yang berubah

Di sini, dimulai dengan formulir ini, kami membuat rantai transisi baru untuk y yang mereproduksi semua transisi sebelumnya. Hanya sekarang properti 'y' akan direpresentasikan sebagai Double . Sekarang kita menggunakan rantai transisi baru ini untuk y , menandainya sebagai subtree lama yang sudah usang. Pada langkah terakhir, kami memigrasikan instance objek o ke bentuk baru, sekarang menggunakan entitas MutableHeapNumber untuk menyimpan nilai y . Dengan pendekatan ini, objek baru tidak akan menggunakan fragmen dari pohon transisi lama dan, setelah semua referensi ke bentuk lama telah hilang, bagian pohon yang usang juga akan menghilang.

Ekstensibilitas dan Integritas Transisi


Perintah Object.preventExtensions() memungkinkan Anda untuk sepenuhnya mencegah penambahan properti baru ke objek. Jika Anda memproses objek dengan perintah ini dan mencoba menambahkan properti baru ke dalamnya, pengecualian akan dibuang. (Benar, jika kode tidak dieksekusi dalam mode ketat, pengecualian tidak akan dibuang, namun, upaya untuk menambahkan properti tidak akan menimbulkan konsekuensi apa pun). Berikut ini sebuah contoh:

 const object = { x: 1 }; Object.preventExtensions(object); object.y = 2; // TypeError: Cannot add property y; //      object is not extensible 

Metode Object.seal() bertindak pada objek dengan cara yang sama seperti Object.preventExtensions() , tetapi juga menandai semua properti sebagai tidak dapat dikonfigurasi. Ini berarti bahwa mereka tidak dapat dihapus, atau sifat mereka tidak dapat diubah mengenai kemungkinan daftar, pengaturan atau penulisan ulang mereka.

 const object = { x: 1 }; Object.seal(object); object.y = 2; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x 

Metode Object.freeze() melakukan tindakan yang sama seperti Object.seal() , tetapi penggunaannya, selain itu, mengarah pada fakta bahwa nilai-nilai properti yang ada tidak dapat diubah. Mereka ditandai sebagai properti di mana nilai-nilai baru tidak dapat ditulis.

 const object = { x: 1 }; Object.freeze(object); object.y = 2; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x object.x = 3; // TypeError: Cannot assign to read-only property x 

Pertimbangkan contoh spesifik. Kami memiliki dua objek, yang masing-masing memiliki nilai unik x . Kemudian kami melarang ekstensi objek kedua:

 const a = { x: 1 }; const b = { x: 2 }; Object.preventExtensions(b); 

Pemrosesan kode ini dimulai dengan tindakan yang sudah kita ketahui. Yaitu, transisi dibuat dari bentuk kosong objek ke bentuk baru, yang berisi properti 'x' (diwakili sebagai entitas Smi ). Ketika kita melarang ekspansi objek b , ini mengarah ke transisi khusus ke bentuk baru, yang ditandai sebagai tidak dapat diperluas. Transisi khusus ini tidak mengarah pada penampilan beberapa properti baru. Ini sebenarnya hanya penanda.


Hasil dari pemrosesan objek menggunakan metode Object.preventExtensions ()

Harap dicatat bahwa kami tidak dapat hanya mengubah formulir yang ada dengan nilai x di dalamnya, karena diperlukan oleh objek lain, yaitu objek a , yang masih dapat dikembangkan.

Bereaksi masalah kinerja


Sekarang mari kita kumpulkan semua yang kita bicarakan dan gunakan pengetahuan yang telah kita peroleh untuk memahami esensi dari masalah kinerja Bereaksi baru-baru ini. Ketika tim Bereaksi membuat profil aplikasi nyata, mereka melihat penurunan kinerja V8 aneh yang bertindak pada inti Bereaksi. Berikut adalah reproduksi bagian masalah yang disederhanakan dari kode:

 const o = { x: 1, y: 2 }; Object.preventExtensions(o); oy = 0.2; 

Kami memiliki objek dengan dua bidang yang direpresentasikan sebagai entitas Smi . Kami mencegah perluasan objek lebih lanjut, dan kemudian melakukan tindakan yang mengarah pada fakta bahwa bidang kedua harus diwakili dalam format Double .

Kami telah menemukan bahwa larangan ekspansi objek mengarah ke situasi berikut.


Konsekuensi dari larangan ekspansi objek

Untuk mewakili kedua properti objek, entitas Smi , dan transisi terakhir diperlukan untuk menandai bentuk objek sebagai non-extensible.

Sekarang kita perlu mengubah cara properti y diwakili oleh Double . Ini berarti bahwa kita perlu mulai mencari bentuk pemisahan. Dalam hal ini, ini adalah bentuk di mana properti x muncul. Tapi sekarang V8 bingung. Faktanya adalah bahwa bentuk pemisahan dapat diperpanjang, dan bentuk saat ini ditandai sebagai tidak dapat diperpanjang. V8 tidak tahu bagaimana mereproduksi proses transisi dalam situasi yang sama. Akibatnya, mesin menolak untuk mencoba mencari tahu semuanya. Sebagai gantinya, itu hanya membuat formulir terpisah yang tidak terhubung ke pohon formulir saat ini dan tidak dibagikan dengan objek lain. Ini adalah sesuatu seperti bentuk objek yatim.


Bentuk yatim

Mudah ditebak bahwa ini, jika ini terjadi pada banyak objek, sangat buruk. Faktanya adalah bahwa ini membuat seluruh sistem objek V8 tidak berguna.

Ketika masalah Bereaksi terjadi, berikut ini terjadi. Setiap objek dari kelas FiberNode memiliki bidang yang dimaksudkan untuk menyimpan stempel waktu saat pembuatan profil diaktifkan.

 class FiberNode {  constructor() {    this.actualStartTime = 0;    Object.preventExtensions(this);  } } const node1 = new FiberNode(); const node2 = new FiberNode(); 

Bidang-bidang ini (misalnya, actualStartTime ) diinisialisasi ke 0 atau -1. Hal ini menyebabkan fakta bahwa entitas Smi digunakan untuk mewakili maknanya secara Smi . Namun kemudian, mereka menyimpan prangko waktu nyata dalam format angka floating-point yang dikembalikan oleh metode performance.now (). Ini mengarah pada fakta bahwa nilai-nilai ini tidak lagi dapat diwakili dalam bentuk Smi . Untuk mewakili bidang ini, Entitas Double sekarang diperlukan. Di atas semua ini, Bereaksi juga mencegah perluasan instance dari kelas FiberNode .

Awalnya, contoh sederhana kami dapat disajikan dalam bentuk berikut.


Keadaan awal sistem

Ada dua contoh kelas yang berbagi pohon transisi yang sama dari bentuk objek. Sebenarnya, ini adalah tujuan dari sistem bentuk objek di V8. Tetapi kemudian, ketika perangko waktu nyata disimpan dalam objek, V8 tidak dapat memahami bagaimana perangko dapat menemukan bentuk pemisahan.


V8 bingung

V8 memberikan formulir yatim baru ke node1 . Hal yang sama terjadi sedikit kemudian dengan objek node2 . Sebagai hasilnya, kami sekarang memiliki dua bentuk "yatim", yang masing-masing digunakan hanya oleh satu objek. Dalam banyak aplikasi Bereaksi nyata, jumlah objek tersebut jauh lebih dari dua. Ini bisa berupa puluhan atau bahkan ribuan objek dari kelas FiberNode . Sangat mudah untuk memahami bahwa situasi ini tidak mempengaruhi kinerja V8 dengan sangat baik.

Untungnya, kami memperbaiki masalah ini di V8 v7.4 , dan kami sedang menjajaki kemungkinan membuat operasi mengubah representasi bidang objek kurang intensif sumber daya. Ini akan memungkinkan kami untuk menyelesaikan masalah kinerja yang tersisa yang muncul dalam situasi seperti itu. V8, berkat perbaikannya, sekarang berperilaku dengan benar dalam situasi masalah yang dijelaskan di atas.


Keadaan awal sistem

Ini tampilannya. Dua contoh referensi kelas FiberNode bentuk non-extensible. Dalam hal ini, 'actualStartTime' direpresentasikan sebagai bidang Smi . Ketika operasi pertama memberikan nilai ke properti node1.actualStartTime , rantai transisi baru dibuat, dan rantai sebelumnya ditandai sebagai usang.


Hasil Menetapkan Nilai Baru ke Properti Node1.actualStartTime

Harap dicatat bahwa transisi ke formulir yang tidak dapat diperluas sekarang direproduksi dengan benar di rantai baru. Inilah yang masuk ke sistem setelah mengubah nilai node2.actualStartTime .


Hasil menetapkan nilai baru ke properti node2.actualStartTime

Setelah nilai baru ditetapkan ke properti node2.actualStartTime , kedua objek merujuk ke formulir baru, dan bagian usang dari pohon transisi dapat dihancurkan oleh pengumpul sampah.

Harap perhatikan bahwa operasi untuk menandai bentuk objek sebagai usang dan migrasi mereka mungkin terlihat seperti sesuatu yang rumit. Bahkan - apa adanya. Kami menduga bahwa di situs web asli ini tidak lebih berbahaya (dalam hal kinerja, penggunaan memori, kompleksitas) daripada yang baik. Terutama - setelah, dalam kasus kompresi pointer , kita tidak bisa lagi menggunakan pendekatan ini untuk menyimpan bidang Double dalam bentuk nilai yang tertanam dalam objek. Sebagai hasilnya, kami berharap untuk sepenuhnya meninggalkan mekanisme usang bentuk objek V8 dan membuat mekanisme ini sendiri menjadi usang.

Perlu dicatat bahwa tim Bereaksi memecahkan masalah ini sendiri, memastikan bahwa bidang dalam objek kelas FiberNodes awalnya diwakili oleh nilai ganda:

 class FiberNode {  constructor() {    //     `Double`   .    this.actualStartTime = Number.NaN;    //       ,  :    this.actualStartTime = 0;    Object.preventExtensions(this);  } } const node1 = new FiberNode(); const node2 = new FiberNode(); 

Di sini, alih-alih Number.NaN , nilai floating-point apa pun yang tidak cocok dengan rentang Smi dapat digunakan. Di antara nilai-nilai ini adalah Number.MIN_VALUE , Number.MIN_VALUE , -0 dan Infinity .

Perlu dicatat bahwa masalah yang dijelaskan dalam Bereaksi khusus untuk V8, dan bahwa ketika membuat beberapa kode, pengembang tidak perlu berusaha untuk mengoptimalkannya berdasarkan versi spesifik dari mesin JavaScript tertentu. Namun, berguna untuk dapat memperbaiki sesuatu dengan mengoptimalkan kode jika penyebab beberapa kesalahan berakar pada fitur-fitur mesin.

Perlu diingat bahwa di perut mesin-JS ada banyak hal menakjubkan. Pengembang JS dapat membantu semua mekanisme ini, jika mungkin tanpa menetapkan nilai variabel yang sama dari jenis yang berbeda. Misalnya, Anda tidak boleh menginisialisasi bidang numerik ke null , karena ini akan meniadakan semua keuntungan dari mengamati representasi bidang dan meningkatkan keterbacaan kode:

 //   ! class Point {  x = null;  y = null; } const p = new Point(); px = 0.1; py = 402; 

Dengan kata lain - tulis kode yang dapat dibaca, dan kinerja akan datang dengan sendirinya!

Ringkasan


Dalam artikel ini, kami memeriksa masalah-masalah penting berikut:

  • JavaScript membedakan antara nilai "primitif" dan "objek", dan hasil typeof tidak dapat dipercaya.
  • Nilai-nilai genap yang memiliki tipe JavaScript yang sama dapat direpresentasikan dengan berbagai cara di perut mesin.
  • V8 berusaha menemukan cara terbaik untuk mewakili setiap properti objek yang digunakan dalam program JS.
  • Dalam situasi tertentu, V8 melakukan operasi pada menandai bentuk objek sebagai usang dan melakukan migrasi bentuk. Termasuk - mengimplementasikan transisi yang terkait dengan larangan ekspansi objek.

Berdasarkan hal tersebut di atas, kami dapat memberikan beberapa tips pemrograman JavaScript praktis yang dapat membantu meningkatkan kinerja kode:

  • Selalu inisialisasi objek Anda dengan cara yang sama. Ini berkontribusi pada kerja efektif dengan bentuk-bentuk objek.
  • Pilih nilai awal untuk bidang objek secara bertanggung jawab. Ini akan membantu mesin JavaScript dalam memilih cara mewakili nilai-nilai ini secara internal.

Pembaca yang budiman! Apakah Anda pernah mengoptimalkan kode Anda berdasarkan fitur internal mesin JavaScript tertentu?

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


All Articles