Jak są reprezentowane zamknięcia i zakresy w czasie uruchamiania w JavaScript

To pytanie z ciekawości. Rozważmy następujące funkcje

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++ ; } ;
}

W każdym przypadku, po wykonaniu funkcji, nie ma (myślę) możliwości dotarcia do x, więc BigObject może być zbierany jako śmieci, o ile x jest ostatnim odniesieniem do niego. Prosty interpreter przechwyciłby cały łańcuch zakresu, gdy wyrażenie funkcji jest oceniane. (Po pierwsze, musisz to zrobić, aby wykonać połączenia do eval work -- przykład poniżej). Inteligentniejsze wdrożenie może tego uniknąć w f0 i f1. Jeszcze bardziej inteligentna implementacja pozwoliłaby na zachowanie y, ale nie x , co jest konieczne, aby F2 był wydajny.

Moje pytanie brzmi jak nowoczesne silniki JavaScript (JaegerMonkey, V8, itp.) radzić sobie z takimi sytuacjami?

Wreszcie, oto przykład, który pokazuje, że zmienne mogą wymagać zachowania, nawet jeśli nigdy nie są wymienione w zagnieżdżonej funkcji.

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

Jednakże, istnieją ograniczenia, które uniemożliwiają zakradanie się do wywołania eval w sposób, który może zostać pominięty przez kompilator.

Author: Theodore Norvell, 2011-03-20

2 answers

To nie prawda, że istnieją ograniczenia, które uniemożliwiają wywołanie eval, które byłyby pominięte przez Analizę statyczną: po prostu takie odniesienia do eval działają w zasięgu globalnym. Zauważ, że jest to zmiana w ES5 z ES3, gdzie pośrednie i bezpośrednie odniesienia do eval zarówno działał w zakresie lokalnym, i jako takie, nie jestem pewien, czy cokolwiek faktycznie robi jakieś optymalizacje oparte na tym fakcie.

Oczywistym sposobem na przetestowanie tego jest uczynienie BigObject naprawdę dużym obiektem i wymusić gc po uruchomieniu f0-f2. (Ponieważ, hej, o ile myślę, że znam odpowiedź, testowanie jest zawsze lepsze!)

Więc...]}

Test

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
}

Dodałem testy dla eval / Function, wydaje się, że są to również ciekawe przypadki. Różnica między f5/f6 jest interesująca, ponieważ F5 jest tak naprawdę identyczna z f3, biorąc pod uwagę to, co jest tak naprawdę identyczną funkcją zamknięcia; f6 zwraca tylko coś, co raz ocenione daje to, a ponieważ eval nie został jeszcze oceniony, kompilator Nie wiem, czy nie ma w nim odniesienia do X.

SpiderMonkey

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 pojawia się w GC "x" na wszystkim oprócz f3, f5 i f6.

Wydaje się tak bardzo, jak to możliwe (tj., jeśli to możliwe, y, jak również x), chyba że istnieje bezpośrednie wywołanie eval w łańcuchu zakresu dowolnej funkcji, która nadal istnieje. (Nawet jeśli sam obiekt funkcji został GC ' D i już nie istnieje, Jak to ma miejsce w przypadku f5, co teoretycznie oznacza, że może 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 wydaje się Gc x na wszystkim oprócz f3, f5 i f6. Jest to identyczne jak SpiderMonkey, patrz Analiza powyżej. (Zauważ jednak, że liczby nie są wystarczająco szczegółowe, aby powiedzieć, czy y jest GC ' D, gdy x nie jest, nie pofatygowałem się, aby zbadać to.)

Carakan

Nie będę zawracał sobie głowy uruchomieniem tego ponownie, ale nie trzeba dodawać, że zachowanie jest identyczne z SpiderMonkey i V8. Trudniejsze do przetestowania bez powłoki JS, ale wykonalne z czas.

JSC (Nitro) i czakra

Budowanie JSC to ból w Linuksie, a Chakra nie działa na Linuksie. Wydaje mi się, że JSC ma takie samo zachowanie jak wyżej wymienione silniki. zdziwiłbym się, gdyby nie Chakra. (Robienie czegoś lepszego szybko staje się bardzo złożone, robienie czegoś gorszego, cóż, prawie nigdy nie robiłbyś GC i nie miałbyś poważnych problemów z pamięcią...) {]}

 36
Author: gsnedders,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-20 16:48:00

W normalnych sytuacjach zmienne lokalne w funkcji są przydzielane na stosie -- i" automatycznie " znikają, gdy funkcja powróci. Uważam, że wiele popularnych silników JavaScript uruchamia interpreter (lub kompilator JIT) na architekturze stosu maszyn, więc ta awersacja powinna być rozsądnie poprawna.

Teraz, jeśli zmienna jest odwołana do zamknięcia (tj. przez funkcję zdefiniowaną lokalnie, która może być wywołana później), funkcji" wewnątrz "przypisany jest " łańcuch zakresu", który rozpoczyna z najbardziej wewnętrznym zakresem , który jest samą funkcją. Następnym zakresem jest funkcja zewnętrzna (która zawiera dostęp do zmiennej lokalnej). Interpreter (lub kompilator) utworzy "closure", zasadniczo kawałek pamięci przydzielonej na stos (nie stos), który zawiera te zmienne w zakresie.

W związku z tym, jeśli zmienne lokalne są odwoływane w zamknięciu, nie są już przydzielane na stosie (co spowoduje, że znikną, gdy funkcja zwroty). Są one przydzielane tak jak normalne, długotrwałe zmienne, A "Zakres" zawiera wskaźnik do każdej z nich. "Scope-chain" funkcji wewnętrznej zawiera wskaźniki do wszystkich tych "zakresów".

Niektóre silniki optymalizują łańcuch zakresu, pomijając zmienne, które są zacienione (tj. zasłonięte przez zmienną lokalną w wewnętrznym zakresie), więc w Twoim przypadku pozostaje tylko jeden BigObject, o ile zmienna " x " jest dostępna tylko w wewnętrznym zakresie i nie ma wywołań "eval" w wewnętrznym zakresie. zewnętrzne lunety. Niektóre silniki "spłaszczają" łańcuchy zakresu (myślę, że robi to V8) dla szybkiej zmiennej rozdzielczości - coś, co można zrobić tylko wtedy, gdy nie ma wywołań "eval" pomiędzy (lub nie ma wywołań funkcji, które mogą wykonywać Ukryte eval, np. setTimeout).

Chciałbym zaprosić jakiegoś Guru JavaScript engine ' s, aby podać więcej soczystych szczegółów niż mogę.

 10
Author: Stephen Chung,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-20 13:01:09