Pregunta Cómo se representan los cierres y ámbitos en tiempo de ejecución en JavaScript


Esta es principalmente una pregunta fuera de la curiosidad. Considere las siguientes funciones

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

En todos los casos, después de que se haya ejecutado la función, no hay (creo) forma de alcanzar X y entonces el BigObject puede ser basura recolectada, siempre y cuando X es la última referencia a eso. Un intérprete de mente simple capturaría toda la cadena de alcance siempre que se evalúe una expresión de función. (Por un lado, debe hacer esto para hacer llamadas a eval trabajo - ejemplo a continuación). Una implementación más inteligente podría evitar esto en f0 y f1. Una implementación aún más inteligente permitiría y para ser retenido, pero no X, como se necesita para que f2 sea eficiente.

Mi pregunta es ¿cómo los motores de JavaScript modernos (JaegerMonkey, V8, etc.) se ocupan de estas situaciones?

Finalmente, aquí hay un ejemplo que muestra que las variables pueden necesitar ser retenidas incluso si nunca se mencionan en la función anidada.

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

Sin embargo, existen restricciones que le impiden a uno hacer un llamado a la evaluación de manera que el compilador puede pasar por alto.


32
2018-03-20 11:03


origen


Respuestas:


No es cierto que haya restricciones que le impidan llamar a eval que el análisis estático omitirá: es solo que tales referencias a eval se ejecutan en el ámbito global. Tenga en cuenta que esto es un cambio en ES5 de ES3, donde las referencias indirectas y directas a eval se ejecutan en el ámbito local, y como tal, no estoy seguro de si algo realmente hace alguna optimización basada en este hecho.

Una manera obvia de probar esto es hacer que BigObject sea un objeto realmente grande, y forzar un gc después de ejecutar f0-f2. (Porque, oye, por más que creo saber la respuesta, ¡las pruebas siempre son mejores!)

Asi que…

La prueba

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
}

He agregado pruebas para eval / Function, pareciendo que estos también son casos interesantes. La diferencia entre f5 / f6 es interesante, porque f5 es realmente idéntica a f3, dado lo que realmente es una función idéntica para el cierre; f6 simplemente devuelve algo que una vez evaluado da eso, y como la evaluación aún no se ha evaluado, el compilador no puede saber que no hay referencia a x dentro de ella.

Mono araña

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 aparece en GC "x" en todo excepto en f3, f5 y f6.

Parece tanto como sea posible (es decir, cuando sea posible, y, así como x) a menos que haya una llamada de evaluación directa dentro de la cadena de alcance de cualquier función que aún exista. (Incluso si ese objeto de función en sí mismo ha sido GC'd y ya no existe, como es el caso de f5, que teóricamente significa que podría 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 aparece en GC x en todo aparte de f3, f5 y f6. Esto es idéntico a SpiderMonkey, vea el análisis anterior. (Tenga en cuenta, sin embargo, que los números no son lo suficientemente detallados para decir si y está siendo GC'd cuando x no lo es, no me he molestado en investigar esto).

Carakan

No voy a molestarme en ejecutar esto de nuevo, pero no hace falta decir que el comportamiento es idéntico al de SpiderMonkey y V8. Más difícil de probar sin un shell JS, pero factible con el tiempo.

JSC (Nitro) y Chakra

Construir JSC es un dolor en Linux, y Chakra no se ejecuta en Linux. Creo que JSC tiene el mismo comportamiento con los motores anteriores, y me sorprendería si Chakra no tuviera también. (Hacer algo mejor rápidamente se vuelve muy complejo, haciendo algo peor, bueno, casi nunca harías GC y tendrías serios problemas de memoria ...)


36
2018-03-20 16:48



En situaciones normales, las variables locales en una función se asignan en la pila y desaparecen "automáticamente" cuando la función retorna. Creo que muchos motores populares de JavaScript ejecutan el intérprete (o el compilador JIT) en una arquitectura de máquina de pila, por lo que esta observación debe ser razonablemente válida.

Ahora bien, si se hace referencia a una variable en un cierre (es decir, mediante una función definida localmente que se puede llamar más adelante), a la función "interior" se le asigna una "cadena de alcance" que comienza con el más interno alcance cual es la función en sí misma El siguiente ámbito es la función externa (que contiene la variable local accedida). El intérprete (o compilador) creará un "cierre", esencialmente una pieza de memoria asignada en el montón (no la pila) que contiene esas variables en el alcance.

Por lo tanto, si se hace referencia a las variables locales en un cierre, ya no se asignan en la pila (lo que las hará desaparecer cuando la función regrese). Se asignan al igual que las variables normales de larga duración, y el "alcance" contiene un puntero a cada una de ellas. La "cadena de alcance" de la función interna contiene punteros a todos estos "ámbitos".

Algunos motores optimizan la cadena de alcance al omitir variables sombreadas (es decir, cubiertas por una variable local en un ámbito interno), por lo que en su caso solo queda un BigObject, siempre que la variable "x" solo se acceda al ámbito interno , y no hay llamadas "eval" en los ámbitos externos. Algunos motores "aplanan" las cadenas de alcance (creo que V8 lo hace) para una resolución variable rápida, algo que se puede hacer solo si no hay llamadas "eval" intermedias (o no hay llamadas a funciones que pueden hacer una evaluación implícita, por ej. setTimeout).

Invitaría a algunos gurús de motor de JavaScript a proporcionar más detalles jugosos que yo.


10
2018-03-20 12:54