Is js native array.flat slow for depth=1?

为君一笑 提交于 2021-02-04 16:36:45

问题


This gist is a small benchmark I wrote comparing the performance for 4 alternatives for flattening arrays of depth=1 in JS (the code can be copied as-is into the google console). If I'm not missing anything, the native Array.prototype.flat has the worst performance by far - on the order of 30-50 times slower than any of the alternatives.

Update: I've created a benchmark on jsperf.

It should be noted that the 4th implementation in this benchmark is consistently the most performant - often achieving a performance that is 70 times better. The code was tested several times in node v12 and the Chrome console.

This result is accentuated most in a large subset - see the last 2 arrays tested below. This result is very surprising, given the spec, and the V8 implementation which seems to follow the spec by the letter. My C++ knowledge is non-existent, as is my familiarity with the V8 rabbit hole, but it seems to me that given the recursive definition, once we reach a final depth subarray, no further recursive calls are made for that subarray call (the flag shouldFlatten is false when the decremented depth reaches 0, i.e., the final sub-level) and adding to the flattened result includes iterative looping over each sub-element, and a simple call to this method. Therefore, I cannot see a good reason why a.flat should suffer so much on performance.

I thought perhaps the fact that in the native flat the result's size isn't pre-allocated might explain the difference. The second implementation in this benchmark, which isn't pre-allocated, shows that this alone cannot explain the difference - it is still 5-10 times more performant than the native flat. What could be the reason for this?

Implementations tested (order is the same in code, stored in the implementations array - the two I wrote are at the end of code snippet):

  1. My own flattening implementation that includes pre-allocating the final flattened length (thus avoiding size all re-allocations). Excuse the imperative style code, I was going for max performance.
  2. Simplest naive implementation, looping over each sub-array and pushing into the final array. (thus risking many size re-allocations).
  3. Array.prototype.flat (native flat)
  4. [ ].concat(...arr) (=spreading array, then concatenating the results together. This is a popular way of accomplishing a depth=1 flattening).

Arrays tested (order is the same in code, stored in the benchmarks object):

  1. 1000 subarrays with 10 elements each. (10 thou total)
  2. 10 subarrays with 1000 elements each. (10 thou total)
  3. 10000 subarrays with 1000 elements each. (10 mil total)
  4. 100 subarrays with 100000 elements each. (10 mil total)

let TenThouWideArray = Array(1000).fill().map(el => Array(10).fill(1));
let TenThouNarrowArray = Array(10).fill().map(el => Array(1000).fill(1));
let TenMilWideArray = Array(10000).fill().map(el => Array(1000).fill(1));
let TenMilNarrowArray = Array(100).fill().map(el => Array(100000).fill(1));

let benchmarks = { TenThouWideArray, TenThouNarrowArray, TenMilWideArray, TenMilNarrowArray };
let implementations = [
    flattenPreAllocated,
    flattenNotPreAllocated,
    function nativeFlat(arr) { return Array.prototype.flat.call(arr) },
    function spreadThenConcat(arr) { return [].concat(...arr) }
];

let result;
Object.keys(benchmarks).forEach(arrayName => {
    console.log(`\n............${arrayName}............\n`)
    implementations.forEach(impl => {
        console.time(impl.name);
        result = impl(benchmarks[arrayName]);
        console.timeEnd(impl.name);
    })
    console.log(`\n............${arrayName}............\n`)
})


function flattenPreAllocated(arr) {
    let numElementsUptoIndex = Array(arr.length);
    numElementsUptoIndex[0] = 0;
    for (let i = 1; i < arr.length; i++)
        numElementsUptoIndex[i] = numElementsUptoIndex[i - 1] + arr[i - 1].length;
    let flattened = new Array(numElementsUptoIndex[arr.length - 1] + arr[arr.length - 1].length);
    let skip;
    for (let i = 0; i < arr.length; i++) {
        skip = numElementsUptoIndex[i];
        for (let j = 0; j < arr[i].length; j++)
            flattened[skip + j] = arr[i][j];
    }
    return flattened;
}
function flattenNotPreAllocated(arr) {
    let flattened = [];
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr[i].length; j++) {
            flattened.push(arr[i][j])
        }
    }
    return flattened;
}

回答1:


(V8 developer here.)

The key point is that the implementation of Array.prototype.flat that you found is not at all optimized. As you observe, it follows the spec almost to the letter -- that's how you get an implementation that's correct but slow. (Actually the verdict on performance is not that simple: there are advantages to this implementation technique, like reliable performance from the first invocation, regardless of type feedback.)

Optimizing means adding additional fast paths that take various shortcuts when possible. That work hasn't been done yet for .flat(). It has been done for .concat(), for which V8 has a highly complex, super optimized implementation, which is why that approach is so stunningly fast.

The two handwritten methods you provided get to make assumptions that the generic .flat() has to check for (they know that they're iterating over arrays, they know that all elements are present, they know that the depth is 1), so they need to perform significantly fewer checks. Being JavaScript, they also (eventually) benefit from V8's optimizing compiler. (The fact that they get optimized after some time is part of the explanation why their performance appears somewhat variable at first; in a more robust test you could actually observe that effect quite clearly.)

All that said, in most real applications you probably won't notice a difference in practice: most applications don't spend their time flattening arrays with millions of elements, and for small arrays (tens, hundreds, or thousands of elements) the differences are below the noise level.



来源:https://stackoverflow.com/questions/61411776/is-js-native-array-flat-slow-for-depth-1

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!