简体   繁体   中英

Increasing performance of a function which processes an array using destructuring and recursion

I wanted to create a function which takes another function and an array as parameters and calls that function for each three consecutive elements of the array (eg first, second and third; second, third and forth). I implemented it using destructuring and recursion. However, I found out that it has terrible performance – it takes about 100 ms to process a 1000-element array, and uses a lot of memory. Here's a code snippet:

 const eachThree = fn => ([first, second, third, ...rest]) => { fn(first, second, third); if (rest.length !== 0) { eachThree(fn)([second, third, ...rest]); } }; const noop = () => {}; const arr = Array(1000).fill(undefined); console.time('eachThree'); eachThree(noop)(arr); console.timeEnd('eachThree'); 

I'm aware that to increase performance I could just use a regular for loop, but is it possible to modify this function somehow to make it faster while keeping destructuring and recursion?

Also, are there any plans to optimize JavaScript engines to make code like this run faster? Would tail-call optimization solve this?

it takes about 100 ms to process a 1000-element array, and uses a lot of memory.

The reason for that is obviously the destructuring and spread syntax, which creates 2000 arrays with an average size of 500 elements. That ain't cheap.

I'm aware that to increase performance I could just use a regular for loop, but is it possible to modify this function somehow to make it faster while keeping destructuring and recursion?

Giving up the destructuring (in favour of an index, like in @Sylwester's answer) would be the best solution, but there's indeed a few things other things you can optimise:

  • Use return to allow for tail call optimisation (if the engine supports it)
  • Cache the inner function instead of re-creating the closure with the same fn over and over again.

const eachThree = fn => {
  const eachThreeFn = ([first, second, third, ...rest]) => {
    fn(first, second, third);
    if (rest.length !== 0) {
      return eachThreeFn([second, third, ...rest]);
    }
  };
  return eachThreeFn;
};

So I hope for your and our sake you really need to shave off those 200ms since it doesn't seem like a real life problem to me. Know that in the rules of optimization:

  1. Don't
  2. Don't
  3. Profile so you know were to optimize.

When it comes to optimization neatness and style goes out of the window. That means you can throw out recursion. In this case however, it's not the recursion that is your biggest problem but the 2n arrays you are making. Here is a slightly faster version:

const eachThree = fn => arr => {
  const maxLen = arr.length-3;
  const recur = (n) => {
    fn(arr[n], arr[n+1], arr[n+2]);
    if (n < maxLen) {
      recur(n+1);
    }
  }; 
  recur(0);
};

Now ES6 has proper tail calls and Node6 has implemented this. It runs about 400 times faster than your original code when I test it there. Not that you will notice the change IRL.

Since you seem open to refactoring the code, here's a pretty dramatic rewrite that performs the same task about 10x faster (using your means of benchmarking)

I think you'll appreciate that a very functional style has been maintained

 const drop = (n,xs) => xs.slice(n) const take = (n,xs) => xs.slice(0,n) const slide = (x,y) => xs => x > xs.length ? [] : [take (x,xs)] .concat (slide (x,y) (drop (y,xs))) const eachThree = f => xs => slide (3,1) (xs) .forEach (([a,b,c]) => f (a,b,c)) const noop = () => {} const arr = Array(1000).fill(undefined) console.time('eachThree') eachThree(noop)(arr) console.timeEnd('eachThree') 

Using a trampoline solves the memory usage problem, but it doesn't speed up the code. Here's a code snippet:

 const trampoline = fn => { do { fn = fn(); } while (typeof fn === 'function'); }; const eachThree = fn => ([first, second, third, ...rest]) => () => { fn(first, second, third); if (rest.length !== 0) { return eachThree(fn)([second, third, ...rest]); } }; const noop = () => {}; const arr = Array(5000).fill(undefined); console.time('eachThree'); trampoline(eachThree(noop)(arr)); console.timeEnd('eachThree'); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM