微调性能时,多次调用JavaScript方法的最佳方式是什么?

问题描述:

我一直在研究JavaScript的性能。我了解到,当访问不止一次时,通常最好将闭包变量和类成员复制到本地范围以加快速度。例如:微调性能时,多次调用JavaScript方法的最佳方式是什么?

var i = 100; 
var doSomething = function() { 
    var localI = i; 
    // do something with localI a bunch of times 

    var obj = { 
     a: 100 
    }; 
    var objA = obj.a; 
    // do something with objA a bunch of times 
}; 

我明白这一点;它为解释者添加了一个按名称查找价值的快捷方式。处理方法时,这个概念变得很不清楚。起初,我认为它会以同样的方式工作。例如:

var obj = { 
    fn: function() { 
     // Do something 
     return this.value; 
    }, 
    value: 100 
}; 
var objFn = obj.fn 
objFn(); 
// call objFn a bunch of times 

事实上,这根本不起作用。访问像这样的方法将其从其范围中移除。当它到达this.value行时,这指的是窗口对象,this.value可能是未定义的。而不是直接调用objFn并丢失作用域,我可以通过objFn.call(obj)将它的作用域传回给它,但是这样做会比原来的obj.fn()更好或更差吗?

我决定写一个脚本来测试这个,我得到了非常混乱的结果。该脚本对多次循环上述函数调用的多次测试进行迭代。每次测试的平均时间输出到身体。

一个对象是用很多简单的方法创建的。额外的方法来确定解释者是否必须更加努力地找到一个特定的方法。

测试1简单地调用this.a();
测试2创建一个局部变量a = this.a然后调用a.call(this);
测试3使用YUI的绑定函数创建一个局部变量来保留范围。我评论了这一点。由YUI创建的额外函数调用使这种方式变得更慢。

测试4,5和6是1,2,3的副本,除了使用z而不是a。

YUI的后期功能用于防止失控脚本错误。时间在实际的测试方法中完成,所以setTimeouts不应该影响结果。每个函数总共被称为10000000次。 (如果要运行测试,可以轻松配置)。

这是我用来测试的整个XHTML文档。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" dir="ltr"> 
    <head> 
     <script type="text/javascript" src="http://yui.yahooapis.com/combo?3.1.2/build/yui/yui-min.js"></script> 
     <script> 
      YUI().use('node', function (Y) { 
       var o = { 
        value: '', 
        a: function() { 
         this.value += 'a'; 
        }, 
        b: function() { 
         this.value += 'b'; 
        }, 
        c: function() { 
         this.value += 'c'; 
        }, 
        d: function() { 
         this.value += 'd'; 
        }, 
        e: function() { 
         this.value += 'e'; 
        }, 
        f: function() { 
         this.value += 'f'; 
        }, 
        g: function() { 
         this.value += 'g'; 
        }, 
        h: function() { 
         this.value += 'h'; 
        }, 
        i: function() { 
         this.value += 'i'; 
        }, 
        j: function() { 
         this.value += 'j'; 
        }, 
        k: function() { 
         this.value += 'k'; 
        }, 
        l: function() { 
         this.value += 'l'; 
        }, 
        m: function() { 
         this.value += 'm'; 
        }, 
        n: function() { 
         this.value += 'n'; 
        }, 
        o: function() { 
         this.value += 'o'; 
        }, 
        p: function() { 
         this.value += 'p'; 
        }, 
        q: function() { 
         this.value += 'q'; 
        }, 
        r: function() { 
         this.value += 'r'; 
        }, 
        s: function() { 
         this.value += 's'; 
        }, 
        t: function() { 
         this.value += 't'; 
        }, 
        u: function() { 
         this.value += 'u'; 
        }, 
        v: function() { 
         this.value += 'v'; 
        }, 
        w: function() { 
         this.value += 'w'; 
        }, 
        x: function() { 
         this.value += 'x'; 
        }, 
        y: function() { 
         this.value += 'y'; 
        }, 
        z: function() { 
         this.value += 'z'; 
        }, 
        reset: function() { 
         this.value = ''; 
        }, 
        test1: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test2: function (length) { 
         var a = this.a, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test3: function (length) { 
         var a = Y.bind(this.a, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test4: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.z(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test5: function (length) { 
         var z = this.z, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test6: function (length) { 
         var z = Y.bind(this.z, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z(); 
         } 
         return new Date().getTime() - time; 
        } 
       }, 
       iterations = 100, iteration = iterations, length = 100000, 
       t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, body = Y.one('body'); 

       body.set('innerHTML', '<span>Running ' + iterations + ' Iterations&hellip;</span>'); 
       while ((iteration -= 1)) { 
        Y.later(1, null, function (iteration) { 
         Y.later(1, null, function() { 
          o.reset(); 
          t1 += o.test1(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t2 += o.test2(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t3 += o.test3(length); 
         });*/ 
         Y.later(1, null, function() { 
          o.reset(); 
          t4 += o.test4(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t5 += o.test5(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t6 += o.test6(length); 
         });*/ 
         if (iteration === 1) { 
          Y.later(10, null, function() { 
           t1 /= iterations; 
           t2 /= iterations; 
           //t3 /= iterations; 
           t4 /= iterations; 
           t5 /= iterations; 
           //t6 /= iterations; 

           //body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 3: a();</dt><dd>' + t3 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd><dt>Test 6: z();</dt><dd>' + t6 + '</dd></dl>'); 
           body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd></dl>'); 
          }); 
         } 
        }, iteration); 
       } 
      }); 
     </script> 
    </head> 
    <body> 
    </body> 
</html> 

我已经在三个不同的浏览器中使用Windows 7运行此操作。这些结果以毫秒为单位。

火狐3.6.8

Test 1: this.a(); 
    9.23 
Test 2: a.call(this); 
    9.67 
Test 4: this.z(); 
    9.2 
Test 5: z.call(this); 
    9.61 

的Chrome 7.0.503.0

Test 1: this.a(); 
    5.25 
Test 2: a.call(this); 
    4.66 
Test 4: this.z(); 
    3.71 
Test 5: z.call(this); 
    4.15 

的Internet Explorer 8

Test 1: this.a(); 
    168.2 
Test 2: a.call(this); 
    197.94 
Test 4: this.z(); 
    169.6 
Test 5: z.call(this); 
    199.02 

Firefox和Internet Explorer所产生如何我预期的结果。测试1和测试4相对接近,测试2和测试5相对接近,测试2和测试5比测试1和测试4需要更长的时间,因为需要额外的函数调用来处理。 Chrome我一点也不懂,但速度要快得多,也许对亚毫秒性能的调整是不必要的。

有没有人对结果有很好的解释?什么是多次调用JavaScript方法的最佳方式?

+2

可以添加此链接在你的问题,以便其他用户可以运行测试本身? - http://jsfiddle.net/Lbbx5/ – Anurag 2010-08-28 21:34:42

+1

相关阅读:[JavaScript Widgets Without“this”](http://michaux.ca/articles/javascript-widgets-without-this) – 2010-08-28 21:41:38

+1

仅供参考,使用Chromium(Linux)在我的(相对较慢的)系统上,经过几次测试后,结果与Firefox中的结果大致相同(仅更快):组合1/4和2/5或多或少接近,2和5比1和4. – 2010-08-28 21:50:24

那么,只要你的网站有IE8用户作为访问者,这是非常不相干的。使用1或3(用户不会看到区别)。

“为什么”问题可能没有一个好的答案。当涉及到优化时,这些脚本引擎可能将重点放在优化他们在现实生活中发生的场景,在这些场景中,优化可以被证明是正确的,并且它在哪里产生了变化,并且使其失效最少量的测试。

+0

在任何必须迭代数百万次的实际应用程序中,它可能会调用比value + ='a'更复杂的函数,并且每次迭代可能会调用多个函数。在这个测试中,Internet Explorer上大约三十毫秒的优化可能是无关紧要的,但是当应用程序需要更多时间来实际执行某些操作并且30毫秒乘以几十个函数调用时,它会加起来非常明显的延迟。 – Killthesand 2010-08-29 15:01:36

只是理论化,所以借此与一粒盐...

Chrome的JavaScript引擎V8,使用称为隐类的优化技术。基本上它构造了静态对象,这些静态对象会影响动态JavaScript对象,其中每个属性/方法都映射到一个固定的内存地址,该地址可以被immedital引用而不需要昂贵的查表操作。每当Javascript对象添加/删除属性时,都会创建一个新的隐藏类。

我对Chrome测试结果的理论是,在免费的局部变量中引用函数可以打破隐藏的类关系。虽然引用局部变量可能也不需要查找表,但现在必须执行额外的步骤来重新分配“this”变量。对于一个隐藏类的方法,'this'是一个固定值,所以可以在没有这个步骤的情况下调用它。

再次只是理论。可能值得进行一次测试,以测试Chrome中局部变量引用与object.member引用之间的差异,以查看后者对性能的影响是否显着低于其他浏览器,可能是因为隐藏类。

+0

V8引擎中的隐藏类的概念使这更加陌生。为什么调用this.a()比调用this.z()更加昂贵?调用a.call(this)比调用this.a()要便宜,但调用z.call(this)比调用this.z()更昂贵。他们是否反向遍历成员而不是解引用内存指针?我想知道这些结果是否可靠。我们的规模为3-5毫秒;我大概可以在屏幕上移动鼠标并扭曲这些结果。我会在chrome上尝试大量的迭代,看看它是如何工作的。 – Killthesand 2010-08-29 15:12:33

+0

我在Chrome上运行了另一个测试,1000次迭代,每次有100万次调用。运行需要相当长的时间,但提供了更加一致的结果。 测试1:this.a(); 168.475 测试2:a.call(this); 172.069 测试4:this.z(); 168.936 测试5:z.call(this); 173.012 – Killthesand 2010-08-29 17:07:40

就像一般的观点一样,如果你不知道知道它会负责很多时间。 (所谓“多”我的意思是显著百分比)。

Here's an easy way to find out which code is responsible for much time.