Как замыкания и области представляются во время выполнения в JavaScript - PullRequest
32 голосов
/ 20 марта 2011

Это в основном вопрос из любопытства. Рассмотрим следующие функции

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

В каждом случае, после того, как функция была выполнена, (я думаю) нет никакого способа достичь x , и поэтому BigObject можно собирать, пока x является последней ссылкой на него. Простой интерпретируемый интерпретатор будет захватывать всю цепочку области действия всякий раз, когда вычисляется выражение функции. (Во-первых, вам нужно сделать это, чтобы звонки на eval работали - пример ниже). Более разумная реализация может избежать этого в f0 и f1. Еще более разумная реализация позволила бы сохранить y , но не x , что необходимо для эффективности f2.

Мой вопрос заключается в том, как современные движки JavaScript (JaegerMonkey, V8 и т. Д.) Справляются с этими ситуациями?

Наконец, вот пример, который показывает, что переменные, возможно, потребуется сохранить, даже если они никогда не упоминаются во вложенной функции.

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

Однако существуют ограничения, которые не позволяют прокрасться при вызове eval таким образом, который может быть пропущен компилятором.

Ответы [ 2 ]

36 голосов
/ 20 марта 2011

Это неправда, что существуют ограничения, которые не позволяют вам вызывать eval, который был бы пропущен при статическом анализе: просто такие ссылки на eval выполняются в глобальной области видимости. Обратите внимание, что это изменение в ES5 по сравнению с ES3, когда косвенные и прямые ссылки на eval оба выполнялись в локальной области, и поэтому я не уверен, что что-то действительно делает какие-либо оптимизации, основанные на этом факте.

Очевидный способ проверить это - сделать BigObject действительно большим объектом и принудительно вызвать gc после запуска f0 – f2. (Потому что, насколько я думаю, я знаю ответ, тестирование всегда лучше!)

Итак ...

Тест

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
}

Я добавил тесты для eval / Function, похоже, это тоже интересные случаи. Различия между f5 / f6 интересны, потому что f5 на самом деле просто идентична f3, учитывая, что на самом деле является идентичной функцией для замыкания; f6 просто возвращает что-то, что когда-то вычислено, дает это, и поскольку eval еще не был оценен, компилятор не может знать, что в нем нет ссылки на 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 отображается для GC "x" на всем, кроме f3, f5 и f6.

Это выглядит как можно больше (то есть, когда это возможно, y, а также x), если в цепочке областей действия какой-либо функции, которая все еще существует, нет прямого вызова eval. (Даже если сам объект функции был GC'd и больше не существует, как в случае с f5, что теоретически означает, что он мог бы 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 отображается для GC x на всем, кроме f3, f5 и f6. Это идентично SpiderMonkey, см. Анализ выше. (Тем не менее, обратите внимание, что цифры не достаточно подробны, чтобы сказать, является ли y GC'd, когда x нет, я не потрудился исследовать это.)

Carakan

Я не собираюсь снова запускать это, но само собой разумеется, что поведение идентично SpiderMonkey и V8. Сложнее тестировать без оболочки JS, но выполнимо со временем.

АО (Нитро) и Чакра

Создание JSC - это боль в Linux, а Chakra не работает в Linux. Я полагаю, что АО имеет то же самое поведение с вышеупомянутыми двигателями, и я был бы удивлен, если бы у Чакры тоже не было. (Делать что-то лучше быстро становится очень сложно, делать что-то хуже, ну, вы почти никогда не будете делать ГХ и у вас будут серьезные проблемы с памятью…)

10 голосов
/ 20 марта 2011

В обычных ситуациях локальные переменные в функции размещаются в стеке - и они «автоматически» исчезают, когда функция возвращается. Я полагаю, что многие популярные движки JavaScript запускают интерпретатор (или JIT-компилятор) на архитектуре стековой машины, поэтому этот обзор должен быть разумным.

Теперь, если переменная упоминается в замыкании (т. Е. С помощью функции, определенной локально, которая может быть вызвана позднее), функции "inside" присваивается "цепочка областей действия", которая начинается с самого внутреннего область действия , которая является самой функцией. Следующая область видимости - это внешняя функция (которая содержит локальную переменную, к которой осуществляется доступ). Интерпретатор (или компилятор) создаст «замыкание», по сути, часть памяти, выделенной в куче (не в стеке), которая содержит эти переменные в области действия.

Следовательно, если локальные переменные упоминаются в замыкании, они больше не размещаются в стеке (что приведет к их исчезновению при возврате из функции). Они размещаются так же, как и обычные долгоживущие переменные, а «область действия» содержит указатель на каждую из них. «Область видимости» внутренней функции содержит указатели на все эти «области видимости».

Некоторые движки оптимизируют цепочку областей видимости, опуская теневые переменные (то есть закрытые локальной переменной во внутренней области видимости), поэтому в вашем случае остается только один BigObject, пока к переменной «x» обращаются только в внутренняя область видимости, и во внешних областях видимости нет вызовов eval. Некоторые движки «сплющивают» цепочки областей видимости (я думаю, V8 ​​делает это) для быстрого разрешения переменных - то, что может быть сделано, только если между ними нет «eval» вызовов (или нет вызовов функций, которые могут выполнять неявное eval, например, SetTimeout).

Я бы пригласил некоторых гуру движка JavaScript предоставить больше сочных деталей, чем смогу.

...