高性能JavaScript中的对象池?

时间:2011-12-07 05:06:33

标签: javascript performance

我正在编写一些需要快速运行的javascript代码,并使用了大量短期对象。我最好使用对象池,还是只根据需要创建对象?

我写了一个JSPerf test,表明使用对象池没有任何好处,但是我不确定jsperf基准测试是否运行得足够长,以便浏览器的垃圾收集器能够启动。

代码是游戏的一部分,所以我不关心传统的浏览器支持。无论如何,我的图形引擎无法在旧浏览器上运行。

5 个答案:

答案 0 :(得分:47)

首先让我说:我会建议不要使用池,除非你正在开发可视化,游戏或其他计算成本高昂的代码,这些代码实际上做了很多工作。您的普通Web应用程序受I / O限制,您的CPU和RAM大部分时间都处于空闲状态。在这种情况下,通过优化I / O而不是执行速度,您可以获得更多收益;即确保,您的文件加载速度快,您使用客户端而不是服务器端渲染+模板。但是,如果您正在玩游戏,科学计算或其他受CPU限制的Javascript代码,那么本文可能对您有用。

短版

在性能关键代码中:

  1. 首先使用general-purpose optimizations [1] [2] [3] [4](以及更多内容)。不要马上跳进游泳池(你知道我的意思!)。
  2. 小心语法糖和外部库,因为即使Promises和许多内置函数(例如Array.concat等)也会做很多邪恶的事情,包括分配。
  3. 避免使用不可变项(例如String),因为这些将在您对其执行的状态更改操作期间创建新对象。
  4. 了解您的分配。使用封装来创建对象,这样您可以在分析期间轻松找到所有分配并快速更改分配策略。
  5. 如果您担心性能,请始终分析并比较不同的方法。理想情况下,你不应该随便相信intarwebz(包括我)。请记住,我们对单词的定义,例如" fast"," long-lived"等可能差别很大。
  6. 如果您决定使用合并:
    • 您可能必须为长寿命和短期对象使用不同的池,以避免短期池的碎片化。
    • 您想要针对不同的场景比较不同的算法和不同的池化粒度(池整个对象或仅汇集一些对象属性?)。
    • 池化会增加代码复杂性,从而使优化程序的工作更加困难,从而可能降低性能。
  7. 长版

    首先,请考虑系统堆与大型对象池基本相同。这意味着,无论何时创建新对象(使用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.createXreturn new X();,然后再将其替换为池,以便轻松比较速度。更好的是,它允许您在代码中快速找到所有分配,以便在时机成熟时逐个查看。不要担心额外的函数调用,因为任何体面的优化工具都会对此进行内联,甚至可能是运行时优化器。

    • 尝试将对象创建保持在最低限度。如果您可以重复使用现有对象,那就这样做吧。以2D矢量数学为例:不要使矢量(或其他常用对象)不可变。即使不变性产生更漂亮和更具bug的代码,但它往往非常昂贵(因为突然每个向量操作都需要创建一个新的向量,或从池中获取一个,而不是仅添加或乘以几个数字)。在其他语言中,您可以使向量不可变的原因是因为通常这些分配可以在堆栈上完成,从而将分配成本降低到几乎为零。但是在Javascript中 -

    而不是:

    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)。
    • 除非你真的喜欢修补,否则不要写自己的游泳池。那里已经有很多了。例如,This article列出了一大堆。
    • 如果您发现memory churn毁了您的一天,请尝试合并。在这种情况下,请确保正确分析您的应用程序,找出瓶颈并做出反应。一如既往:不要盲目优化。
    • 根据池查询算法的类型,您可能希望对长寿命和短期对象使用不同的池,以避免短期池的碎片化。查询短期对象比查询长寿命对象更具性能要求(因为前者可能每秒发生数百次,数千次甚至数百次)。

    池算法

    除非您编写非常复杂的池查询算法,否则通常会遇到两个或三个选项。这些选项中的每一个在某些情况下都更快,在其他情况下更慢。我经常看到的是:

    1. 链接列表:仅在列表中保留空对象。每当需要一个对象时,只需很少的费用就将其从列表中删除。当不再需要该物体时,将其放回去。
    2. 数组:保留数组中的所有对象。每当需要一个对象时,迭代所有池化对象,返回第一个空闲对象,并将其inUse标志设置为true。当不再需要该对象时取消设置。
    3. 玩这些选项。除非您的链表实现相当复杂,否则您可能会发现基于数组的解决方案对于短期对象(这是池性能实际上很重要)更快,给定,数组中没有长期存在的对象,导致搜索自由对象变得不必要地长。如果您通常需要一次分配多个对象(例如,对于部分展开的循环),请考虑一个批量分配选项,它分配(小)对象数组而不是一个,以减少未分配对象的查找开销。如果你真的很喜欢快速游泳池(和/或只想尝试一些新的游戏),请查看速度快的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)

对象池可能有所帮助,特别是如果你正在搅拌很多对象。我最近写了一篇关于这个主题的文章,可能值得一读。

http://buildnewgames.com/garbage-collector-friendly-code/

答案 4 :(得分:3)

我认为这取决于对象的复杂程度。我最近优化了一个JavaScript文字处理器,它使用与DOM对象配对的JS对象来处理文档中的每个元素。在实现对象池之前,我的测试文档的加载时间约为480毫秒。汇集技术将其减少到220毫秒。

这当然是轶事,但在我的情况下,它大大增加了应用程序的快速性,现在我经常在具有高对象更新的应用程序中使用池。