高性能的JavaScript对象池?
我正在写一些JavaScript代码,需要运行速度快,并使用了很多短暂的对象。 我最好使用一个对象池,或只是创build对象,因为我需要他们?
我写了一个JSPerftesting ,这表明使用对象池没有任何好处,但是我不确定是否jsperf基准testing运行足够长的时间来浏览器的垃圾回收器。
代码是游戏的一部分,所以我不关心旧版浏览器的支持。 无论如何,我的graphics引擎不能在旧版浏览器上运行。
让我开始说:我会build议反对池,除非你正在开发可视化,游戏或其他计算昂贵的代码,实际上做了很多工作。 您的平均networking应用程序是I / O限制,你的CPU和RAM大部分时间都是空闲的。 在这种情况下,通过优化I / O-而不是执行速度可以获得更多的收益。 即确保您的文件加载速度快,而且您采用客户端而不是服务器端渲染+模板。 但是,如果你正在玩游戏,科学计算或其他CPU绑定的Javascript代码,这篇文章可能对你很有趣。
短版 :
在性能严重的代码中:
- 首先使用通用优化 [1] [2] [3] [4] (还有更多)。 不要马上跳进水池(你知道我的意思!)。
- 小心句法糖和外部库,因为即使Promises和许多内置函数(比如
Array.concat
等)也会在引擎盖下做很多邪恶的东西,包括分配。 - 避免不可变(如
String
),因为这些将在您执行的状态更改操作期间创build新对象。 - 知道你的分配。 使用封装来创build对象,因此您可以轻松地查找所有分配,并在分析期间快速更改分配策略。
- 如果担心绩效,请始终分析并比较不同的方法。 理想情况下,你不应该随机相信intarwebz(包括我)的人。 请记住,我们对“快”,“长寿”等词语的定义可能差别很大。
- 如果你决定使用池:
- 您可能必须为长寿命和短寿命的对象使用不同的池,以避免短寿命池的分裂。
- 您想要比较不同的algorithm和不同的池化粒度(池整个对象或仅池对象属性?)为不同的场景。
- 合并增加了代码的复杂性,从而使优化器的工作变得更加困难,潜在地降低了性能。
长版本 :
首先,请考虑系统堆本质上与大型对象池相同。 这意味着,无论何时创build新对象(使用new
, []
, {}
, ()
, 嵌套函数 ,string连接等),系统都将使用(非常复杂,快速和低级的性能调优)algorithm给你一些未使用的空间(即一个对象),确保它的字节被清零并返回它。 这与对象池必须做的非常相似。 然而,Javascript的运行时堆pipe理器使用GC来检索“借来的对象”,其中一个池以几乎零成本获取对象,但是需要开发者自己处理所有这些对象。
现代的Javascript运行环境,比如V8,有一个运行时分析器和运行时优化器,当它识别性能关键的代码段时,理想情况下可以(但不一定(还))优化。 它也可以使用这些信息来确定垃圾收集的好时机。 如果它意识到你运行一个游戏循环,它可能会在每几个循环之后运行GC(甚至可能会减less旧一代的收集等),从而不会让你感觉到它正在做的工作(但是,它仍然会如果这是一个昂贵的操作,则更快地排空你的电池)。 有时,优化器甚至可以将分配移动到堆栈,而这种分配基本上是免费的,并且更容易caching。 这就是说,这些优化技术并不完美(实际上它们不是完美的,因为完美的代码优化是NP-hard,但这是另一个话题)。
让我们以游戏为例: 这个关于JS中快速向量math的讨论解释了如何重复向量分配(在大多数游戏中你需要大量的向量math)减慢了应该非常快的事情:vectormath与Float32Array
。 在这种情况下,如果您以正确的方式使用正确的游泳池,您可以从游泳池中受益。
这些是我从Javascript编写游戏中学到的教训:
- 在函数中封装所有经常使用的对象的创build。 让它首先返回一个新的对象,然后将其与一个池版本进行比较:
代替
var x = new X(...);
使用:
var x = X.create(...);
甚至:
// this keeps all your allocation in the control of `Allocator`: var x = Allocator.createX(...); // or: var y = Allocator.create('Y', ...);
这样,你可以实现X.create
或Allocator.createX
return new X();
首先,然后用一个池replace它,以便轻松地比较速度。 更好的是,它可以让你快速find你的代码中的所有分配 ,所以你可以一个接一个地查看它们。 不要担心额外的函数调用,因为任何像样的优化器工具都可以内联,甚至可能还有运行时优化器。
- 尽量将对象创build保持在最低限度。 如果您可以重新使用现有的对象,就这样做。 以2Dvectormath为例:不要使vector(或其他经常使用的对象)不可变。 即使不变性产生更漂亮和更具错误弹性的代码,它往往是非常昂贵的(因为突然间,每个向量操作都需要创build一个新的向量,或者从池中获取一个,而不是仅添加或乘以几个数字)。 为什么在其他语言中,可以使向量不可变是因为这些分配通常可以在堆栈上完成,从而将分配成本降低到几乎为零。 然而在Javascript中 –
代替:
function add(a, b) { return new Vector(ax + bx, ay + ay); } // ... var z = add(x, y);
尝试:
function add(out, a, b) { out.set(ax + bx, ay + ay); return out; } // ... var z = add(x, x, y); // you can do that here, if you don't need x anymore (Note: z = x)
- 不要创build临时variables。 那些并行优化实际上是不可能的。
避免:
var tmp = new X(...); for (var x ...) { tmp.set(x); use(tmp); // use() will modify tmp instead of x now, and x remains unchanged. }
- 就像循环前面的临时variables一样,简单的池化会妨碍简单循环的并行优化:优化器将很难certificate池操作不需要特定的顺序,并且至less需要额外的同步这可能不是
new
需要(因为运行时间完全控制如何分配的东西)。 在紧密计算循环的情况下,您可能需要考虑每次迭代进行多次计算,而不仅仅是一次(也称为部分展开循环 )。 - 除非你真的喜欢修补,不要写自己的游泳池。 那里已经有很多了。 例如, 这篇文章列出了一大堆。
- 如果你发现内存stream失会破坏你的一天,那么只能尝试集中。 在这种情况下,请确保正确地分析您的应用程序,找出瓶颈并作出反应。 一如既往:不要盲目优化。
- 根据池查询algorithm的types,您可能希望将不同的池用于长寿命和短寿命的对象,以避免短寿命池的碎片化。 查询短寿命对象比查询长寿命对象(因为前者可能每秒发生数百次,数千甚至数百万次)要关键得多。
池algorithm
除非你写了一个非常复杂的池查询algorithm,否则你通常会遇到两个或三个选项。 这些选项中的每一个在某些情况下速度更快,在其他情况下则更慢 我经常看到的是:
- 链接列表:只保留列表中的空对象。 无论什么时候需要一个对象,将它从列表中删除很less的成本。 放回去,当物体不再需要时。
- 数组:保留数组中的所有对象。 无论什么时候需要一个对象,遍历所有的池对象,返回第一个空闲的对象,并将它的
inUse
标志设置为true。 当对象不再需要时,将其解除。
玩这些选项。 除非您的链表实现相当复杂,否则您可能会发现基于数组的解决scheme对于短期对象(这是池性能实际上很重要的地方)更快,因为数组中没有长寿命的对象search一个免费的对象变得不必要的漫长。 如果您通常需要一次分配多个对象(例如,对于部分展开的循环),请考虑使用批量分配选项来分配(小)对象数组,而不是仅分配一个对象,以减less未分配对象的查找开销。 如果你真的是一个快速的池(和/或只是想尝试新的东西)热,看看如何实现系统堆快速,并允许分配不同的大小。
最后的话
无论您决定使用什么,保持分析,研究和分享成功的方法,使我们心爱的JS代码运行得更快!
一般来说(以我个人的经验),集中对象不会提高速度 。 创build对象通常非常便宜。 相反,对象池的目的是减less由垃圾收集引起的周期性滞后。
作为一个具体的例子(不一定是用于JavaScript,而是作为一个普通的例子),想象高级3Dgraphics的游戏。 如果一个游戏的平均帧率为60fps,那么比另一个游戏的平均帧率为40fps 更快 。 但是,如果第二款游戏的fps 始终为 40,则graphics看起来很平滑,而如果第一款游戏的fps往往高于60fps,但偶尔会偶尔下降到10fps,graphics看起来会波涛汹涌。
如果您创build一个运行两个游戏10分钟的基准,并且每隔一段时间对帧频进行一次采样,则会告诉您第一个游戏具有更好的性能。 但是它不会起反应。 这就是问题对象池所要解决的问题。
当然,这不是一个涵盖所有案例的总括性陈述。 一种情况下,不仅可以改善混乱,而且可以改善原始性能:当您经常分配大型数组时:通过简单地设置arr.length = 0
并重新使用arr
,您可以通过避免将来重新resize来提高性能。 同样的,如果你经常创build非常大的对象,它们都共享一个共同的模式 (也就是说,它们有一组定义良好的属性,所以在返回池时不必“清理”每个对象),在这种情况下,您可能会看到池中的性能改善。
正如我所说, 一般来说 ,这不是对象池的主要目的。
对象池用于避免通过重用现有对象来创build新对象的实例化成本。 这只在实例化对象的成本大于使用池所产生的开销时才有用。
你已经certificate,非常简单的物体不会从池中获益。 随着你的对象变得更复杂,这可能会改变。 我的build议是遵循KISS原则,忽略对象池,直到对象创build被certificate太慢。
对象池可能会有帮助,特别是如果你正在搅动很多对象。 我最近写了一篇关于这个话题的文章,值得一读。
我认为这取决于你的对象的复杂性。 我最近优化了一个JavaScript文字处理器,该文字处理器使用与DOM对象配对的JS对象来处理文档中的每个元素。 在实现对象池之前,我的testing文档的加载时间大约是480ms。 池化技术减less到220ms。
这当然是轶事,但在我的情况下,它大大增加了应用程序的快感,现在我经常使用池高应用程序周转的应用程序。