给数组赋值的性能表现

5
在SO中提到,代码优化的第一步是分析JavaScript,建议使用Chrome和Firefox的分析器。但是这些工具以某种奇怪的方式告诉我们每个函数执行的时间,我对它们没有很好的理解。最有帮助的方式是分析器可以告诉我们每行代码执行的次数,如果可能的话,还可以告诉我们每行代码所花费的时间。这样就可以严格地看到瓶颈所在了。但在找到这样的工具之前,我们有两个选择:
1)制作自己的计算器,计算某个代码块或行的执行时间和执行次数 2)学习理解哪些方法慢,哪些方法不慢
对于选项2,jsperf.com非常有帮助。我尝试学习如何优化数组,并在JSPERF.COM上进行了速度测试。以下图片显示了5个主要浏览器的结果,并发现了一些以前不知道的瓶颈。

Speed test

主要发现:

1)无论使用哪种方法进行赋值,将值分配给数组比将值分配给普通变量的速度明显慢。

2)在性能关键循环之前预初始化和/或预填充数组可以显着提高速度。

3)与将值推入数组相比,数学三角函数并不那么慢!

以下是每个测试的说明:


1. 非数组 (100%):

变量是通过预定义的方式赋值的:

var non_array_0=0;
var non_array_1=0;
var non_array_2=0;
...

在定时区域中,它们被这样称呼:

non_array_0=0;
non_array_1=1;
non_array_2=2;
non_array_3=3;
non_array_4=4;
non_array_5=5;
non_array_6=6;
non_array_7=7;
non_array_8=8;
non_array_9=9;

上述是一个类数组变量,但似乎没有一种方式可以迭代或以与数组相反的其他方式引用这些变量。还是有方法吗?
这个测试中没有比将数字赋值给变量更快的操作。

2. non_array_non_pre (83.78%)

与测试1完全相同,但变量未预初始化或填充。速度为测试1速度的83.78%。在每个经过测试的浏览器中,预填充变量的速度比非预填充变量的速度更快。因此,请在任何速度关键循环之外初始化(并可能预填充)变量。

测试代码在此处:

var non_array_non_pre_0=0;
var non_array_non_pre_1=0;
var non_array_non_pre_2=0;
var non_array_non_pre_3=0;
var non_array_non_pre_4=0;
var non_array_non_pre_5=0;
var non_array_non_pre_6=0;
var non_array_non_pre_7=0;
var non_array_non_pre_8=0;
var non_array_non_pre_9=0;

3. pre_filled_array (19.96%):

数组是恶魔! 当我们放弃普通变量(test1和test2)并将数组引入到图像中时,速度显着降低。 尽管我们进行了所有优化(预初始化和预填充数组),然后直接分配值而不需要循环或推动,但速度降至19.96%。 这非常令人难过,我真的不明白为什么会发生这种情况。这是我在这次测试中的主要震惊之一。数组非常重要,我还没有找到一种可以不使用数组完成许多事情的方法。

测试数据在此处:

pre_filled_array[0]=0;
pre_filled_array[1]=1;
pre_filled_array[2]=2;
pre_filled_array[3]=3;
pre_filled_array[4]=4;
pre_filled_array[5]=5;
pre_filled_array[6]=6;
pre_filled_array[7]=7;
pre_filled_array[8]=8;
pre_filled_array[9]=9;

4. non_pre_filled_array (8.34%):

这个测试与第3个测试相同,但是数组成员没有预初始化或预填充,只有优化是事先初始化数组:var non_pre_filled_array=[];

与预初始化的第3个测试相比,速度降低了58.23%。因此,预初始化和/或预填充数组可以使速度提高一倍以上。

测试代码在此处:

non_pre_filled_array[0]=0;
non_pre_filled_array[1]=1;
non_pre_filled_array[2]=2;
non_pre_filled_array[3]=3;
non_pre_filled_array[4]=4;
non_pre_filled_array[5]=5;
non_pre_filled_array[6]=6;
non_pre_filled_array[7]=7;
non_pre_filled_array[8]=8;
non_pre_filled_array[9]=9;

5. pre_filled_array[i] (7.10%):

接下来是循环。这是测试中最快的循环方法。该数组已经被预初始化和预填充。

与内联版本(测试3)相比,速度下降了64.44%。这是如此显着的差异,以至于我会说,如果不需要循环,则不要循环。如果数组大小很小(不知道多小,必须单独测试),则使用内联赋值而不是循环更加明智。

由于速度下降非常巨大,而我们确实需要循环,因此明智的做法是找到更好的循环方法(例如while(i--))。

测试代码在此处:

for(var i=0;i<10;i++)
{
  pre_filled_array[i]=i;
}

6. non_pre_filled_array[i] (5.26%):

如果我们不预初始化和预填充数组,速度会下降25.96%。因此,在速度关键的循环之前进行预初始化和/或预填充是明智的选择。 代码如下:
for(var i=0;i<10;i++) 
{
  non_pre_filled_array[i]=i;
}

7. 数学计算 (1.17%):

每个测试都必须有一些参考点。数学函数被认为是慢的。该测试由十个“重型”数学计算组成,但现在另一件打击我的事情出现了。看看推送十个整数到循环数组中的8和9的速度。计算这10个数学函数比在循环中推送十个整数要快30%以上。因此,也许将一些数组推送转换为预初始化的非数组,并保留那些三角函数会更容易。当然,如果每帧有数百或数千个计算,则明智使用例如sqrt而不是sin/cos/tan,并使用出租车距离进行距离比较和用于角度比较的diamond angles (t-radians),但仍然主要瓶颈可能在其他地方:循环比内联慢,推动比使用预初始化/预填充的直接赋值慢,代码逻辑、绘图算法和DOM访问可能很慢。所有这些都无法在Javascript中进行优化(我们必须在屏幕上看到某些东西!),但我们可以做的所有简单且重要的事情,是明智的去做。这里的某个人在SO上说过,代码是给人类看的,可读性比快速代码更为重要,因为维护成本是最大的成本。这是经济学的观点,但我发现代码优化可以同时实现优雅和可读性以及性能。如果实现了5%的性能提升,并且代码更加简单明了,那么会给人一种良好的感觉!

代码在这里:

non_array_0=Math.sqrt(10435.4557);
non_array_1=Math.atan2(12345,24869);
non_array_2=Math.sin(35.345262356547);
non_array_3=Math.cos(232.43575432);
non_array_4=Math.tan(325);
non_array_5=Math.asin(3459.35498534536);
non_array_6=Math.acos(3452.35);
non_array_7=Math.atan(34.346);
non_array_8=Math.pow(234,222);
non_array_9=9374.34524/342734.255;

8. pre_filled_array.push(i) (0.8%):

Push是有害的!将push与循环结合使用是非常有害的!这是一种非常慢的方法,用于将值分配到数组中。测试5(在循环中直接赋值)比这种方法快近9倍,而两种方法都完全相同:将整数0-9分配到预初始化和预填充的数组中。我没有测试过这种push-for-loop的邪恶是由于推送还是循环或两者的组合或循环计数。在JSPERF.COM上还有其他给出冲突结果的示例。最好只使用实际数据进行测试并做出决策。此测试可能与未使用的其他数据不兼容。

以下是代码:

for(var i=0;i<10;i++)
{
  pre_filled_array.push(i);
}

9. non_pre_filled_array.push(i) (0.74%):

这个测试中最后也是最慢的方法与测试8相同,但数组不是预填充的。比9略慢,但差异不明显(7.23%)。但让我们举个例子,将这个最慢的方法与最快的方法进行比较。该方法的速度是方法1速度的0.74%,这意味着方法1比此方法快135倍。因此,请仔细考虑,在特定用例中是否需要使用数组。如果只有一个或几个推送,则总速度差异不明显,但另一方面,如果只有几个推送,它们非常简单和优雅,可以转换为非数组变量。

这是代码:

for(var i=0;i<10;i++)
{
  non_pre_filled_array.push(i);
}

最后强制提问:

因为根据这个测试,非数组变量赋值和数组赋值之间的速度差异似乎如此巨大,是否有任何方法可以获得非数组变量赋值的速度和数组的动态性?

我不能在循环中使用var variable_$i = 1,以便将 $i 转换为某个整数。我必须使用var variable[i] = 1,这比var variable1 = 1慢得多,正如测试所证明的那样。只有当存在大型数组并且在许多情况下它们才是关键。


编辑: 我进行了一项新的测试,以确认数组访问的缓慢,并试图找到更快的方法:

http://jsperf.com/read-write-array-vs-variable

Array-read和/或array-write比使用普通变量慢得多。如果对数组成员进行了某些操作,则更明智的做法是将数组成员值存储到临时变量中,对临时变量进行这些操作,最后将值存储到数组成员中。虽然代码变得更大,但在循环中内联执行这些操作要快得多。

结论:数组与普通变量类似于磁盘与内存。通常,内存访问比磁盘访问快,普通变量访问比数组访问快。可能连接操作也比使用中间变量更快,但这使得代码有点不可读。



什么是Javascript引擎?代码是否具有JIT的机会? - vonbrand
数组不是邪恶的,它们是对象,而且拥有很多很棒的功能,你不必担心。它们有方便的length属性以及用于过滤/排序等的方法。如果你不需要这些功能,你可以尝试像 window["varname_" + i] 这样的东西来获得一个变量值的“数组”,但似乎你没有测试过。 - Explosion Pills
@Timo 如果你编写自己的JavaScript解释器,也许你可以找到答案。 - Explosion Pills
@PascalBelloncle:http://jsperf.com/pre-filled-array/3 当使用var arr = new Array(10)var arr = []时,性能几乎相同。仅在用零填充数组时,在FF18中可以大幅提高速度,在Chrome24中可以略微提高速度。 - Timo Kähkönen
@ExplosionPills:一定有某些原因。我曾认为变量指向内存中的某个位置,而数组成员则像其他变量一样是一个变量。因此,arr[0]=1variable=1的速度差异不应该太大。也许原因在于我的MacBook。 - Timo Kähkönen
显示剩余4条评论
1个回答

4
给数组赋值比给普通变量赋值要慢得多。数组是恶魔!这很令人沮丧,我真的不明白为什么会出现这种情况。数组是如此重要!正常变量是静态作用域的,可以轻松优化。编译器/解释器将学习它们的类型,并可能避免重复分配相同的值。这些类型的优化也将针对数组进行,但它们并不容易,并且需要更长时间才能生效。解析属性引用时有额外的开销,而且由于JavaScript数组是自动增长列表,因此还需要检查长度。预填充数组将有助于避免容量更改的重新分配,但对于您的小数组(length = 10),这不应该有太大的区别。
有没有一种方法可以获得非数组变量分配的速度和数组的动态性?没有。动态成本确实存在,但它们是值得的 - 循环也是。
你几乎不需要这样的微优化,不要尝试。我所能想到的唯一情况是处理ImageData时的固定大小循环(n <= 4),这里可以应用内联。

Push是邪恶的!

不,只有你的测试有缺陷。jsperf片段在一个定时循环中执行而没有tearup和-down,并且只有在那里你才重置了大小。你重复的push已经产生了长度为数十万的数组,相应地需要内存(re-)分配。请参见http://jsperf.com/pre-filled-array/11上的控制台。

实际上,push 和属性赋值一样快。好的测量数据很少,但是那些被正确执行的数据显示,在不同的浏览器引擎版本中结果各异,并且变化迅速而出乎意料。请参见如何向数组追加内容?, 为什么array.push有时比array[n] = value更快?JavaScript开发人员为什么不使用Array.push()? - 结论是您应该使用最易读/最适合您用例的方法,而不是您认为可能更快的方法。


我甚至不理解这篇帖子...这是一个问题吗?它怎么能得到5个赞和1个收藏呢?无论如何,感谢你正确地回答了。 - 1nfiniti
@mikeyUX:是的,它太长了(而且计算部分似乎完全不相关),但在深处有一个实际的问题。使用Strg+F查找Question即可找到它 :-) - Bergi
虽然有点长,但还好。对此我很抱歉,因为它存在一些缺陷(关于推送邪恶性质的问题,感谢@Bergi发现了这个问题)。这些测量是为了找出在Clipper(http://jsclipper.sourceforge.net/6.2.1.0/main_demo.html)中使用的最高效的变量处理方式。它使用由顶点对象填充的数组,这些对象具有属性X和Y,可能还有Z。 - Timo Kähkönen
我考虑用类型化数组替换这个类似结构,并将 [{X:0,Y:1},{X:0,Y:1},{X:0,Y:1}] 结构替换为 Xs = [0,0,0], Ys = [1,1,1]。但是这需要对整个代码库进行完全的重新编码,并且会使得难以保持 JS 版本与 Angus Johnson 的原始 C# 版本同步。 - Timo Kähkönen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接