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

→
Bagian pertamaUsang 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 };
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 objekKetika 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 ObyekSaat 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 sampahHal-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 berubahDi 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;
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;
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;
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 objekUntuk 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 yatimMudah 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 sistemAda 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 bingungV8 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 sistemIni 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.actualStartTimeHarap 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.actualStartTimeSetelah 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() {
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:
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?
