5

代码优化在 SO 中说,分析是优化 javascript 的第一步,建议的引擎是 Chrome 和 Firefox 的分析器。问题在于它们以某种奇怪的方式告诉每个函数执行的时间,但我对它们没有很好的理解。最有用的方法是分析器会告诉,每行执行了多少次,如果可能的话,还有每行花费的时间。这样就可以严格地看到瓶颈。但在实施/找到此类工具之前,我们有两个选择:

1)制作自己的计算器,计算特定代码块或行执行的时间和次数 2)学会理解哪些是慢速方法,哪些不是

对于选项 2 , jsperf.com有很大帮助。我尝试学习优化数组并在JSPERF.COM中进行了速度测试。下图显示了在 5 个主要浏览器中的结果,发现了一些我之前不知道的瓶颈。

速度测试

主要发现是:

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 并使用出租车距离进行距离比较,使用菱形角(t-弧度)进行角度比较,但主要瓶颈仍然可能在其他地方:循环比内联慢,推送比使用预初始化和/或预填充的直接分配慢,代码逻辑、绘图算法和 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%):

推是邪恶的!推结合循环是恶毒的!由于某种原因,这是将值分配到数组中的非常慢的方法。测试 5(循环中的直接赋值)比此方法快近 9 倍,并且两种方法的作用完全相同:将整数 0-9 分配到预初始化和预填充的数组中。我还没有测试过这种按循环的邪恶是否是由于推动或循环或两者的组合或循环计数。在 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);
}

最后是强制性的 SO 问题:

因为根据这个测试,non-array-variable-assignments 和 array-assignments 之间的速度差异似乎很大,有没有什么方法可以得到 non-array-variable-assignments 的速度和数组的动态?

我不能var variable_$i = 1在循环中使用,以便将 $i 转换为某个整数。我必须使用var variable[i] = 1它比var variable1 = 1测试证明的要慢得多。仅当存在大型阵列并且在许多情况下确实如此时,这才可能至关重要。


编辑:我做了一个新的测试来确认数组访问的速度并试图找到更快的方法:

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

数组读取和/或数组写入比使用普通变量慢得多。如果对数组成员进行了一些操作,最好将数组成员的值存储到临时变量中,对临时变量进行这些操作,最后将值存储到数组成员中。尽管代码变得更大,但将这些操作内联比循环内要快得多。

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


4

1 回答 1

4

给数组赋值比给普通变量赋值要慢得多。数组是邪恶的!这非常可悲,我真的不明白为什么会发生这种情况。数组太重要了!

这是因为普通变量是静态范围的,可以(并且)很容易优化。编译器/解释器将学习它们的类型,甚至可能避免重复分配相同的值。

这些优化也将针对数组进行,但它们并不那么容易,需要更长的时间才能生效。解析属性引用时会有额外的开销,并且由于 JavaScript 数组是自动增长的列表,因此还需要检查长度。

预填充阵列将有助于避免因容量更改而重新分配,但对于您的小阵列 ( length=10),它不应该有太大的区别。

有什么方法可以获得非数组变量分配的速度和数组的动态?

不。动态确实需要成本,但它们是值得的——循环也是如此。

你几乎不会需要这样的微优化,不要尝试。我唯一能想到的是在处理时固定大小的循环(n <= 4)ImageData,内联是适用的。

推是邪恶的!

不,只有你的测试有缺陷。jsperf 片段在一个定时循环中执行,没有撕裂和向下,只有在那里你才重新设置大小。您重复的pushes 一直在生成长度为十万的数组,并且需要相应的内存(重新)分配。请参阅http://jsperf.com/pre-filled-array/11上的控制台。

实际上push和属性分配一样快。好的测量是罕见的,但那些正确完成的测量会在不同的浏览器引擎版本中显示出不同的结果——变化迅速且出乎意料。请参阅如何将某些内容附加到数组?,为什么array.push 有时比array[n] = value 快?JavaScript开发人员不使用 Array.push() 是否有原因?- 结论是您应该使用最易读/最适合您的用例的内容,而不是您认为可能更快的内容。

于 2013-08-27T23:23:18.343 回答