走路应该留下脚印

人生的代码我来书写

导航

[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属性就是为此准备的)。

posted on 2009-03-17 13:33  wingfay  阅读(244)  评论(0编辑  收藏  举报