ChatGPT解决这个技术问题 Extra ChatGPT

Best practices for reducing Garbage Collector activity in Javascript

I have a fairly complex Javascript app, which has a main loop that is called 60 times per second. There seems to be a lot of garbage collection going on (based on the 'sawtooth' output from the Memory timeline in the Chrome dev tools) - and this often impacts the performance of the application.

So, I'm trying to research best practices for reducing the amount of work that the garbage collector has to do. (Most of the information I've been able to find on the web regards avoiding memory leaks, which is a slightly different question - my memory is getting freed up, it's just that there's too much garbage collection going on.) I'm assuming that this mostly comes down to reusing objects as much as possible, but of course the devil is in the details.

The app is structured in 'classes' along the lines of John Resig's Simple JavaScript Inheritance.

I think one issue is that some functions can be called thousands of times per second (as they are used hundreds of times during each iteration of the main loop), and perhaps the local working variables in these functions (strings, arrays, etc.) might be the issue.

I'm aware of object pooling for larger/heavier objects (and we use this to a degree), but I'm looking for techniques that can be applied across the board, especially relating to functions that are called very many times in tight loops.

What techniques can I use to reduce the amount of work that the garbage collector must do?

And, perhaps also - what techniques can be employed to identify which objects are being garbage collected the most? (It's a farly large codebase, so comparing snapshots of the heap has not been very fruitful)

Do you have an example of your code you could show us? The question will be easier to answer then (but also potentially less general, so I'm not sure here)
How about stop running functions thousands of times per second? Is that really the only way to approach this? This question seems like an XY problem. You are describing X but what you are really looking for is a solution to Y.
@TravisJ: He runs it only 60 times per second, which is a quite common animation rate. He does not ask to do less work, but how to do it more garbage-collection-efficient.
@Bergi - "some functions can be called thousands of times per second". That is once per millisecond (possibly worse!). That is not common at all. 60 times per second should not be an issue. This question is overly vague and is only going to produce opinions or guesses.
@TravisJ - It's not at all uncommon in game frameworks.

M
Mike Samuel

A lot of the things you need to do to minimize GC churn go against what is considered idiomatic JS in most other scenarios, so please keep in mind the context when judging the advice I give.

Allocation happens in modern interpreters in several places:

When you create an object via new or via literal syntax [...], or {}. When you concatenate strings. When you enter a scope that contains function declarations. When you perform an action that triggers an exception. When you evaluate a function expression: (function (...) { ... }). When you perform an operation that coerces to Object like Object(myNumber) or Number.prototype.toString.call(42) When you call a builtin that does any of these under the hood, like Array.prototype.slice. When you use arguments to reflect over the parameter list. When you split a string or match with a regular expression.

Avoid doing those, and pool and reuse objects where possible.

Specifically, look out for opportunities to:

Pull inner functions that have no or few dependencies on closed-over state out into a higher, longer-lived scope. (Some code minifiers like Closure compiler can inline inner functions and might improve your GC performance.) Avoid using strings to represent structured data or for dynamic addressing. Especially avoid repeatedly parsing using split or regular expression matches since each requires multiple object allocations. This frequently happens with keys into lookup tables and dynamic DOM node IDs. For example, lookupTable['foo-' + x] and document.getElementById('foo-' + x) both involve an allocation since there is a string concatenation. Often you can attach keys to long-lived objects instead of re-concatenating. Depending on the browsers you need to support, you might be able to use Map to use objects as keys directly. Avoid catching exceptions on normal code-paths. Instead of try { op(x) } catch (e) { ... }, do if (!opCouldFailOn(x)) { op(x); } else { ... }. When you can't avoid creating strings, e.g. to pass a message to a server, use a builtin like JSON.stringify which uses an internal native buffer to accumulate content instead of allocating multiple objects. Avoid using callbacks for high-frequency events, and where you can, pass as a callback a long-lived function (see 1) that recreates state from the message content. Avoid using arguments since functions that use that have to create an array-like object when called.

I suggested using JSON.stringify to create outgoing network messages. Parsing input messages using JSON.parse obviously involves allocation, and lots of it for large messages. If you can represent your incoming messages as arrays of primitives, then you can save a lot of allocations. The only other builtin around which you can build a parser that does not allocate is String.prototype.charCodeAt. A parser for a complex format that only uses that is going to be hellish to read though.


Don't you think the JSON.parsed objects allocate less (or equal) space than the message string?
@Bergi, That depends on whether the property names require separate allocations, but a parser that generates events instead of a parse tree does no extraneous allocaitons.
Fantastic answer, thank you! Many apologies for the bounty expiring - I was travelling at the time, and for some reason I could not log into SO with my gmail account on my phone.... :/
To make up for my bad timing with the bounty I've added an additional one to top it up (200 was the minimum I could give ;) - For some reason though it's requiring me to wait 24 hours before I award it (even though I selected 'reward existing answer'). Will be yours tomorrow...
@UpTheCreek, no worries. I'm glad you found it useful.
N
Noelle L.

The Chrome developer tools have a very nice feature for tracing memory allocation. It's called the Memory Timeline. This article describes some details. I suppose this is what you're talking about re the "sawtooth"? This is normal behavior for most GC'ed runtimes. Allocation proceeds until a usage threshold is reached triggering a collection. Normally there are different kinds of collections at different thresholds.

https://i.stack.imgur.com/lwrwY.png

Garbage collections are included in the event list associated with the trace along with their duration. On my rather old notebook, ephemeral collections are occurring at about 4Mb and take 30ms. This is 2 of your 60Hz loop iterations. If this is an animation, 30ms collections are probably causing stutter. You should start here to see what's going on in your environment: where the collection threshold is and how long your collections are taking. This gives you a reference point to assess optimizations. But you probably won't do better than to decrease the frequency of the stutter by slowing the allocation rate, lengthening the interval between collections.

The next step is to use the Profiles | Record Heap Allocations feature to generate a catalog of allocations by record type. This will quickly show which object types are consuming the most memory during the trace period, which is equivalent to allocation rate. Focus on these in descending order of rate.

The techniques are not rocket science. Avoid boxed objects when you can do with an unboxed one. Use global variables to hold and reuse single boxed objects rather than allocating fresh ones in each iteration. Pool common object types in free lists rather than abandoning them. Cache string concatenation results that are likely reusable in future iterations. Avoid allocation just to return function results by setting variables in an enclosing scope instead. You will have to consider each object type in its own context to find the best strategy. If you need help with specifics, post an edit describing details of the challenge you're looking at.

I advise against perverting your normal coding style throughout an application in a shotgun attempt to produce less garbage. This is for the same reason you should not optimize for speed prematurely. Most of your effort plus much of the added complexity and obscurity of code will be meaningless.


Right, that's what I mean by the sawtooth. I know there will always be a sawtooth pattern of some kind, but my concern is that with my app the sawtooth frequency and the 'cliffs' are quite high. Interestingly, GC events do not show up on my timeline - the only events that appear in the 'records' pane (the middle one) are: request animation frame, animation frame fired, and composite layers. I have no idea why I'm not seeing GC Event like you are (this is on latest version of chrome, and also canary).
I've tried using the profiler with 'record heap allocations' but thus far have not found it very useful. Perhaps this is because I don't know how to use it properly. It seems to be full of references which mean nothing to me, such as @342342 and code relocation info.
Regarding "premature optimization is the root of all evil": Understand. Don't just follow blindly. In certain scenarios, such as game and multimedia programming, performance is paramount and you're going to have a lot of "hot" code. So yeah, you're going to have to adjust your programming style.
C
Chris B

As a general principle you'd want to cache as much as possible and do as little creating and destroying for each run of your loop.

The first thing that pops in my head is to reduce the use of anonymous functions (if you have any) inside your main loop. Also it'd be easy to fall into the trap of creating and destroying objects that are passed into other functions. I'm by no means a javascript expert, but I would imagine that this:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

would run much faster than this:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Is there ever any downtime for your program? Maybe you need it to run smoothly for a second or two (e.g. for an animation) and then it has more time to process? If this is the case I could see taking objects that would normally be garbage collected throughout the animation and keeping a reference to them in some global object. Then when the animation ends you can clear all the references and let the garbage collector do it's work.

Sorry if this is all a bit trivial compared to what you've already tried and thought of.


This. Plus also functions mentioned inside other functions (that are not IIFEs) is also common abuse that burns a lot of memory and is easy to miss.
Thanks Chris! I don't have any downtime unfortunately :/
M
Mahdi

I'd make one or few objects in the global scope (where I'm sure garbage collector is not allowed to touch them), then I'd try to refactor my solution to use those objects to get the job done, instead of using local variables.

Of course it couldn't be done everywhere in the code, but generally that's my way to avoid garbage collector.

P.S. It might make that specific part of code a little bit less maintainable.


The GC takes out my global scope variables consistently.