简体   繁体   English

使用解构和递归处理数组的函数的性能提高

[英]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. 但是,我发现它的性能很差–处理大约1000个元素的数组大约需要100毫秒,并占用大量内存。 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? 我知道要提高性能,我可以只使用常规的for循环,但是是否可以以某种方式修改此函数以使其更快,同时保持分解和递归?

Also, are there any plans to optimize JavaScript engines to make code like this run faster? 另外,是否有计划优化JavaScript引擎以使这样的代码运行更快? Would tail-call optimization solve this? 尾注优化会解决这个问题吗?

it takes about 100 ms to process a 1000-element array, and uses a lot of memory. 处理1000个元素的数组大约需要100毫秒,并占用大量内存。

The reason for that is obviously the destructuring and spread syntax, which creates 2000 arrays with an average size of 500 elements. 显然,其原因是解构和散布语法,该语法创建了2000个数组,平均大小为500个元素。 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? 我知道要提高性能,我可以只使用常规的for循环,但是是否可以以某种方式修改此函数以使其更快,同时保持分解和递归?

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: 放弃销毁(支持索引,例如@Sylwester的答案)将是最好的解决方案,但实际上还有其他一些事情可以优化:

  • Use return to allow for tail call optimisation (if the engine supports it) 使用return可以进行尾部调用优化(如果引擎支持的话)
  • Cache the inner function instead of re-creating the closure with the same fn over and over again. 缓存内部函数,而不是一遍又一遍地使用相同的fn重新创建闭包。

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. 因此,我希望为您和我们着想,您确实需要缩短这200ms的时间,因为这对我而言似乎不是一个现实生活中的问题。 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. 但是,在这种情况下,不是最大的问题就是递归,而是要创建的2n数组。 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. 现在,ES6具有适当的尾部调用,而Node6已实现了此功能。 It runs about 400 times faster than your original code when I test it there. 当我在那里测试时,它的运行速度比原始代码快400倍。 Not that you will notice the change IRL. 并不是说您会注意到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) 由于您似乎愿意重构代码,因此这是一个非常生动的重写,它可以以大约10倍的速度(使用基准测试方法)执行相同的任务

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'); 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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