If we look at the iterator protocol we'd notice it comes at a cost of creating {value, done}
objects, accessing their props, and calls of next()
(a function call is generally is an expensive operation).
Given all the hurdles we have a quite inefficient tool to iterate our data. Moreover when we implement an iterator with a generator we suddenly notice that performance got even worse. I guess that's because of the cost of keeping a generator's scope and its state between calls.
Don't get me wrong, iterators and generators are good abstractions, for await
is exceptionally amazing when dealing with async data and abstracting backend API calls, so generally used in business logic they bring benefits but when we descend into the realm of transforming data, generic data handling functions and algorithms we should care to avoid them when possible.
Here's a benchmark of several ways to iterate an array and you see the iterator and generator fail considerably. I'm not the only one complaining, I've seen several comments that generators' speed keeps people from using them widely.
class Iter {
*gen(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}
iter(arr) {
let i = 0;
const out = { value: null, done: false };
const iter = {
[Symbol.iterator]: () => iter,
next: () => {
if (i === arr.length) out.done = true, out.value = null;
else out.value = arr[i++];
return out;
}
};
return iter;
}
}
const $chunk = () => Array.from({length: 10}, (_, i) => i);
const $input = [], arr = $input;
const iter = new Iter;
// @benchmark generator
for(const e of iter.gen(arr));
// @benchmark iterator
for(const e of iter.iter(arr));
// @benchmark for..of
for(const e of arr);
// @benchmark for
for(let i = 0; i < arr.length; i++) arr[i];
// @benchmark forEach
arr.forEach(e => {});
` Chrome/125
-----------------------------------------------------------------------------------------
> n=10 | n=100 | n=1000 | n=10000
for 1.17x x100m 336 | 1.20x x10m 354 | 1.01x x1m 235 | ■ 1.00x x100k 218
for..of 1.19x x100m 340 | 1.21x x10m 355 | ■ 1.00x x1m 232 | 1.00x x100k 219
forEach ■ 1.00x x100m 286 | ■ 1.00x x10m 294 | 1.00x x1m 233 | 4.54x x10k 99
iterator 64.34x x1m 184 | 13.71x x1m 403 | 10.86x x100k 252 | 10.96x x10k 239
generator 43.01x x1m 123 | 36.05x x100k 106 | 45.69x x10k 106 | 43.58x x1k 95
----------------------------------------------------------------------------------------- `
` Firefox/126
-----------------------------------------------------------------------------------------
> n=10 | n=100 | n=1000 | n=10000
for ■ 1.00x x100m 512 | ■ 1.00x x10m 505 | ■ 1.00x x1m 453 | ■ 1.00x x100k 449
forEach 1.62x x100m 828 | 1.48x x10m 746 | 4.26x x100k 193 | 6.57x x10k 295
for..of 11.07x x10m 567 | 9.35x x1m 472 | 9.98x x100k 452 | 10.11x x10k 454
iterator 23.44x x1m 120 | 10.93x x1m 552 | 11.24x x100k 509 | 10.71x x10k 481
generator 82.62x x1m 423 | 64.75x x100k 327 | 66.67x x10k 302 | 68.37x x1k 307
----------------------------------------------------------------------------------------- `
One would argue that for..of
in Chrome performs nice despite of using an array's iterator but that's an optimization of using a native iterator. Firefox is generally more honest in benchmarks if we want generic evaluation of Javascript code, but about that is in the next post.
Top comments (0)