高性能javascript中的对象池? [英] Object pools in high performance javascript?

查看:65
本文介绍了高性能javascript中的对象池?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm writing some javascript code which needs to run fast, and uses a lot of short-lived objects. Am I better off using an object pool, or just creating objects as I need them?

我写了一个 JSPerf 测试,这表明使用对象池没有任何好处,但是我不确定 jsperf 基准测试的运行时间是否足以让浏览器的垃圾收集器启动.

I wrote a JSPerf test which suggests that there is no benefit to using an object pool, however I'm not sure if jsperf benchmarks are run long enough for the browser's garbage collector to kick in.

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

The code is part of a game, so I don't care about legacy browser support. My graphics engine won't work on old browsers anyway.

推荐答案

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

Let me start by saying: I would advice against pools, unless you are developing visualizations, games or other computationally expensive code that actually does a lot of work. Your average web app is I/O bound and your CPU and RAM will be idle most of the time. In that case, you gain much more by optimizing I/O- rather than execution- speed; i.e. make sure, your files load fast and you employ client-side rather than server-side rendering+templating. However, if you are toying around with games, scientific computation or other CPU-bound Javascript code, this article might be interesting for you.

简短版本:

在性能关键代码中:

  1. 首先使用通用优化 [1] [2] [3] [4](以及更多).不要马上跳进游泳池(你懂我的意思!).
  2. 注意语法糖和外部库,即使 Promises 和许多内置函数(例如 Array.concat 等)在幕后做了很多邪恶的事情,包括分配.
  3. 避免使用不可变对象(例如 String),因为它们会在您对它们执行的状态更改操作期间创建新对象.
  4. 了解您的分配情况.使用封装来创建对象,因此您可以在分析期间轻松找到所有分配,并快速更改分配策略.
  5. 如果您担心性能,请始终分析和比较不同的方法.理想情况下,你不应该随意相信 intarwebz 上的某个人(包括我).请记住,我们对快速"、长寿"等词的定义可能会有很大不同.
  6. 如果您决定使用池化:
    • 您可能必须为长期和短期的对象使用不同的池,以避免短期池的碎片化.
    • 您想针对不同的场景比较不同的算法和不同的池化粒度(池化整个对象还是仅池化某些对象属性?).
    • 池化会增加代码的复杂性,从而使优化器的工作更加困难,可能会降低性能.
  1. Start by using general-purpose optimizations [1] [2] [3] [4] (and many more). Don't jump into pools right away (you know what I mean!).
  2. Be careful with syntactic sugar and external libraries, as even Promises and many built-ins (such as Array.concat etc.) do a lot of evil stuff under the hood, including allocations.
  3. Avoid immutables (such as String), since those will create new objects during state-changing operations you perform on them.
  4. Know your allocations. Use encapsulation for object creation, so you can easily find all allocations, and quickly change your allocation strategy, during profiling.
  5. If you worry about performance, always profile and compare different approaches. Ideally, you should not randomly believe someone on the intarwebz (including me). Remember that our definitions of words such as "fast", "long-lived" etc. might differ vastly.
  6. If you decide to use pooling:
    • You might have to use different pools for long-lived and short-lived objects to avoid fragmentation of the short-lived pool.
    • You want to compare different algorithms and different pooling granularity (pool entire objects or only pool some object properties?) for different scenarios.
    • Pooling increases code complexity and thus makes the optimizer's job more difficult, potentially reducing performance.

长版:

首先考虑系统堆本质上和大对象池是一样的.这意味着,每当您创建一个新对象(使用 new[]{}()嵌套函数、字符串连接等),系统将使用(非常复杂、快速和低级性能调优)算法为您提供一些未使用的空间(即对象),确保将其字节清零并返回.这与对象池所做的非常相似.但是,Javascript 的运行时堆管理器使用 GC 来检索借用对象",其中池以几乎零成本取回其对象,但需要开发人员自己负责跟踪所有此类对象.

First, consider that the system heap is essentially the same as a large object pool. That means, whenever you create a new object (using new, [], {}, (), nested functions, string concatenation, etc.), the system will use a (very sophisticated, fast and low-level performance-tuned) algorithm to give you some unused space (i.e. an object), makes sure it's bytes are zeroed out and return it. That is very similar to what an object pool has to do. However, the Javascript's run-time heap manager uses the GC to retrieve "borrowed objects", where a pool gets it's objects back at almost zero cost, but requires the developer to take care of tracking all such objects herself.

现代 Javascript 运行时环境,例如 V8,具有运行时分析器和运行时优化器,理想情况下可以(但不一定(还))在识别性能关键代码部分时进行积极优化.它还可以使用该信息来确定垃圾收集的好时机.如果它意识到你运行了一个游戏循环,它可能只是在每几个循环之后运行一次 GC(甚至可能将老一代收集减少到最低限度等),从而实际上并没有让你感觉到它正在做的工作(但是,它仍然会如果这是一项昂贵的操作,请更快地耗尽电池电量).有时,优化器甚至可以将分配移到堆栈中,这种分配基本上是免费的,而且对缓存更友好.话虽如此,这些优化技术并不完美(实际上也不可能完美,因为完美的代码优化是 NP 难的,但那是另一个话题).

Modern Javascript run-time environments, such as V8, have a run-time profiler and run-time optimizer that ideally can (but do not necessarily (yet)) optimize aggressively, when it identifies performance-critical code sections. It can also use that information to determine a good time for garbage collection. If it realizes you run a game loop, it might just run the GC after every few loops (maybe even reduce older generation collection to a minimum etc.), thereby not actually letting you feel the work it is doing (however, it will still drain your battery faster, if it is an expensive operation). Sometimes, the optimizer can even move the allocation to the stack, and that sort of allocation is basically free and much more cache-friendly. That being said, these kinds of optimization techniques are not perfect (and they actually cannot be, since perfect code optimization is NP-hard, but that's another topic).

让我们以游戏为例:这个关于 JS 中快速向量数学的演讲 解释了如何重复矢量分配(在大多数游戏中你需要大量的矢量数学)减慢了一些应该非常快的事情:带有 Float32Array 的矢量数学.在这种情况下,如果您以正确的方式使用正确类型的池,您就可以从池中受益.

Let us take games for example: This talk on fast vector math in JS explains how repeated vector allocation (and you need A LOT of vector math in most games) slowed down something that should be very fast: Vector math with Float32Array. In this case, you can benefit from a pool, if you use the right kind of pool in the right way.

这些是我从用 Javascript 编写游戏中学到的经验教训:

These are my lessons learned from writing games in 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', ...);

这样,你可以先用return new X();实现X.createAllocator.createX,然后再替换稍后与游泳池,以轻松比较速度.更好的是,它允许您快速找到代码中的所有分配,以便您可以在适当的时候一一查看它们.不要担心额外的函数调用,因为它会被任何合适的优化器工具内联,甚至可能被运行时优化器内联.

This way, you can implement X.create or Allocator.createX with return new X(); first, and then replace it with a pool later on, to easily compare the speed. Better yet, it allows you to quickly find all allocations in your code, so you can review them one by one, when the time comes. Don't worry about the extra function invocation, as that will be inlined by any decent optimizer tool, and possibly even by the run-time optimizer.

  • 通常尽量减少对象的创建.如果您可以重用现有对象,就这样做.以二维向量数学为例:不要让向量(或其他常用对象)不可变.尽管不变性产生了更漂亮、更能抵御错误的代码,但它往往非常昂贵(因为突然每个向量操作都需要创建一个新向量,或从池中获取一个向量,而不是仅仅添加或乘以几个数字).在其他语言中,您可以使向量不可变的原因是因为这些分配通常可以在堆栈上完成,从而将分配成本几乎为零.然而在 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 可能不需要这些同步(因为运行时可以完全控制如何分配事物).如果计算循环紧凑,您可能需要考虑在每次迭代中进行多次计算,而不仅仅是一次(也称为 部分展开循环).
    • 除非您真的喜欢修补,否则不要编写自己的池.那里已经有很多了.例如,这篇文章列出了一大堆.
    • 仅当您发现内存流失毁了您的一天时才尝试池化.在这种情况下,请确保正确分析您的应用程序,找出瓶颈并做出反应.一如既往:不要盲目优化.
    • 根据池查询算法的类型,您可能希望对长期存在和短期存在的对象使用不同的池,以避免短期池的碎片化.查询短期对象比查询长期对象对性能的要求更高(因为前者每秒可能发生数百、数千甚至数百万次).
      • Just like temp variables in front of your loops, simple pooling will hamper parallelization optimizations of simple loops: The optimizer will have a hard time proving that your pool operations don't require a specific order, and at the very least it will need additional synchronization that might not be necessary for new (because the run-time has full control over how to allocate things). In case of tight computational loops, you might want to consider doing multiple computations per iteration, rather than just one (that is also known as a partially unrolled loop).
      • Unless, you really like to tinker, don't write your own pool. There are already plenty of them out there. This article, for example, lists a whole bunch.
      • Only try pooling, if you find that memory churn ruins your day. In that case, make sure to properly profile your application, figure out the bottlenecks, and react. As always: Don't optimize blindly.
      • Depending on the type of pool querying algorithm, you might want to use different pools for long-lived and short-lived objects to avoid fragmentation of the short-lived pool. Querying short-lived objects is much more performance-critical than querying long-lived objects (because the former can happen hundreds, thousands or even millions of times per second).
      • 池算法

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

        Unless you write a very sophisticated pool querying algorithm, you are generally stuck with two or three options. Each of these options are faster in some and slower in other scenarios. The ones I saw most often are:

        1. 链表:只在链表中保留空对象.每当需要一个对象时,以很少的代价将其从列表中删除.当不再需要该对象时,将其放回原处.
        2. Array:保留数组中的所有对象.每当需要一个对象时,遍历所有池对象,返回第一个空闲的对象,并将它的 inUse 标志设置为 true.当不再需要对象时取消设置.
        1. Linked list: Only keep empty objects in the list. Whenever an object is needed, remove it from the list at little cost. Put it back, when the object is no longer needed.
        2. Array: Keep all objects in the array. Whenever an object is needed, iterate over all pooled objects, return the first one that is free, and set it's inUse flag to true. Unset it when the object is no longer needed.

        尝试使用这些选项.除非您的链表实现相当复杂,否则您可能会发现基于数组的解决方案对于短期对象(这是池性能实际上很重要的地方)更快,因为数组中没有长期存在的对象,导致搜索空闲对象变得不必要的长时间.如果您通常需要一次分配多个对象(例如,对于部分展开的循环),请考虑分配(小)对象数组而不是一个对象数组的批量分配选项,以减少未分配对象的查找开销.如果您真的很喜欢快速泳池(和/或只是想尝试新事物),请查看 如何实现系统堆,这些堆速度很快,并允许分配不同大小的内存.

        Play around with those options. Unless your linked list implementation is rather sophisticated, you will probably find that the array-based solution is faster for short-lived objects (which is where pool performance actually matters), given, there are no long-lived objects in the array, causing the search for a free object to become unnecessarily long. If you usually need to allocate more than one object at a time (e.g. for your partially unrolled loops), consider a bulk allocation option that allocates (small) arrays of objects, rather than just one, to reduce the lookup overhead for unallocated objects. If you are really hot for a fast pool (and/or just wanna try out something new), look at how system heaps are implemented which are fast and allow for allocations of varying sizes.

        最后的话

        无论您决定使用什么,请继续分析、研究和分享使我们心爱的 JS 代码运行得更快的成功方法!

        Whatever you decide to use, keep profiling, researching and sharing successful approaches of making our beloved JS code run even faster!

        这篇关于高性能javascript中的对象池?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆