我正在编写一些需要快速运行的javascript代码,并使用了大量短期对象。我最好使用对象池,还是只根据需要创建对象?
我写了一个JSPerf test,表明使用对象池没有任何好处,但是我不确定jsperf基准测试是否运行得足够长,以便浏览器的垃圾收集器能够启动。
代码是游戏的一部分,所以我不关心传统的浏览器支持。无论如何,我的图形引擎无法在旧浏览器上运行。
答案 0 :(得分:47)
首先让我说:我会建议不要使用池,除非你正在开发可视化,游戏或其他计算成本高昂的代码,这些代码实际上做了很多工作。您的普通Web应用程序受I / O限制,您的CPU和RAM大部分时间都处于空闲状态。在这种情况下,通过优化I / O而不是执行速度,您可以获得更多收益;即确保,您的文件加载速度快,您使用客户端而不是服务器端渲染+模板。但是,如果您正在玩游戏,科学计算或其他受CPU限制的Javascript代码,那么本文可能对您有用。
短版:
在性能关键代码中:
Array.concat
等)也会做很多邪恶的事情,包括分配。String
),因为这些将在您对其执行的状态更改操作期间创建新对象。长版:
首先,请考虑系统堆与大型对象池基本相同。这意味着,无论何时创建新对象(使用new
,[]
,{}
,()
,nested functions,字符串连接等),系统将使用(非常复杂,快速和低级别的性能调整)算法为您提供一些未使用的空间(即一个对象),确保它的字节被清零并返回它。这与对象池必须做的非常相似。但是,Javascript的运行时堆管理器使用GC来检索"借用的对象",其中池以几乎零成本的方式获取它的对象,但需要开发人员采取小心自己跟踪所有这些物体。
现代Javascript运行时环境(例如V8)具有运行时分析器和运行时优化器,当它识别性能关键代码段时,理想情况下可以(但不一定(尚未))进行积极优化。它还可以使用该信息来确定垃圾收集的好时机。如果它意识到你运行游戏循环,它可能只是在每几个循环后运行GC(甚至可能将老一代集合减少到最小等),从而实际上不会让你感觉它正在做的工作(但是,它仍然会如果是昂贵的操作,请更快地耗尽电池)。有时,优化器甚至可以将分配移动到堆栈,这种分配基本上是免费的,而且对缓存更友好。话虽这么说,这些优化技术并不完美(实际上它们不可能,因为完美的代码优化是NP难的,但那是另一个主题)。
让我们以游戏为例:This talk on fast vector math in JS解释了重复的矢量分配(在大多数游戏中你需要大量的矢量数学)如何减慢速度非常快的东西:Float32Array
的矢量数学。在这种情况下,如果您以正确的方式使用正确类型的游泳池,您可以从游泳池中受益。
这些是我在Javascript中编写游戏时学到的经验:
而不是
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();
,然后再将其替换为池,以便轻松比较速度。更好的是,它允许您在代码中快速找到所有分配,以便在时机成熟时逐个查看。不要担心额外的函数调用,因为任何体面的优化工具都会对此进行内联,甚至可能是运行时优化器。
而不是:
function add(a, b) { return new Vector(a.x + b.x, a.y + a.y); }
// ...
var z = add(x, y);
尝试:
function add(out, a, b) { out.set(a.x + b.x, a.y + a.y); return out; }
// ...
var z = add(x, x, y); // you can do that here, if you don't need x anymore (Note: z = x)
避免:
var tmp = new X(...);
for (var x ...) {
tmp.set(x);
use(tmp); // use() will modify tmp instead of x now, and x remains unchanged.
}
new
可能不需要的额外同步(因为运行时完全控制如何分配事物)。在计算循环紧张的情况下,您可能需要考虑每次迭代进行多次计算,而不是仅进行一次计算(也称为partially unrolled loop)。池算法
除非您编写非常复杂的池查询算法,否则通常会遇到两个或三个选项。这些选项中的每一个在某些情况下都更快,在其他情况下更慢。我经常看到的是:
inUse
标志设置为true。当不再需要该对象时取消设置。玩这些选项。除非您的链表实现相当复杂,否则您可能会发现基于数组的解决方案对于短期对象(这是池性能实际上很重要)更快,给定,数组中没有长期存在的对象,导致搜索自由对象变得不必要地长。如果您通常需要一次分配多个对象(例如,对于部分展开的循环),请考虑一个批量分配选项,它分配(小)对象数组而不是一个,以减少未分配对象的查找开销。如果你真的很喜欢快速游泳池(和/或只想尝试一些新的游戏),请查看速度快的how system heaps are implemented并允许分配不同大小的游戏。
最后的话
无论你决定使用什么,不断进行剖析,研究和分享使我们心爱的JS代码运行得更快的成功方法!
答案 1 :(得分:13)
一般来说(根据我的个人经验),池化对象不会提高速度。创建对象通常非常便宜。相反,对象池的目的是减少垃圾收集导致的 jank (周期性延迟)。
作为一个具体的例子(不一定是JavaScript,但作为一般说明),想想具有高级3D图形的游戏。如果一个游戏的平均帧速率为60fps,那么比其他游戏的更快,平均帧速率为40fps。但是如果第二个游戏的fps 一致 40,那么图形看起来很平滑,而如果第一个游戏的图像看起来通常远高于60fps,但偶尔会下降到10fps,那么图形看起来就会不稳定。
如果您创建一个运行两个游戏10分钟的基准测试并且每隔一段时间对帧速率进行采样,它就会告诉您第一个游戏具有更好的性能。但它不会在波动中发现。这就是对象池要解决的问题。
当然,这不是涵盖所有案例的一揽子声明。一种情况是,池化不仅可以提高波动性,还可以提高原始性能,这是因为您经常分配大型阵列:只需设置arr.length = 0
并重新使用arr
,您就可以通过逃避未来的重新分析来提高性能。类似地,如果您经常创建非常大的对象,它们共享一个共同的模式(即,它们具有明确定义的属性集,因此您不必在清除每个对象时将它返回到池中,在这种情况下,您可能会看到池的性能提升。
正如我所说,通常说话,但这不是对象池的主要目的。
答案 2 :(得分:4)
对象池用于避免通过重用现有对象来创建新对象的实例化成本。只有在实例化对象的成本大于使用池所产生的开销时,这才有用。
您所展示的是,非常简单的对象无法从池中获益。随着您的对象变得更加复杂,这可能会改变。我的建议是遵循KISS原则并忽略对象池,直到对象创建被证明太慢。
答案 3 :(得分:3)
对象池可能有所帮助,特别是如果你正在搅拌很多对象。我最近写了一篇关于这个主题的文章,可能值得一读。
答案 4 :(得分:3)
我认为这取决于对象的复杂程度。我最近优化了一个JavaScript文字处理器,它使用与DOM对象配对的JS对象来处理文档中的每个元素。在实现对象池之前,我的测试文档的加载时间约为480毫秒。汇集技术将其减少到220毫秒。
这当然是轶事,但在我的情况下,它大大增加了应用程序的快速性,现在我经常在具有高对象更新的应用程序中使用池。