Search code examples
javascriptperformanceiteratorsetv8

Why does iterating over a Set's values allocate and create garbage?


Yesterday I asked this question on how to iterate over a Map without allocating. A v8 developer replied with the following:

But if you're only interested in processing the values anyway, you can avoid it being created by iterating over just the values: for (let v of map.values()) {...} does not allocate any short-lived objects. The same is true for iterating over map.keys().

I'm trying to replicate this in a test but I'm having trouble. Here's a demo I made that reproduces the problem I'm having with a simple Set. Paste the following code in an empty HTML file in its script tags:

let a = new Set([ 1, 2, 3, 4, 5 ]);
let b = [ 1, 2, 3, 4, 5 ];

let sum = 0;

function setIterate() {
  for (let item of a.values()) {
    sum += item;
  }
}

function arrayIterate() {
  for (let i = 0; i < b.length; i++) {
    sum += b[i];
  }
}

setInterval(setIterate, 1);
// setInterval(arrayIterate, 1);

If you open this HTML file and then hit Shift+Escape it should open the Chrome Task Manager. If you then look at the Javascript Memory column, you'll see the memory slowly growing over time.

However, if you comment the line setInterval(setIterate, 1); and uncomment the line setInterval(arrayIterate, 1); and refresh the page you'll see that the memory usage stays constant.

That is, iterating over a Set produces garbage build-up over-time while iterating over an array does not.

Is there any way to iterate a Set without producing this garbage build-up over time? I'm developing a browser game and iterating over many Sets and Maps in my render loop and getting occasional frame-spikes due to the GC running.


Solution

  • Why does iterating over a Set's values allocate and create garbage?

    It doesn't. Doing a million calls to setIterate with --trace-gc (in d8 or node) shows zero activity. If the function allocated even a single byte per iteration (which it can't; the minimum allocation granularity is two pointers, i.e. 8 bytes), then the young generation would have filled up at least once in that time.

    Is there any way to iterate a Set without producing this garbage build-up over time?

    Absolutely; for example how you're doing it.

    why does the Task Manager show memory increasing over time?

    I don't see it doing that. I've copy-pasted your snippet exactly as-is and let it sit for a couple of minutes; the "JavaScript memory" column of Chrome's task manager for that tab hasn't budged by a single kilobyte. (Which actually only proves that this test isn't really a stress test: it will start allocating like crazy once sum exceeds 1<<30, but that's not because of the set or the array...)


    Wild guess: maybe you have some extension installed that injects code into every page and causes allocations? At any rate, whatever it is, it's not the for..of-loops over Sets.