`
hax
  • 浏览: 951565 次
  • 性别: 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内置对象Array中元素的删除方法

    JScript内置对象Array中元素的删除方法

    jscript语言参考手册chm

    JScript 对象 固有对象 创建自己的对象 JScript 保留字 -------------------------------------------------------------------------------- © 2000 Microsoft Corporation 版权所有。保

    Microsoft JScript 运行时错误: 缺少对象

    供提问使用,Microsoft JScript 运行时错误: 缺少对象 Microsoft JScript 运行时错误: 缺少对象

    JScript.chm JScript帮助

    JScript.rar JScript.chm JScript帮助 JScript.rar JScript.chm JScript帮助 JScript.rar JScript.chm JScript帮助 JScript.rar JScript.chm JScript帮助

    微软《JScript 语言参考》中文

    微软《JScript 语言参考》中文 目录: JScript用户指南 JScript错误 JScript函数 ...JScript对象 JScript运算符 JScript属性 JScript语句 FileSystemObject用户指南 Scripting运行时库参考 正则表达式简介

    JScript中文参考手册

    此外,在大多数情况下,JScript 将根据需要自动进行转换。例如,如果将一个数值添加到由文本组成的某项(一个字符串),该数值将被转换为文本。 本用户指南的其余部分是 JScript 特性概述。有关该语言实现的全部...

    JScript中文帮助文档.zip

    如果只需要查看某个主题(例如对象),则有对该主题进行详细说明的章节可供查阅。 如何操作呢?单击左边任意一个标题,即可显示该标题所包含的项目列表。再从该列表中选择要查看的主题。在打开所选主题后,就可以...

    编程脚本参考(JScript,VBScript等)

    包括JScript,VBScript,Windows脚本部件,Windows脚本宿主等

    JScript 语言参考

    不过,如果只想查看某个类别,例如对象,每个语言类别都有其自己更简洁的列表。 &lt;br/&gt;如何使用?单击左边的一个标题,将显示该类别中包含的项的列表。从该列表中选择想查看的主题。在打开该主题后,您可以方便地...

    JScript 中文语言参考

    如果只需要查看某个主题(例如对象),则有对该主题进行详细说明的章节可供查阅。 如何操作呢?单击左边任意一个标题,即可显示该标题所包含的项目列表。再从该列表中选择要查看的主题。在打开所选主题后,就可以...

    JScript 8.0 中文手册

    虽然大多数构造都与 JScript 的以前版本相同,但 JScript 8.0 引入了强大的新构造,这些构造类似于其他基于类的面向对象语言中的构造。 如果您是编程新手,本节中的资料可作为编写代码的基本构造块的介绍。当了解...

    WScript 对象

    脚本语言(VBScript and JScript)怎样使用WScript 对象

    面向对象的Jscript分析

    面向对象的Jscript分析 挺不错的

    Jscript及Html中文API

    Jscript及Html中文API

    JScript中文帮助文档

    JScript中文帮助文档JScript中文帮助.CHM

    jscript 手册

    jscript 手册

    Jscript8.0中文手册chm版

    它是一种真正面向对象的语言,不过仍保留其“脚本”特色。Jscript 8.0 保持与 Jscript 以前版本的完全向后兼容性,同时包含了强大的新功能并提供了对公共语言运行库和 .NET Framework 的访问。 以下...

    JScript例子.chm

    JScript例子.chm JScript例子.chm JScript例子.chm JScript例子.chm JScript例子.chm

    jscript.net程序开发

    jscript.net程序开发jscript.net程序开发jscript.net程序开发

Global site tag (gtag.js) - Google Analytics