【问题标题】:JavaScript `reduce` performanceJavaScript `降低`性能
【发布时间】:2020-06-02 22:30:12
【问题描述】:

我最近花了一些时间研究传感器(函数式编程中的工具,旨在提高性能而不损失代码的可读性/灵活性),当我开始测试它们的实际速度时,我得到了一些非常令人失望的结果。考虑:

const inc = x => x + 1;

const isEven = x => x % 2 === 0;

// simplest, shortest way I would be comfortable with if performance wasn't an issue

const mapFilter = xs => xs.filter(isEven).map(inc);

// transducers way

// function composition
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);

const map = f => step => (a, c) => step(a, f(c));
const filter = p => step => (a, c) => (p(c) ? step(a, c) : a);

// basic reducer for building array
const build = (acc, x) => {
  acc.push(x);
  return acc;
};

// transducer, it doesn't create intermediate arrays hence should theoretically be faster 
const transducers = xs =>
  xs.reduce(compose(filter(isEven), map(inc))(build), []);

// native loop for comparison
const nativeLoop = data => {
  const result = [];
  const l = data.length;
  for (let i = 0; i < l; i++) {
    const x = data[i];
    if (isEven(x)) result.push(inc(x));
  }
  return result;
};

const data = Array(1000).fill(1);

const base = ["simplest, chained map and filter", () => mapFilter(data)];
const alternative = ["composed transducers", () => transducers(data)];
const alternative2 = ["native loop", () => nativeLoop(data)];

/* console.log(Benchmark) */
console.log("Running benchmarks....");

const suite = new Benchmark.Suite();
suite
  .add(...base)
  .add(...alternative)
  .add(...alternative2)
  .on("cycle", function(event) {
  console.log(String(event.target));
})
  .on("complete", function() {
  console.log("Fastest is " + this.filter("fastest").map("name").join(", "));
})
// run async
  .run({ async: true });
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>

我希望表演的顺序是

本机循环 > 传感器 > 链式映射/过滤器

同时,除了比其他任何方法都快得多的原生方法之外,令我惊讶的是,reduce/transduce 方法比使用 map/filter 和创建中间数组要慢得多(更慢,就像 Chrome 中的一个数量级)。有人可以向我解释一下这个结果的由来吗?

【问题讨论】:

  • 如果您对微优化感兴趣,请坚持使用原生循环。没有错。
  • 我的猜测是内置的 Array#mapArray#filter 等等都是经过大量优化的本机代码,但传感器会遭受大量函数调用开销。必须在符号表中查找函数,创建堆栈帧,推送参数,跳转,返回结果......如果性能很重要,我同意 bob——坚持原生和幼稚的代码。即使性能不重要,转换器对我来说似乎也不可读,尽管我不是 Schemer。 const mapFilter = xs =&gt; xs.filter(isEven).map(inc); 是惯用的、直接的和不聪明的。
  • 顺便说一句,将build 替换为const build = (acc, x) =&gt; { return [...acc, x]; }; 使其成为纯函数。
  • @MichałKaczanowicz 你是对的。只是我厌倦了所有这些关于 FP 的性能相关问题。但是你的具体情况不属于这一类。我的错。

标签: javascript functional-programming reduce transducer


【解决方案1】:

基准有缺陷。 reducer 不需要做任何工作。

  • 创建一个奇数为 1 的统一数组。
  • 然后对每个元素运行 isEven 函数
  • 总是会返回一个空数组

我们正在对返回空数组的性能进行基准测试。

如果我们用真实数据预填充一个数组,本机方法将获胜。 Aadit 是正确的,他的传感器是两个传感器实现中最快的。

const data = [];

for (let i = 0; i < 1000; i++) {
    data.push(Math.floor(Math.random() * 10));
}

【讨论】:

    【解决方案2】:

    您的基准测试是错误的,因为您在每次运行时都构建了一个新的传感器链。

    const inc = x => x + 1;
    
    const isEven = x => x % 2 === 0;
    
    // simplest, shortest way I would be comfortable with if performance wasn't an issue
    
    const mapFilter = xs => xs.filter(isEven).map(inc);
    
    // transducers way
    
    // function composition
    const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
    
    const map = f => step => (a, c) => step(a, f(c));
    const filter = p => step => (a, c) => (p(c) ? step(a, c) : a);
    
    // basic reducer for building array
    const build = (acc, x) => {
      acc.push(x);
      return acc;
    };
    
    // transducer, it doesn't create intermediate arrays hence should theoretically be faster
    const reducer = compose(filter(isEven), map(inc))(build);
    const transducers = xs => xs.reduce(reducer, []);
    
    // native loop for comparison
    const nativeLoop = data => {
      const result = [];
      const l = data.length;
      for (let i = 0; i < l; i++) {
        const x = data[i];
        if (isEven(x)) result.push(inc(x));
      }
      return result;
    };
    
    const data = Array(1000).fill(1);
    
    const base = ["simplest, chained map and filter", () => mapFilter(data)];
    const alternative = ["composed transducers", () => transducers(data)];
    const alternative2 = ["native loop", () => nativeLoop(data)];
    
    /* console.log(Benchmark) */
    console.log("Running benchmarks....");
    
    const suite = new Benchmark.Suite();
    suite
      .add(...base)
      .add(...alternative)
      .add(...alternative2)
      .on("cycle", function(event) {
      console.log(String(event.target));
    })
      .on("complete", function() {
      console.log("Fastest is " + this.filter("fastest").map("name").join(", "));
    })
    // run async
      .run({ async: true });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>

    如您所见,转换器确实比链接的 mapfilter 方法更快。

    【讨论】:

    • 天啊,我不会认为这几个简单的调用来构建reducer 会对性能产生如此大的影响。谢谢!
    猜你喜欢
    • 1970-01-01
    • 2016-02-25
    • 1970-01-01
    • 1970-01-01
    • 2012-09-21
    • 2015-07-01
    • 2019-07-06
    • 2019-11-15
    • 2018-12-12
    相关资源
    最近更新 更多