Как закрыты и области, представленные во время выполнения в JavaScript

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

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 способами, которые могут быть упущены компилятором.

Ответ 1

Неверно, что существуют ограничения, запрещающие вам вызывать 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

[email protected]:~$ 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 (Nitro) и Chakra

Building JSC - это боль в Linux, а Chakra не работает в Linux. Я считаю, что такое же поведение у таких же двигателей есть, и я был бы удивлен, если бы у Чакры тоже не было. (Делать что-нибудь лучшее быстро становится очень сложным, делая что-то еще хуже, ну, вы почти никогда не будете делать GC и иметь серьезные проблемы с памятью...)

Ответ 2

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

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

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

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

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