JavaScript全局优化带来的负面效果……

读者在China-pub中指出《JavaScript语言精髓与编程实践》的一个示例在FF上不正常。具体来说,就是第“4.6.6 语句或语句块中的闭包问题”中的第二、三、四三个示例。这个问题我反复测试过,只出现FireFox(SpiderMonkey JavaScript)中。我当时意识到是SpiderMonkey特殊的全局变量引用机制导致的,但因为没有切实地、在源代码级别观察过,因此我没有立即回答这个问题。

这些天有点时间了,把这个话题拿出来重新讨论。代码是这样子的:

var obj = new Object();
var events = {m1: "clicked", m2: "changed"};

for (e in events) {
   obj[e] = function(){
      alert(events[e]);
   };
}
// 显示false, 表明是不同的函数实例
alert( obj.m1 === obj.m2 );

这个示例在其它的js引擎中,总是显示false。这与我们在“4.6.6”章中的分析是一致的。但是在Firefox中,就会显示true。按照我们的分析,这里的匿名函数直接量应该根据循环次数创建多个实例。然而在Firefox中,它只创建了一次。

出了什么问题了?

回到SpiderMonkey JavaScript引擎的源代码中,我们得知道,执行引擎并不直接执行代码,而是先将代码文本分析(parser)成代码树,再按规则执行代码树中的指令结点(op)。指令在JS中总是确定的,例如源代码中是FOR,那么总是JSOP_FOR*这个操作码。但是,指令所使用的参数却不是确定的,而是通过参数名绑定到一个具体的操作数。参数名是字符串,也就是标识符;操作数则位于该代码上下文所使用的栈中。这个栈中的操作数使用slot位置来表示。因此,这个关键的函数就是BindNameToSlot()。
这个函数具体分析执行过程中使用的一个标识符应该绑定到那个闭包(上下文)中的哪个slot位置。这受非常多的因素影响,例如我们下面这段代码(出自“5.2.3 奇特的、甚至是负面的影响”):

var i = 100;
function myFunc(ctx) {
  alert("value is: " + i);
  eval(ctx);
  alert("value is: " + i);
}

在myFunc()中,由于有一行动态执行eval()的代码,因此第一行和第二行中的变量“i”,就可能被绑定在不同闭包的solt上。例如我们执行:

myFunc("var i = 10;");

那么代码的执行效果将是显示:

value is: 100
value is: 10

这也就意味着变量名必须在每行代码执行时都重新绑定——而这,也正是JavaScript效率偏低的原因。

这些过程,对所有的JS引擎来说是统一的,这是语言规范所决定的。然而,SpiderMonkey大概是觉得这想在全局范围上的效率太差……准确地说,如果一个变量标识符总是要绑定到全局变量,而每次绑定都要回溯很多层次的闭包链,所以从效率上来讲,每次都这样做就会很不经济。

于是SpiderMonkey做了一个优化。这个优化在SpiderMonkey中叫“optimizeGlobal”。其规则是:

  • 1. 在语法分析期(jsparse.c)对标识符进行计数
                /* Measure optimizable global variable uses. */
                ATOM_LIST_SEARCH(ale, &tc->decls, pn->pn_atom);
                if (……
                    js_IsGlobalReference(tc, pn->pn_atom, &loopy)) {
                    tc->globalUses++;
                    if (loopy)
                        tc->loopyGlobalUses++;
                }

也就是说,当一个标识符是引用全局的,则globalUses加1,而当它处于循环体内(loopy)时,loopyGlobalUses也同时加1。

  • 2. 在标识符绑定时(BindNameToSlot)确定是否优化,即启用optimizeGlobal
function BindNameToSlot(...) {
...
        optimizeGlobals = (tc->globalUses >= 100 ||
                           (tc->loopyGlobalUses &&
                            tc->loopyGlobalUses >= tc->globalUses / 2));
...
}

也就是说,在一个代码块中有超过100个全局变量名的引用;或在循环中引用全局变量的数量,超过了该上下文中所引用全局变量数量的1/2。则开启optimizeGlobal……

  • 3. 当启用optimizeGlobal时的一些优化效果

这个就不讲了……极复杂的一套规则,其中就包括本文前面我们提到的这个问题。

OK。到这里我们告一段落。基本上来说,从这个例子的实际效果来讲,这是一个失败的优化。或者说SpiderMonkey中存在这样的BUG。在书中“4.6.6 语句或语句块中的闭包问题”中的第二、三、四三个示例,仅在SpiderMonkey中存在异常,就证明了这一点。另一方面,我们可以将示例代码稍作一下修改,改变它的上下文环境,效果就大为不同了。例如:

var obj = new Object();
var events = {m1: "clicked", m2: "changed"};

with ({}) { // <-- 打开一个对象闭包  
  for (e in events) {  
   obj[e] = function(){  
    alert(events[e]);  
   }  
  }  
}  

// 显示false, 表明是不同的函数实例  
alert( obj.m1 = obj.m2 );