Pertanyaan Bagaimana penutupan dan cakupan diwakili pada waktu berjalan dalam JavaScript


Ini sebagian besar merupakan pertanyaan yang tidak diinginkan. Pertimbangkan fungsi-fungsi berikut

var closure ;
function f0() {
    var x = new BigObject() ;
    var y = 0 ;
    closure = function(){ return 7; } ;
}
function f1() {
    var x = BigObject() ;
    closure =  (function(y) { return function(){return y++;} ; })(0) ;
}
function f2() {
    var x = BigObject() ;
    var y = 0 ;
    closure = function(){ return y++ ; } ;
}

Dalam setiap kasus, setelah fungsi tersebut dijalankan, ada (saya pikir) tidak ada cara untuk mencapai x dan begitu BigObject bisa menjadi sampah yang dikumpulkan, selama x adalah referensi terakhir untuk itu. Seorang penerjemah berpikiran sederhana akan menangkap seluruh rantai lingkup setiap kali ekspresi fungsi dievaluasi. (Untuk satu hal, Anda perlu melakukan ini untuk melakukan panggilan ke eval bekerja - contoh di bawah). Implementasi yang lebih cerdas mungkin menghindari ini di f0 dan f1. Implementasi yang lebih cerdas pun akan memungkinkan y untuk dipertahankan, tetapi tidak x, seperti yang diperlukan untuk f2 menjadi efisien.

Pertanyaan saya adalah bagaimana mesin JavaScript modern (JaegerMonkey, V8, dll.) Menghadapi situasi ini?

Akhirnya, di sini adalah contoh yang menunjukkan bahwa variabel mungkin perlu dipertahankan bahkan jika mereka tidak pernah disebutkan dalam fungsi nested.

var f = (function(x, y){ return function(str) { return eval(str) ; } } )(4, 5) ;
f("1+2") ; // 3
f("x+y") ; // 9
f("x=6") ;
f("x+y") ; // 11

Namun, ada pembatasan yang mencegah seseorang menyelinap dalam panggilan untuk eval dengan cara yang mungkin terlewatkan oleh kompilator.


32
2018-03-20 11:03


asal


Jawaban:


Tidak benar bahwa ada pembatasan yang mencegah Anda memanggil eval yang akan dilewatkan oleh analisis statis: hanya saja referensi tersebut untuk eval berjalan dalam lingkup global. Perhatikan bahwa ini adalah perubahan ES5 dari ES3, di mana referensi langsung dan langsung ke eval keduanya berjalan di lingkup lokal, dan karena itu, saya tidak yakin apakah sebenarnya ada pengoptimalan berdasarkan fakta ini.

Cara yang jelas untuk menguji ini adalah membuat BigObject menjadi objek yang sangat besar, dan memaksa gc setelah menjalankan f0 – f2. (Karena, hei, sebanyak yang saya kira saya tahu jawabannya, pengujian selalu lebih baik!)

Begitu…

Ujian

var closure;
function BigObject() {
  var a = '';
  for (var i = 0; i <= 0xFFFF; i++) a += String.fromCharCode(i);
  return new String(a); // Turn this into an actual object
}
function f0() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return 7; };
}
function f1() {
  var x = new BigObject();
  closure =  (function(y) { return function(){return y++;}; })(0);
}
function f2() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return y++; };
}
function f3() {
  var x = new BigObject();
  var y = 0;
  closure = eval("(function(){ return 7; })"); // direct eval
}
function f4() {
  var x = new BigObject();
  var y = 0;
  closure = (1,eval)("(function(){ return 7; })"); // indirect eval (evaluates in global scope)
}
function f5() {
  var x = new BigObject();
  var y = 0;
  closure = (function(){ return eval("(function(){ return 7; })"); })();
}
function f6() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return eval("(function(){ return 7; })"); };
}
function f7() {
  var x = new BigObject();
  var y = 0;
  closure = (function(){ return (1,eval)("(function(){ return 7; })"); })();
}
function f8() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return (1,eval)("(function(){ return 7; })"); };
}
function f9() {
  var x = new BigObject();
  var y = 0;
  closure = new Function("return 7;"); // creates function in global scope
}

Saya telah menambahkan tes untuk eval / Function, sepertinya ini juga merupakan kasus yang menarik. Perbedaan antara f5 / f6 menarik, karena f5 benar-benar identik dengan f3, mengingat apa yang sebenarnya merupakan fungsi yang identik untuk penutupan; f6 hanya mengembalikan sesuatu yang pernah dievaluasi memberikan itu, dan karena eval belum dievaluasi, compiler tidak dapat mengetahui bahwa tidak ada referensi ke x di dalamnya.

Monyet laba-laba

js> gc();
"before 73728, after 69632, break 01d91000\n"
js> f0();
js> gc(); 
"before 6455296, after 73728, break 01d91000\n"
js> f1(); 
js> gc(); 
"before 6455296, after 77824, break 01d91000\n"
js> f2(); 
js> gc(); 
"before 6455296, after 77824, break 01d91000\n"
js> f3(); 
js> gc(); 
"before 6455296, after 6455296, break 01db1000\n"
js> f4(); 
js> gc(); 
"before 12828672, after 73728, break 01da2000\n"
js> f5(); 
js> gc(); 
"before 6455296, after 6455296, break 01da2000\n"
js> f6(); 
js> gc(); 
"before 12828672, after 6467584, break 01da2000\n"
js> f7(); 
js> gc(); 
"before 12828672, after 73728, break 01da2000\n"
js> f8(); 
js> gc(); 
"before 6455296, after 73728, break 01da2000\n"
js> f9(); 
js> gc(); 
"before 6455296, after 73728, break 01da2000\n"

SpiderMonkey tampaknya GC "x" pada semuanya kecuali f3, f5, dan f6.

Tampaknya sebanyak mungkin (yaitu, bila mungkin, y, serta x) kecuali ada panggilan eval langsung dalam rantai-ruang lingkup dari fungsi apa pun yang masih ada. (Bahkan jika objek fungsi itu sendiri telah GC'd dan tidak ada lagi, seperti halnya di f5, yang secara teoritis berarti bahwa itu bisa GC x / y.)

V8

gsnedders@dolores:~$ v8 --expose-gc --trace_gc --shell foo.js
V8 version 3.0.7
> gc();
Mark-sweep 0.8 -> 0.7 MB, 1 ms.
> f0();
Scavenge 1.7 -> 1.7 MB, 2 ms.
Scavenge 2.4 -> 2.4 MB, 2 ms.
Scavenge 3.9 -> 3.9 MB, 4 ms.
> gc();   
Mark-sweep 5.2 -> 0.7 MB, 3 ms.
> f1();
Scavenge 4.7 -> 4.7 MB, 9 ms.
> gc();
Mark-sweep 5.2 -> 0.7 MB, 3 ms.
> f2();
Scavenge 4.8 -> 4.8 MB, 6 ms.
> gc();
Mark-sweep 5.3 -> 0.8 MB, 3 ms.
> f3();
> gc();
Mark-sweep 5.3 -> 5.2 MB, 17 ms.
> f4();
> gc();
Mark-sweep 9.7 -> 0.7 MB, 5 ms.
> f5();
> gc();
Mark-sweep 5.3 -> 5.2 MB, 12 ms.
> f6();
> gc();
Mark-sweep 9.7 -> 5.2 MB, 14 ms.
> f7();
> gc();
Mark-sweep 9.7 -> 0.7 MB, 5 ms.
> f8();
> gc();
Mark-sweep 5.2 -> 0.7 MB, 2 ms.
> f9();
> gc();
Mark-sweep 5.2 -> 0.7 MB, 2 ms.

V8 muncul ke GC x pada semua yang terpisah dari f3, f5, dan f6. Ini identik dengan SpiderMonkey, lihat analisis di atas. (Perlu dicatat bahwa angka-angkanya tidak cukup rinci untuk mengetahui apakah y sedang GC'd ketika x tidak, saya tidak peduli untuk menyelidiki ini.)

Carakan

Saya tidak akan repot-repot menjalankan ini lagi, tetapi tidak perlu dikatakan bahwa perilaku identik dengan SpiderMonkey dan V8. Lebih sulit untuk menguji tanpa shell JS, tetapi dapat dilakukan dengan waktu.

JSC (Nitro) dan Chakra

Membangun JSC adalah rasa sakit di Linux, dan Chakra tidak berjalan di Linux. Saya percaya JSC memiliki perilaku yang sama dengan mesin di atas, dan saya akan terkejut jika Chakra tidak memilikinya juga. (Melakukan sesuatu yang lebih baik dengan cepat menjadi sangat kompleks, melakukan sesuatu yang lebih buruk, baik, Anda hampir tidak akan pernah melakukan GC dan memiliki masalah memori yang serius ...)


36
2018-03-20 16:48



Dalam situasi normal, variabel lokal dalam fungsi dialokasikan pada stack - dan mereka "otomatis" pergi ketika fungsi kembali. Saya yakin banyak mesin JavaScript yang populer menjalankan interpreter (atau compiler JIT) pada arsitektur mesin tumpukan sehingga obversasi ini seharusnya cukup valid.

Sekarang jika sebuah variabel dirujuk dalam sebuah penutupan (yaitu oleh fungsi yang didefinisikan secara lokal yang dapat dipanggil nanti), fungsi "dalam" diberi "rantai lingkup" yang dimulai dengan yang paling dalam cakupan yang merupakan fungsi itu sendiri. Ruang lingkup selanjutnya adalah fungsi luar (yang berisi variabel lokal yang diakses). Interpreter (atau kompilator) akan membuat "penutupan", pada dasarnya bagian dari memori yang dialokasikan pada tumpukan (bukan tumpukan) yang berisi variabel-variabel dalam ruang lingkup.

Oleh karena itu, jika variabel lokal disebut dalam penutupan, mereka tidak lagi dialokasikan pada stack (yang akan membuat mereka pergi ketika fungsi kembali). Mereka dialokasikan seperti biasa, variabel berumur panjang, dan "lingkup" berisi pointer ke masing-masing. "Lingkup-rantai" dari fungsi dalam berisi pointer ke semua "lingkup" ini.

Beberapa mesin mengoptimalkan rantai lingkup dengan menghilangkan variabel yang dibayangi (yaitu ditutupi oleh variabel lokal dalam ruang lingkup batin), sehingga dalam kasus Anda hanya satu BigObject tetap, selama variabel "x" hanya diakses dalam lingkup bagian dalam , dan tidak ada panggilan "eval" di luar lingkup. Beberapa mesin "meratakan" lingkup rantai (saya pikir V8 melakukan itu) untuk resolusi variabel cepat - sesuatu yang dapat dilakukan hanya jika tidak ada "eval" panggilan di antara (atau tidak ada panggilan ke fungsi yang dapat melakukan eval implisit, misalnya setTimeout).

Saya akan mengundang beberapa guru mesin JavaScript untuk memberikan detail yang lebih menarik daripada yang saya bisa.


10
2018-03-20 12:54