Pada awal April, artikel "JavaScript: Call stack dan keajaiban ukurannya" diterbitkan di HabrΓ© - penulisnya sampai pada kesimpulan bahwa setiap frame stack menempati (72 + 8 * jumlah variabel_lokal) byte: "Ternyata kami telah menghitung semuanya dengan benar dan kami dapat menegaskan bahwa ukuran ExecutionStack kosong di Chrome adalah 72 byte, dan ukuran callstack hanya di bawah satu megabyte. Kerja bagus! "
Untuk penyemaian - mari ubah sedikit kode yang digunakan AxemaFr untuk eksperimen:
{let i = 0;
const func = () => {
i += 1.00000000000001;
func();
};
try {
func();
} catch (e) {
console.log(i);
}}
Alih-alih 1, sekarang di setiap langkah kami menambahkan sedikit lagi, dan sebagai hasilnya, alih-alih 13951, kami mendapatkan 12556.000000000002 - seolah-olah variabel lokal ditambahkan ke fungsi!
Mari ulangi pertanyaan yang diajukan oleh Senior Frontend Developer AxemaFr: βMengapa demikian? Apa yang berubah? Bagaimana memahaminya, melihat fungsinya, berapa kali bisa dieksekusi secara rekursif ?! "
Alat memasak
Pada baris perintah Chrome, Anda dapat meneruskan argumen ke mesin JS; khususnya, Anda dapat mengubah ukuran tumpukan dari 984 KB ke kunci lainnya
--js-flags=--stack-size=
.
Untuk mengetahui berapa banyak tumpukan yang dibutuhkan setiap fungsi, kunci yang
--print-bytecode
telah disebutkan akan membantu kami . Tidak disebutkan bahwa keluaran debug dikirim ke stdout, yang secara bodoh tidak dimiliki Chrome di bawah Windows, karena dikompilasi sebagai aplikasi GUI. Mudah untuk memperbaikinya: buat salinan chrome.exe, dan di editor hex favorit Anda perbaiki byte
0xD4
dari nilai
0x02
menjadi
0x03
(bagi mereka yang tidak berteman dengan editor hex, byte ini akan membantu memperbaiki skrip Python). Tetapi jika Anda membaca artikel ini sekarang di Chrome, dan hanya menjalankan file yang ditambal - katakanlah Anda menamakannya cui_chrome.exe - maka jendela baru akan terbuka di browser yang ada dan argumennya
--js-flags
akan diabaikan. Untuk memulai contoh baru Chrome, Anda perlu mengirimkannya beberapa yang baru
--user-data-dir
:
cui_chrome.exe --no-sandbox --js-flags="--print-bytecode --print-bytecode-filter=func" --user-data-dir=\Windows\Temp
Tanpanya,
--print-bytecode-filter
Anda akan tenggelam dalam tumpukan bytecode kilometer dari fungsi yang dibangun ke dalam Chrome.
Setelah meluncurkan browser, buka konsol pengembang dan masukkan kode yang digunakan AxemaFr:
{let i = 0;
const func = () => {
i++;
func();
};
func()}
Sebelum Anda menekan Enter, dump akan muncul di jendela konsol di belakang Chrome:
[dihasilkan bytecode untuk function: func (0x44db08635355 <SharedFunctionInfo func>)]
Jumlah parameter 1
Register count 1
Frame size 8
36 S> 000044DB086355EE @ 0 : 1a 02 LdaCurrentContextSlot [2]
000044DB086355F0 @ 2 : ac 00 ThrowReferenceErrorIfHole [0]
000044DB086355F2 @ 4 : 4d 00 Inc [0]
000044DB086355F4 @ 6 : 26 fa Star r0
000044DB086355F6 @ 8 : 1a 02 LdaCurrentContextSlot [2]
37 E> 000044DB086355F8 @ 10 : ac 00 ThrowReferenceErrorIfHole [0]
000044DB086355FA @ 12 : 25 fa Ldar r0
000044DB086355FC @ 14 : 1d 02 StaCurrentContextSlot [2]
44 S> 000044DB086355FE @ 16 : 1b 03 LdaImmutableCurrentContextSlot [3]
000044DB08635600 @ 18 : ac 01 ThrowReferenceErrorIfHole [1]
000044DB08635602 @ 20 : 26 fa Star r0
44 E> 000044DB08635604 @ 22: 5d fa 01 CallUndefinedReceiver0 r0, [1]
000044DB08635607 @ 25: 0d LdaUndefined
52 S> 000044DB08635608 @ 26: ab Kembali
Kolam konstan (size = 2)
Meja Penangan (size = 0)
Tabel Posisi Sumber (size = 12)
Bagaimana dump berubah jika saluran
i++;
diganti dengan
i += 1.00000000000001;
?
[dihasilkan bytecode untuk function: func (0x44db0892d495 <SharedFunctionInfo func>)]
Jumlah parameter 1
Daftar hitungan 2
Ukuran bingkai 16
36 S> 000044DB0892D742 @ 0: 1a 02 LdaCurrentContextSlot [2]
000044DB0892D744 @ 2: ac 00 ThrowReferenceErrorIfHole [0]
000044DB0892D746 @ 4: 26 fa Bintang r0
000044DB0892D748 @ 6: 12 01 LdaConstant [1]
000044DB0892D74A @ 8 : 35 fa 00 Add r0, [0]
000044DB0892D74D @ 11 : 26 f9 Star r1
000044DB0892D74F @ 13 : 1a 02 LdaCurrentContextSlot [2]
37 E> 000044DB0892D751 @ 15 : ac 00 ThrowReferenceErrorIfHole [0]
000044DB0892D753 @ 17 : 25 f9 Ldar r1
000044DB0892D755 @ 19 : 1d 02 StaCurrentContextSlot [2]
60 S> 000044DB0892D757 @ 21 : 1b 03 LdaImmutableCurrentContextSlot [3]
000044DB0892D759 @ 23 : ac 02 ThrowReferenceErrorIfHole [2]
000044DB0892D75B @ 25 : 26 fa Star r0
60 E> 000044DB0892D75D @ 27 : 5d fa 01 CallUndefinedReceiver0 r0, [1]
000044DB0892D760 @ 30 : 0d LdaUndefined
68 S> 000044DB0892D761 @ 31: ab Kembali
Kolam konstan (size = 3)
Meja Penangan (size = 0)
Tabel Posisi Sumber (size = 12)
Sekarang mari kita cari tahu apa yang berubah dan mengapa.
Menjelajahi contoh
Semua opcode V8 dijelaskan di github.com/v8/v8/blob/master/src/interpreter/interpreter-generator.cc
Dump pertama didekodekan seperti ini:
LdaCurrentContextSlot [2]; a: = konteks [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
; throw ("ReferenceError:% s tidak ditentukan", const [0])
Inc [0]; a ++
Bintang r0; r0: = a
LdaCurrentContextSlot [2]; a: = konteks [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
; throw ("ReferenceError:% s tidak ditentukan", const [0])
Ldar r0; a: = r0
StaCurrentContextSlot [2] ; context[2] := a
LdaImmutableCurrentContextSlot [3] ; a := context[3]
ThrowReferenceErrorIfHole [1] ; if (a === undefined)
; throw("ReferenceError: %s is not defined", const[1])
Star r0 ; r0 := a
CallUndefinedReceiver0 r0, [1] ; r0()
LdaUndefined ; a := undefined
Return
Argumen terakhir meng
Inc
- opcode dan
CallUndefinedReceiver0
menyetel slot umpan balik, di mana pengoptimal mengumpulkan statistik tentang jenis yang digunakan. Ini tidak mempengaruhi semantik bytecode, jadi hari ini kita sama sekali tidak tertarik.
Di bawah dump ada postscript: "Constant pool (size = 2)" - dan memang kita melihat bahwa bytecode menggunakan dua baris -
"i"
dan
"func"
- untuk substitusi dalam pesan pengecualian ketika simbol dengan nama seperti itu tidak ditentukan. Ada postscript di atas dump: "Frame size 8" - sesuai dengan fakta bahwa fungsinya menggunakan satu register interpreter (
r0
).
Bingkai tumpukan fungsi kami terdiri dari:
- argumen tunggal
this
; - alamat pengirim;
- jumlah argumen yang diteruskan (
arguments.length
); - referensi ke kumpulan konstan dengan string yang digunakan;
- tautan ke konteks dengan variabel lokal;
- tiga petunjuk lagi yang dibutuhkan oleh mesin; dan akhirnya
- ruang untuk satu register.
Total 9 * 8 = 72 byte, sebagai penanda AxemaFrdan menemukan jawabannya.
Dari tujuh suku yang terdaftar, secara teoritis, tiga dapat berubah - jumlah argumen, keberadaan kumpulan konstan, dan jumlah register. Apa yang kami dapatkan dalam varian dengan 1.00000000000001?
LdaCurrentContextSlot [2]; a: = konteks [2]
ThrowReferenceErrorIfHole [0]; if (a === undefined)
; throw ("ReferenceError:% s tidak ditentukan", const [0])
Bintang r0; r0: = a
LdaConstant [1]; a: = const [1]
Tambahkan r0, [0]; a + = r0
Bintang r1; r1: = a
; ... lebih jauh seperti sebelumnya
Pertama, konstanta yang ditambahkan menempati tempat ketiga di kumpulan konstan; kedua, satu register lagi diperlukan untuk memuatnya, sehingga stack frame fungsi bertambah 8 byte.
Jika Anda tidak menggunakan simbol bernama dalam fungsi tersebut, maka Anda dapat melakukannya tanpa kumpulan konstan. Di github.com/v8/v8/blob/master/src/execution/frame-constants.h#L289 dijelaskan format bingkai tumpukan V8 dan menyatakan bahwa ketika kumpulan konstan tidak digunakan, ukuran bingkai tumpukan dikurangi dengan satu penunjuk . Bagaimana Anda bisa yakin akan hal ini? Sekilas, tampaknya fungsi yang tidak menggunakan simbol bernama tidak bisa rekursif; tapi lihatlah:
{let i = 0;
function func() {
this()();
};
const helper = () => (i++, func.bind(helper));
try {
helper()();
} catch (e) {
console.log(i);
}}
[generated bytecode for function: func (0x44db0878e575 <SharedFunctionInfo func>)]
Parameter count 1
Register count 1
Frame size 8
37 S> 000044DB0878E8DA @ 0 : 5e 02 02 00 CallUndefinedReceiver1 <this>, <this>, [0]
000044DB0878E8DE @ 4 : 26 fa Star r0
43 E> 000044DB0878E8E0 @ 6 : 5d fa 02 CallUndefinedReceiver0 r0, [2]
000044DB0878E8E3 @ 9 : 0d LdaUndefined
47 S> 000044DB0878E8E4 @ 10 : ab Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 8)
Sasaran - "Kumpulan konstan (ukuran = 0)" - telah tercapai; tetapi stack overflow, seperti sebelumnya, terjadi melalui panggilan 13951. Ini berarti bahwa meskipun kumpulan konstanta tidak digunakan, bingkai tumpukan fungsi masih berisi penunjuk ke sana.
Apakah mungkin untuk mencapai ukuran bingkai tumpukan yang lebih kecil dari yang dihitung AxemaFrnilai minimum? - ya, jika tidak ada register yang digunakan di dalam fungsi:
{function func() {
this();
};
let chain = ()=>null;
for(let i=0; i<15050; i++)
chain = func.bind(chain);
chain()}
[dihasilkan bytecode untuk function: func (0x44db08c34059 <SharedFunctionInfo func>)]
Jumlah parameter 1
Daftarkan hitungan 0
Ukuran bingkai 0
25 S> 000044DB08C34322 @ 0: 5d 02 00 CallUndefinedReceiver0 <this>, [0]
000044DB08C34325 @ 3: 0d LdaUndefined
29 S> 000044DB08C34326 @ 4: ab Kembali
Kolam konstan (size = 0)
Meja Penangan (size = 0)
Tabel Posisi Sumber (size = 6)
(Dalam hal ini, rangkaian panggilan dari 15051 sudah mengarah ke "RangeError: Ukuran tumpukan panggilan maksimum terlampaui".)
Jadi, kesimpulan dari penanda tangan AxemaFrbahwa "ukuran ExecutionStack kosong di Chrome adalah 72 byte" telah berhasil disangkal.
Memperbaiki prediksi
Kami dapat membantah bahwa ukuran bingkai tumpukan minimum untuk fungsi JS di Chrome adalah 64 byte. Untuk ini, Anda perlu menambahkan 8 byte untuk setiap parameter formal yang dideklarasikan, 8 byte lainnya untuk setiap parameter aktual yang melebihi jumlah yang dideklarasikan, dan 8 byte lainnya untuk setiap register yang digunakan. Register dialokasikan untuk setiap variabel lokal, untuk memuat konstanta, untuk mengakses variabel dari konteks eksternal, untuk meneruskan parameter aktual selama panggilan, dll. Hampir tidak mungkin untuk menentukan jumlah pasti register bekas dari kode sumber di JS. Perlu dicatat bahwa interpreter JS mendukung jumlah register yang tidak terbatas - mereka tidak terkait dengan register prosesor tempat interpreter dijalankan.
Sekarang jelas mengapa:
- (
func = (x) => { i++; func(); };
) , ; - (
func = () => { i++; func(1); };
) , β :[generated bytecode for function: func (0x44db08e12da1 <SharedFunctionInfo func>)] Parameter count 1 Register count 2 Frame size 16 34 S> 000044DB08E12FE2 @ 0 : 1a 02 LdaCurrentContextSlot [2] 000044DB08E12FE4 @ 2 : ac 00 ThrowReferenceErrorIfHole [0] 000044DB08E12FE6 @ 4 : 4d 00 Inc [0] 000044DB08E12FE8 @ 6 : 26 fa Star r0 000044DB08E12FEA @ 8 : 1a 02 LdaCurrentContextSlot [2] 35 E> 000044DB08E12FEC @ 10 : ac 00 ThrowReferenceErrorIfHole [0] 000044DB08E12FEE @ 12 : 25 fa Ldar r0 000044DB08E12FF0 @ 14 : 1d 02 StaCurrentContextSlot [2] 39 S> 000044DB08E12FF2 @ 16 : 1b 03 LdaImmutableCurrentContextSlot [3] 000044DB08E12FF4 @ 18 : ac 01 ThrowReferenceErrorIfHole [1] 000044DB08E12FF6 @ 20 : 26 fa Star r0 000044DB08E12FF8 @ 22 : 0c 01 LdaSmi [1] 000044DB08E12FFA @ 24 : 26 f9 Star r1 39 E> 000044DB08E12FFC @ 26 : 5e fa f9 01 CallUndefinedReceiver1 r0, r1, [1] 000044DB08E13000 @ 30 : 0d LdaUndefined 48 S> 000044DB08E13001 @ 31 : ab Return Constant pool (size = 2) Handler Table (size = 0) Source Position Table (size = 12) - 1.00000000000001 β
r1
, .