32 votes

Comment les fermetures et les étendues sont-elles représentées lors de l'exécution en JavaScript

C'est surtout une curiosité question. Considérons les fonctions suivantes

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

Dans tous les cas, après l'exécution de la fonction, il est (je pense) pas moyen de parvenir à x et donc la BigObject peut être nettoyée, aussi longtemps que x est la dernière référence. Un simple d'esprit interprète capturer l'ensemble de la chaîne de domaine chaque fois qu'une fonction d'expression est évaluée. (Pour une chose, vous avez besoin pour ce faire d'effectuer des appels à la fonction eval travail -- exemple ci-dessous). Une intelligente mise en œuvre peut éviter cela en f0 et f1. Encore plus intelligente de la mise en œuvre permettrait d' y être conservé, mais pas de x, comme c'est nécessaire pour la touche f2 pour être efficace.

Ma question est comment faire le moderne moteurs JavaScript (JaegerMonkey, V8, etc.) faire face à ces situations?

Enfin, voici un exemple qui montre que les variables peuvent avoir besoin d'être conservées, même si elles ne sont jamais mentionnées dans la fonction imbriquée.

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

Cependant, il existe des restrictions qui empêchent de se faufiler dans un appel à eval d'une façon qui pourrait être ignorés par le compilateur.

36voto

gsnedders Points 2693

Ce n'est pas vrai qu'il y a des restrictions qui vous empêchent d'appeler eval qui ne seraient pas détectés par analyse statique: c'est juste que de telles références à d'eval exécuter dans la portée globale. Notez que ce est un changement dans l'ES5 de ES3 où directe et indirecte, les références à eval à la fois couru dans la portée locale, et en tant que tel, je ne suis pas sûr que rien ne fait la moindre optimisation basée sur ce fait.

Une façon évidente de tester cela est de faire BigObject vraiment être un gros objet, et la force d'une table, après l'exécution de f0–f2. (Parce que, eh bien, autant que je pense connaître la réponse, le test est toujours mieux!)

Alors...

Le 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
}

J'ai ajouté des tests pour eval/Fonction, semblant ce sont aussi des cas intéressants. La différence entre f5/f6 est intéressant, parce que f5 est vraiment juste identique à f3, compte tenu de ce qu'est réellement une fonction identique à la fermeture; f6 contente de quelque chose qui, une fois évaluées donne, et que l'évaluation n'a pas encore été évalué, le compilateur ne peut pas savoir qu'il n'y est fait aucune référence à x en son sein.

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 semble GC "x" sur tout, sauf sur f3, f5 et f6.

Il semble pour autant que possible (c'est à dire, lorsque c'est possible, y, ainsi que x) sauf s'il est direct eval appel dans le champ d'application de la chaîne d'une fonction qui existe toujours. (Même si cette fonction de l'objet lui-même a été GC avais et n'existe plus, comme c'est le cas en f5, ce qui signifie théoriquement qu'il pourrait 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 semble GC x sur tout, en dehors de f3, f5 et f6. C'est identique à SpiderMonkey, voir l'analyse ci-dessus. (À noter toutefois que les chiffres ne sont pas assez détaillées pour dire si y est GC avais quand x n'est pas, j'ai pas pris la peine d'enquêter sur ce point.)

Carakan

Je ne vais pas la peine de l'exécution de cette à nouveau, mais inutile de dire que le comportement est identique à SpiderMonkey et V8. Plus difficile à tester sans JS shell, mais c'est faisable avec le temps.

JSC (Nitro) et de Chakra

La construction du CDC est une douleur sur Linux, et le Chakra ne fonctionne pas sur Linux. Je crois que CDC a le même comportement pour les moteurs ci-dessus, et je serais surpris si Chakra n'était pas trop. (Faire quelque chose de mieux devient vite très complexe, en faisant quelque chose de pire, eh bien, vous seriez presque jamais faire de GC et ont de graves problèmes de mémoire...)

10voto

Stephen Chung Points 9467

Dans des situations normales, les variables locales à une fonction sont allouées sur la pile -- et ils "automatiquement" aller loin quand la fonction retourne. Je crois que beaucoup de populaires moteurs JavaScript exécuter l'interpréteur (ou le compilateur JIT) sur une pile de l'architecture de la machine de sorte à ce obversation devrait être raisonnablement valable.

Maintenant, si une variable est mentionnée dans un dispositif de fermeture (c'est à dire par une fonction définie localement, qui peut être appelé plus tard), l ' "intérieur" de la fonction est assignée à une "chaîne d'étendue" qui commence avec le plus intérieur de la portée qui est la fonction elle-même. La prochaine portée est alors que l'extérieur de la fonction (qui contient la variable locale d'accès). L'interprète (ou le compilateur) va créer une "fermeture", qui est essentiellement un morceau de la mémoire allouée sur le tas (pas de pile) qui contient les variables dans la portée.

Par conséquent, si les variables locales sont visés par une fermeture, ils ne sont plus allouées sur la pile (qui va les faire disparaître quand la fonction retourne). Ils sont affectés, tout comme normale, longue durée de vie des variables, et la "portée" contient un pointeur vers chacun d'eux. "L'étendue de la chaîne" de l'intérieur de la fonction contient des pointeurs vers tous ces "étendues".

Certains moteurs d'optimiser la portée de la chaîne par l'omission de variables qui sont à l'ombre (c'est à dire couvert par une variable locale à l'intérieur de la portée), donc dans votre cas, un seul BigObject reste, tant que la variable "x" n'est accessible qu'à l'intérieur de l'étendue, et il n'existe pas de "eval" les appels à l'extérieur étendues. Certains moteurs "aplatir" la portée des chaînes (je pense V8 n'est qu') pour rapide à résolution variable -- quelque chose qui peut être fait seulement si il n'existe pas de "eval" les appels entre les deux (ou pas d'appels à des fonctions qui peuvent faire l'implicite eval, par exemple setTimeout).

J'aimerais inviter certains moteur JavaScript gourou de fournir plus de détails croustillants que je peux.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X