`
hax
  • 浏览: 965045 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

JScript下Array对象的性能问题

    博客分类:
  • JS
阅读更多
今天看了微软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的数据:

var arrObj = new Array();
var count = 10000;
var before, after;
for(var i = 0; i < count; i ++)

{
   arrObj.push(i);

}
before = new Date();
for(var i = 0; i < count; i ++)

{
    arrObj.pop();

}
after = new Date();
alert(after- before);


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

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

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


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

var SIZE = 10000, count = 10000;
var arr = new Array(SIZE + count);
for(var i = 0; i < SIZE; i++) {
   arr[i] = i;  // 注意后文将替换本语句来观察效果
}

var start = new Date();
for(var i = 0; i < count; i++) {
    arr.length--
}
var end = new Date();
alert(end - start);


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

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

这才是真正的性能杀手。


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

var SIZE = 10000, count = 10000;
var arr = new Array(SIZE + count);
for(var i = 0; i < SIZE; i++) {
   arr['i' + i] = i;
}

var start = new Date();
for(var i = 0; i < count; i++) {
    arr.length--
}
var end = new Date();
alert(end - start);


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


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

本来这件事情可以这样做(我相信大多数程序员的第一感觉也就是这样做):
function get_length() { return this._length }
function set_length(len) {
  if (len < 0 || 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来示意):

function get_length() { return this._length }
function set_length(len) {
  if (len < 0 || len > 0xffffffff) throw RangeError()
  if (len < this._length) {
    for (var k in this) {
      var i = toUint32(k)
      if (i >= len) delete(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示意:

function toUint32(s) {

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

  var n = 0, d
  for (var i = 0; i < s.length; i++) {
    n *= 10
    d = s.charCodeAt(i) - 48
    if (d < 0 || d > 9) return -1
    n += d
    if (n >= 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)。

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

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

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

对于单个稀疏数组来说,要出现性能问题,除非你这样:
for(var i=0;i<count;i++) {
  array = new Array(50000)
  ...
  array.length = 0
}

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

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


相反,普通(非稀疏)的数组,就是用来遍历的。因此很容易写出这样的代码:
while (array.length > 0) {
  var e = array.pop()
  // do sth for e
}

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


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

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

//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 l = 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属性就是为此准备的)。
6
0
分享到:
评论
2 楼 xuanye 2009-02-18  
长见识了哦
1 楼 whiletrue 2009-02-16  
很好的贴,谢谢lz分享,学习了

相关推荐

    JScript中文参考手册

    JScript提供了一系列内置对象,如Date对象用于处理日期和时间,Math对象包含数学常量和函数,Array对象用于处理数组,String对象则处理字符串操作。此外,还有全局函数,如eval()用于执行字符串作为JavaScript代码,...

    JScript中文参考资料集合

    对象则用于封装数据和方法,JScript支持内置对象(如Math、Date、Array等)和自定义对象。理解函数的声明、调用以及对象的创建和使用是进阶学习的关键。 3. **事件处理** 在Web开发中,JScript通过监听和处理用户...

    JScript中文帮助文档

    2. **内置对象**:JScript提供了多个内置对象,如Math对象用于数学运算,Date对象处理日期和时间,Array对象处理数组,String对象处理字符串,Error对象用于异常处理。 3. **函数和方法**:例如,eval()函数可以...

    JScript详解对(希望对大家有用)

    **JScript详解** JScript,全称为Microsoft JScript,是由微软公司开发的一种脚本语言,主要应用于Web开发,与JavaScript...了解和掌握JScript的基本知识,对于理解早期Web技术以及处理遗留的IE相关问题具有重要意义。

    jscript 8.0中文手册

    1. **ECMAScript 5 支持**:JScript 8.0对ECMAScript 5规范有了更全面的支持,包括新增的函数、对象和语法特性,如`Array.prototype.forEach`、`Object.create`、`JSON.parse/.stringify`等。 2. **闭包与作用域**...

    JSCRIPT.NET教程配套CODE

    基本数据类型包括数字(Number)、字符串(String)、布尔值(Boolean)等,还有引用类型如对象(Object)和数组(Array)。 2. **函数和方法**:函数是可重复使用的代码块,可以接收参数并返回值。JScript.NET中的...

    JScript 语言参考手册 CHM格式

    2. **对象和原型**:JScript支持面向对象编程,包括内置对象(如Array、Date、Math等)、自定义对象以及原型链的概念,这有助于理解对象间的继承关系。 3. **事件处理**:JScript常用于网页交互,手册会详细介绍...

    JScript 8.0 语言参考手册

    - **内置对象**:包括全局对象、Math 对象、Date 对象、String 对象、Array 对象等,提供了丰富的数学计算、日期操作、字符串处理和数组管理功能。 - **函数对象**:函数在 JScript 8.0 中是第一类对象,可以作为...

    JScript官方中文手册

    2. **函数和对象**:讲解了如何定义和调用函数,以及内置对象如Array、Date、Math等的使用方法。 3. **JScript的特殊特性**:比如ActiveXObject,这是JScript特有的,用于在脚本中创建和操控COM组件,使得JScript...

    jscript从入门到精通

    此外,JScript还提供了丰富的内置对象,如Date、Math、Array等,用于处理日期、数学计算和数组操作。 函数是JScript中的核心组成部分,它们可以封装代码并重复使用。函数表达式和函数声明是两种定义函数的方式,...

    JScript网页设计300例

    2. **函数与对象**:掌握如何创建和使用函数,以及理解JavaScript的核心对象,如Date对象、Math对象和Array对象。 3. **DOM操作**:通过JScript与文档对象模型(DOM)交互,可以动态地改变网页内容,实现页面元素的...

    JScript 8.0 语言参考手册 chm

    JScript 8.0 包含了丰富的内置函数,如Math对象提供的数学函数(如sqrt、random),String对象的方法(如substring、indexOf、replace),以及Array对象的方法(如push、pop、slice)。此外,还有DOM操作函数,如...

    Jscript技术手册

    14. 性能优化:理解JScript的运行机制,避免全局变量、减少DOM操作和使用缓存等方法提升程序性能。 通过阅读和研究《JScript技术手册》,开发者可以系统地学习JScript的各个方面,熟练掌握其语法和特性,为ASP开发...

    jscript语言参考

    JScript也可以在服务器端运行,通过Node.js环境,可以创建高性能的网络应用和服务。 9. **与其他技术的交互** JScript可以与CSS(样式表)和HTML(结构语言)紧密结合,共同构建网页。同时,它也能通过JSON...

    css2.0中文手册+JScript中文手册

    2. **函数与对象**:如何定义和调用函数,以及内置对象(如Math、Date、Array等)的使用。 3. **DOM操作**:通过JScript操纵HTML文档对象模型,包括元素的选择、添加、删除和属性修改。 4. **事件处理**:绑定和...

    商业编程-源码-VBScript JScript解释器源代码.zip

    通过研究JScript源代码,我们可以深入了解引擎如何实现垃圾回收机制、函数作用域、原型链、异步编程模型如回调、Promise和async/await,以及如何处理各种内置对象如Array、Date和RegExp。JScript的源码对于理解V8或...

    JScript中文帮助

    4. **数组和集合**:Array对象的方法,如push、pop、shift、unshift、slice、splice等,以及Map、Set等ES6新增集合类型。 5. **字符串处理**:字符串的方法,如concat、indexOf、substring、replace、split等。 6....

    jscriptdeviationsfromes3

    该方法在 JScript 中的行为与 ES3 中相同,但在某些情况下可能进行了优化以提高性能。 ##### 18. 逻辑或运算符(Disjunction): §15.10.2.3 JScript 支持 ES3 中定义的逻辑或运算符,并可能引入了更高效的求值...

    在javascript将NodeList作为Array数组处理的方法

    处理NodeList时还需注意的是,每次页面DOM结构发生变化时,NodeList都会重新计算其元素,这可能会导致性能问题。因此,在处理大量DOM操作时,将NodeList转换成数组可以提高效率,因为我们只需要计算一次长度。 在...

Global site tag (gtag.js) - Google Analytics