• [ZT]JScript下Array对象的性能问题




    原发表于:hax的技术部落格

    今天看了微软JScript官方blog上去年的两篇文章:
    http://blogs.msdn.com/jscript/archive/2008/03/25/performance-optimization-of-arrays-part-i.aspx
    http://blogs.msdn.com/jscript/archive/2008/04/08/performance-optimization-of-arrays-part-ii.aspx
    讲的是IE8对Array性能的改进,其中也解释了过去JScript对Array的内部实现及导致的性能问题。

    狗狗了一下,似乎没有中文文章介绍过,所以我就写一篇来稍作介绍——不过并非翻译——微软的人总不好意思自己骂自己——而我素来要痛打落水狗的。

    首先,目前JScript中数组操作是很慢滴,有多慢呢?我们引用一下JScript团队第一篇blog的数据:

       提示:您可以先修改部分代码再运行

    此段代码,IE7用了7秒,而IE8仅仅用时18毫秒。

    如果你看了第一篇blog中所解释的原因,请注意

    ——其实它在忽悠你,真正的原因在第二篇里。


    我这里有有个更加简单的测试代码:

       提示:您可以先修改部分代码再运行

    你可以改变SIZE和count的值来观察。

    基本上,pop()方法或者任何导致length减小的操作,都是非常耗费的——大体上与数组实际包含元素个数成正比。

    这才是真正的性能杀手。


    而且这一操作所要遍历的元素甚至包括非正整数索引(而只有索引为uint32才会被认为是数组元素):

       提示:您可以先修改部分代码再运行

    可以发现结果也是基本与元素个数成正比。但是比前面要快一些。原因后面解释。


    之所以有这样的结果,是因为length减小时,引擎不仅仅是改变了length的值,而且要删除所有索引大于等于新的length的元素。

    本来这件事情可以这样做(我相信大多数程序员的第一感觉也就是这样做):

    Code:

    function get_length() { return this._length }
    function 
    set_length(len) {
      if (
    len || len 0xffffffff) throw RangeError()
      if (
    len this._length) {
        
    this._length len; return
      }
      while (
    len this._length) {
        
    delete this[--this._length]
      }



    如果确实这样做的话,性能绝不会如此糟糕(不信你可以写个自己的MyArray来试验一下——用JS写出来的居然比built-in的C实现还要快)。

    但是,JScript团队的程序员,据说认为JS的Array其实是稀疏数组,或者说就是一个hash——理论上也确实如此,所以采用了大概是这样的算法(以JS来示意):

    Code:

    function get_length() { return this._length }
    function 
    set_length(len) {
      if (
    len || len 0xffffffff) throw RangeError()
      if (
    len this._length) {
        for (var 
    k in this) {
          var 
    toUint32(k)
          if (
    >= lendelete(this[k])
        }
      }
      
    this._length len



    其中toUint32(s)将一个32位无符号整数的十进制字符串表达转为数字,如果转换失败则返回-1。【注意,此toUint32与parseInt不同,传入'0123'、'123.00'、'+123'、'1e4'等都会转换失败。】

    这段代码遍历所有的key(因为Array本质上是把数字索引转换为字符串索引来保存的),如果这个key可以被转回无符号整数(也就是数字索引),则看看它是否大于指定的长度,大于的话就删掉。

    好了,现在我们就不难理解为什么性能会与元素个数成正比了。假如一个50000个元素的数组,pop()了一下之后,居然也要遍历50000个元素!如果是在一个循环中不断pop()可想而知会有多慢。

    那么为什么我们第二个例子会快一些呢?
    这是因为我们的字符串索引是字母"i"开头的,而toUint32的实现,应该会一个字符一个字符的解析,当第一个字母是i时已然可知转换失败,直接返回了。以下是也以JS示意:

    Code:

    function toUint32(s) {

      if (
    == '') return -1
      
    if (s.charCodeAt(0) == 48 && s.length 1) return -1

      
    var 0d
      
    for (var 0s.lengthi++) {
        
    *= 10
        d 
    s.charCodeAt(i) - 48
        
    if (|| 9) return -1
        n 
    += d
        
    if (>= 0xffffffff) return -1
      
    }
      return 
    n



    你也可以把前面第二段代码中的:
       arr['i' + i] = i;
    换为
       arr[i + '00000000x'] = i

    结果就会发现耗费时间更长了,因为toUint32必须解析到最后一个字符才知道转换失败。

    【注:这里可以优化,因为uint32十进制最长是10个字符,所以可以先判断string的长度,超过10直接失败——问题是从测试结果来看,JScript实际的实现并没有做此优化】


    那么JScript的开发者为什么要采用这样一个算法呢?

    其实,如果从稀疏数组的角度看,这个算法是可行的,而且在某些条件下性能很好,比如对于以下:

    var a = new Array(100000)
    a.length = 10

    在这次对length的改变中,上述算法飞快的完成,因为这个数组没并有什么实际元素可遍历!

    所以JScript的开发者或许是特意做了这样的优化算法!!


    然而讽刺的是,这本质上是一个极其愚蠢的优化!!


    通常我们在用Array的时候,很少拿它当稀疏数组用!因为如果我们确实要用一个稀疏数组,我们完全可以直接用普通对象(即hash)。

    就算我们经常使用稀疏数组——诚然,在一个稀疏数组时,前面所描述的最显白的算法,可能并不高效,但是稍加分析就可以看出,那样的算法要出现性能问题,只能是如下形式的代码:

    Code:

    for(var i=0;i<arrays.length;i++) {
      
    arrays[i].length 0



    其中每个arrays[i]都是稀疏数组。
    这样的代码我是没有见过。

    对于单个稀疏数组来说,要出现性能问题,除非你这样:

    Code:

    for(var i=0;i<count;i++) {
      array = new Array(
    50000)
      ...
      array.
    length 0



    然而几乎没有人会写这样的代码
    ——而且 array.length=0 这句造成性能问题的语句根本是多余的。

    归根到底,既然是一个稀疏数组,则通常不会进行遍历,更没有理由执行 array.length = 0 之类的语句,如果要抛弃一个数组,直接 array = null 即可。


    相反,普通(非稀疏)的数组,就是用来遍历的。因此很容易写出这样的代码:

    Code:

    while (array.length 0) {
      var 
    = array.pop()
      
    // do sth for e



    或者

    Code:

    while (array.length 0) {
      var 
    = array[array.length 1]
      
    // do sth for e
      
    array.length--



    所以我所能得出的结论是,JScript的Array.length的算法实现实在非常愚蠢。幼稚程度堪比我们诟病的白痴的垃圾回收策略

    当然,比起垃圾到极点的垃圾回收算法给我们造成的麻烦,这个length的问题还是可以有workaround的,不必干等IE8那样复杂到狂出bug(见第一篇blog下面的comments)的设计。比如IE8 beta1的platform performance improvements白皮书里就提到,有人自己造轮子:

    Code:

    //user defined array type called 'prototypeArray' 
    var prototypeArray = function() { 
      
    this._array = []; 
      
    this.length 0
      
    this.start 0
      
    this.end 0
    }
    //push method for user defined array type 'prototypeArray' prototypeArray.prototype.push = function() { 
      
    var arguments.length
      for (var 
    i=0;i<l;i++){ 
        
    this.length++; 
        
    this._array[this.end++] = arguments[i]; 
      } 

    //pop method for user defined array type 'prototypeArray' prototypeArray.prototype.pop = function() { 
      
    var obj null
      if (
    this.length>0){ 
        
    this.length--; 
        
    obj this._array[--this.end]; 
        
    delete this._array[this.end];
      } 
      return 
    obj

    //creating an object of user defined array type 'prototypeArray' var myArray = new prototypeArray(); 
    //accessing push and pop methods of user defined array type 'prototypeArray' 
    myArray.push("Test String"
    myArray.pop(); 


    一个用js代码自造的轮子,居然比c写的built-in函数跑的还要快,真是让人汗颜。

    另外一个更简便的方式,就是记住这个简单的最佳实践:

    少去改length!特别不要在循环中做pop()或length--之类的操作。如果要减,等到循环结束,一次性减到底就好了!

    BTW,我们也要注意shift()、unshift()、splice()之类的操作,因为这些操作可能会改变大量数组元素的索引——按照稀疏数组的实现,显然会比单单改length更慢上许多!如果你真的需要shift()、unshift(),那还是自制一个类似上面的轮子吧(提示:上面代码的start属性就是为此准备的)。
  • 相关阅读:
    深度优先遍历
    从零开始学习Sencha Touch MVC应用之三
    从零开始学习Sencha Touch MVC应用之二
    开始 Sencha Touch 2 之旅之一
    开始 Sencha Touch 2 之旅之三
    从零开始学习Sencha Touch MVC应用之一
    开始 Sencha Touch 2 之旅之二
    开始 Sencha Touch 2 之旅之四
    phonegap一些事件
    ubuntu 10.10安装nginx+php的过程
  • 原文地址:https://www.cnblogs.com/wingfay/p/1414232.html
Copyright © 2020-2023  润新知