Halo semuanya. Semakin sedikit waktu yang tersisa hingga peluncuran kursus
Sistem Informasi Keamanan , itulah sebabnya hari ini kami terus berbagi publikasi yang didedikasikan untuk peluncuran kursus ini. Ngomong-ngomong, publikasi ini adalah kelanjutan dari dua artikel ini:
βDasar-dasar mesin JavaScript: bentuk umum dan caching Inline. Bagian 1 " ,
" Dasar-dasar mesin JavaScript: formulir umum dan caching Inline. Bagian 2 " .
Artikel ini menjelaskan dasar-dasar utama. Mereka umum untuk semua mesin JavaScript, dan bukan hanya untuk
V8 yang sedang dikerjakan oleh penulis (
Benedict dan
Matthias ). Sebagai pengembang JavaScript, saya dapat mengatakan bahwa pemahaman yang lebih dalam tentang cara kerja mesin JavaScript akan membantu Anda mengetahui cara menulis kode yang efisien.

Pada
artikel sebelumnya, kami membahas bagaimana mesin JavaScript mengoptimalkan akses ke objek dan array menggunakan formulir dan cache inline. Pada artikel ini, kita akan melihat mengoptimalkan kompromi jalur pipa dan mempercepat akses ke properti prototipe.
Catatan: jika Anda lebih suka menonton presentasi daripada membaca artikel, maka tonton video ini . Jika tidak, lewatkan saja dan baca terus.
Tingkat Optimalisasi dan PengorbananTerakhir kali kami mengetahui bahwa semua mesin JavaScript modern, sebenarnya, memiliki saluran pipa yang sama:

Kami juga menyadari bahwa terlepas dari kenyataan bahwa saluran pipa tingkat tinggi dari mesin ke mesin memiliki struktur yang serupa, ada perbedaan dalam optimasi pipa. Kenapa begitu? Mengapa beberapa mesin memiliki tingkat optimisasi yang lebih tinggi daripada yang lain? Masalahnya adalah untuk membuat kompromi antara transisi cepat ke tahap eksekusi kode atau menghabiskan sedikit lebih banyak waktu untuk mengeksekusi kode dengan kinerja optimal.

Interpreter dapat dengan cepat menghasilkan bytecode, tetapi bytecode saja tidak cukup efisien dalam hal kecepatan. Melibatkan kompiler yang mengoptimalkan dalam proses ini menghabiskan waktu tertentu, tetapi memungkinkan kode mesin lebih efisien.
Mari kita lihat bagaimana V8 menangani ini. Ingatlah bahwa dalam V8 interpreter disebut Ignition dan dianggap sebagai interpreter tercepat di antara mesin yang ada (dalam hal kecepatan eksekusi bytecode mentah). Kompilator pengoptimal dalam V8 disebut TurboFan, dan dialah yang menghasilkan kode mesin yang sangat optimal.

Pertukaran antara penundaan startup dan kecepatan eksekusi adalah alasan mengapa beberapa mesin JavaScript lebih suka menambahkan level optimisasi tambahan di antara langkah-langkah. Misalnya, SpiderMonkey menambahkan tingkat Baseline antara penerjemahnya dan kompiler IonMonkey yang mengoptimalkan penuh:

Interpreter dengan cepat menghasilkan bytecode, tetapi bytecode itu sendiri relatif lambat. Baseline menghasilkan kode sedikit lebih lama, tetapi memberikan peningkatan kinerja saat runtime. Akhirnya, kompiler mengoptimalkan IonMonkey menghabiskan waktu paling banyak menghasilkan kode mesin, tetapi kode tersebut sangat efisien.
Mari kita lihat contoh spesifik dan lihat bagaimana pipa dari berbagai mesin menangani masalah ini. Di sini, di loop panas, kode yang sama sering diulang.
let result = 0; for (let i = 0; i < 4242424242; ++i) { result += i; } console.log(result);
V8 dimulai dengan memulai bytecode pada interpreter Ignition. Pada titik tertentu, engine menentukan bahwa kode tersebut panas dan meluncurkan antarmuka TurboFan, yang mengintegrasikan data profil dan membangun representasi dasar mesin dari kode tersebut. Kemudian dikirim ke pengoptimal TurboFan di utas lainnya untuk peningkatan lebih lanjut.

Sementara optimasi sedang berlangsung, V8 terus mengeksekusi kode dalam Ignition. Pada titik tertentu, ketika optimizer telah selesai dan kami telah menerima kode mesin yang dapat dieksekusi, ia langsung melanjutkan ke tahap eksekusi.
SpyderMonkey juga memulai eksekusi bytecode pada interpreter. Tetapi memiliki tingkat Baseline tambahan, yang berarti bahwa kode panas dikirim ke sana terlebih dahulu. Kompilator Baseline menghasilkan kode Baseline di utas utama dan melanjutkan eksekusi pada akhir generasinya.

Jika kode Baseline telah berjalan selama beberapa waktu, SpiderMonkey akhirnya meluncurkan antarmuka IonMonkey (frontend IonMonkey) dan menjalankan pengoptimal, prosesnya sangat mirip dengan V8. Semua ini terus bekerja pada saat yang sama di Baseline, sementara IonMonkey terlibat dalam optimasi. Akhirnya, ketika optimizer menyelesaikan tugasnya, kode yang dioptimalkan dijalankan alih-alih kode Baseline.
Arsitektur Chakra sangat mirip dengan SpiderMonkey, tetapi Chakra mencoba menjalankan lebih banyak proses pada saat yang sama untuk menghindari pemblokiran utas utama. Alih-alih menjalankan bagian mana pun dari kompiler di utas utama, Chakra menyalin bytecode dan profil data yang dibutuhkan oleh kompiler dan mengirimkannya ke proses compiler khusus.

Ketika kode yang dihasilkan siap, mesin menjalankan kode SimpleJIT ini alih-alih bytecode. Hal yang sama terjadi dengan FullJIT. Keuntungan dari pendekatan ini adalah bahwa jeda yang terjadi selama penyalinan biasanya jauh lebih pendek daripada memulai kompiler penuh (frontend). Di sisi lain, pendekatan ini memiliki kelemahan. Itu terletak pada kenyataan bahwa salinan heuristik dapat melewatkan beberapa informasi yang akan diperlukan untuk optimasi, sehingga kita dapat mengatakan bahwa sampai taraf tertentu kualitas kode dikorbankan demi mempercepat pekerjaan.
Di JavaScriptCore, semua kompiler pengoptimal bekerja sepenuhnya secara paralel dengan eksekusi dasar JavaScript. Tidak ada fase penyalinan. Sebagai gantinya, utas utama hanya mulai mengkompilasi dalam utas lainnya. Compiler kemudian menggunakan skema penguncian yang kompleks untuk mengakses profil data dari utas utama.

Keuntungan dari pendekatan ini adalah mengurangi jumlah sampah yang muncul setelah optimasi di utas utama. Kerugian dari pendekatan ini adalah bahwa hal itu membutuhkan penyelesaian masalah multithreading yang kompleks dan beberapa biaya pemblokiran untuk berbagai operasi.
Kami berbicara tentang pertukaran antara pembuatan kode cepat ketika interpreter berjalan dan pembuatan kode cepat menggunakan kompiler yang mengoptimalkan. Tapi ada satu kompromi lagi, dan itu menyangkut penggunaan memori. Untuk menggambarkannya, saya menulis program JavaScript sederhana yang menambahkan dua angka.
function add(x, y) { return x + y; } add(1, 2);
Lihatlah bytecode yang dihasilkan untuk fungsi add oleh interpreter Ignition di V8.
StackCheck Ldar a1 Add a0, [0] Return
Jangan khawatir tentang bytecode, Anda tidak harus bisa membacanya. Di sini perlu untuk memperhatikan fakta bahwa itu
hanya berisi
4 instruksi .
Ketika kode menjadi panas, TurboFan menghasilkan kode mesin yang sangat optimal, yang disajikan di bawah ini:
leaq rcx,[rip+0x0] movq rcx,[rcx-0x37] testb [rcx+0xf],0x1 jnz CompileLazyDeoptimizedCode push rbp movq rbp,rsp push rsi push rdi cmpq rsp,[r13+0xe88] jna StackOverflow movq rax,[rbp+0x18] test al,0x1 jnz Deoptimize movq rbx,[rbp+0x10] testb rbx,0x1 jnz Deoptimize movq rdx,rbx shrq rdx, 32 movq rcx,rax shrq rcx, 32 addl rdx,rcx jo Deoptimize shlq rdx, 32 movq rax,rdx movq rsp,rbp pop rbp ret 0x18
Sebenarnya ada banyak tim di sini, terutama dibandingkan dengan empat tim yang kami lihat dalam bytecode. Secara umum, bytecode jauh lebih luas daripada kode mesin, dan khususnya kode mesin yang dioptimalkan. Bytecode, di sisi lain, dijalankan oleh penerjemah, sementara kode yang dioptimalkan dapat dieksekusi langsung oleh prosesor.
Ini adalah salah satu alasan mengapa mesin JavaScript tidak hanya "mengoptimalkan semuanya". Seperti yang kita lihat sebelumnya, menghasilkan kode mesin yang dioptimalkan membutuhkan banyak waktu, dan karena itu membutuhkan lebih banyak memori.
Untuk meringkas: Alasan mesin JavaScript memiliki tingkat optimasi yang berbeda adalah untuk menemukan kompromi antara pembuatan kode cepat menggunakan interpreter dan pembuatan kode cepat menggunakan kompilator pengoptimal. Menambahkan lebih banyak level optimisasi memungkinkan Anda untuk membuat keputusan yang lebih tepat, berdasarkan biaya kompleksitas tambahan dan overhead selama eksekusi. Selain itu, ada trade-off antara tingkat optimasi dan penggunaan memori. Itulah sebabnya mesin JavaScript hanya mencoba mengoptimalkan fungsi-fungsi panas.
Optimalkan akses ke properti prototipeTerakhir kali kami berbicara tentang bagaimana mesin JavaScript mengoptimalkan pemuatan properti objek menggunakan formulir dan cache Inline. Ingat bahwa mesin menyimpan bentuk objek secara terpisah dari nilai-nilai objek.

Formulir memungkinkan Anda menggunakan pengoptimalan menggunakan cache inline atau IC singkat. Saat bekerja bersama, formulir dan IC dapat mempercepat akses berulang ke properti dari tempat yang sama dalam kode Anda.

Jadi bagian pertama dari publikasi berakhir, dan tentang kelas dan pemrograman prototipe dapat ditemukan di bagian
kedua . Secara tradisional, kami menunggu komentar Anda dan diskusi penuh badai, serta kami mengundang Anda ke
hari terbuka pada kursus "Keamanan Sistem Informasi".