这个for循环会重复多次吗?

时间:2018-12-31 19:38:34

标签: javascript performance

我一直在与同事讨论一些代码:

for(const a of arr) {
  if(a.thing)
    continue;
  // do a thing
}

一个建议是对此进行过滤并使用forEach

arr.filter(a => !a.thing)
  .forEach(a => /* do a thing */);

进行了有关重复不必要的讨论。我查了一下,找不到任何东西。我还试图弄清楚如何查看优化的输出,但是我也不知道该怎么做。

我希望filterforEach会变成非常类似于for ofcontinue的代码,但我不知道该怎么做当然。

如何找到?到目前为止,我唯一尝试过的就是Google。

3 个答案:

答案 0 :(得分:6)

您的第一个示例(for in循环)是O(n),它将执行n次(n是数组的大小)。

您的第二个示例(Each的过滤器)为O(n + m),它将在过滤器中执行n次(n为数组的大小),然后执行m次(m为结果数组的大小)过滤后)。

因此,第一个示例更快。但是,在这种类型的示例中,如果没有太大的样本集,则差异可能以微秒或纳秒为单位。

关于编译优化,本质上是 all 内存访问优化。主要的解释器和引擎都将分析与功能,变量和属性访问有关的代码中的问题,例如访问图的频率和形状如何;然后,利用所有这些信息,优化其隐藏结构,以提高访问效率。就代码的循环替换或过程分析而言,基本上没有优化,因为它大部分是在 运行时进行了优化(如果代码的特定部分确实开始花费了很长时间,可能会对其代码进行优化)。

  

第一次执行JavaScript代码时,V8利用了完整的代码生成器,该代码生成器将已解析的JavaScript直接转换为机器代码,而无需进行任何转换。这使它可以非常快速地开始执行机器代码。请注意,V8不会以这种方式使用中间字节码表示,从而无需解释器。

     

代码运行了一段时间后,探查器线程已经收集了足够的数据来告诉您应该对哪种方法进行优化。

     

接下来,曲轴优化从另一个线程开始。它将JavaScript抽象语法树转换为称为Hydrogen的高级静态单一分配(SSA)表示形式,并尝试优化该Hydrogen图。大多数优化都在此级别完成。
  -https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e

* 虽然 continue 可能导致执行转到下一个迭代,但仍算作一个循环的迭代

答案 1 :(得分:3)

正确的答案是“这真的没关系”。先前发布的一些答案指出第二种方法是O(n + m),但我希望有所不同。同样的精确“ m”操作也将在第一种方法中运行。在最坏的情况下,即使您将第二批操作都视为“ m”(这实际上没有多大意义,我们正在谈论作为输入提供的相同n个元素,这不是复杂度分析的工作原理),最坏的情况是m == n,复杂度将是O(2n),无论如何最终还是O(n)。

要直接回答您的问题,是的,第二种方法将对集合进行两次迭代,而第一种则仅进行一次。但这可能对您没有任何影响。在这种情况下,您可能希望提高效率的可读性。您的收藏有几件? 10点100?最好编写一段随时间而维护的代码,而不是一直追求最高效率,因为在大多数情况下,代码没有任何区别。

此外,多次迭代并不意味着您的代码运行速度较慢。这与每个循环中的内容有关。例如:

for (const item of arr) {
  // do A
  // do B
}

实际上与以下内容相同:

for (const item of arr) {
  // do A
}
for (const item of arr) {
  // do B
}

for循环本身不会给CPU增加任何可观的开销。尽管您可能仍然想编写一个循环,但是如果在执行两个循环时提高了代码的可读性,请继续进行操作。


效率就是选择正确的算法

如果您确实需要提高效率,那么您就不想遍历整个集合,甚至一次都不需要。您需要一种更聪明的方法来实现:除以征服(O(log n))或使用哈希映射(O(1))。每天使用散列图可以避免效率低下:-)


只做一次

现在,回到您的示例,如果我发现自己一次又一次地迭代并执行相同的操作,那么在开始时,我只会运行一次过滤操作:

// during initialization

const things = [];
const notThings = [];

for (const item of arr) {
    item.thing ? things.push(item) : notThings.push(item);
}

// now every time you need to iterate through the items...

for (const a of notThings) {  // replaced arr with notThings
    // if (a.thing)  // <- no need to check this anymore
    //    continue;

    // do a thing
}

然后,您就可以自由地遍历notThings,知道不需要的项目已经被过滤掉了。有道理吗?


对“ for of的批评要比调用方法快”

有些人喜欢声明for of总是比呼叫forEach()更快。我们只是不能这样说。有很多Java语言解释器,每个解释器都有不同的版本,每个版本都有其优化事物的特殊方式。为了证明我的观点,我能够在MacOS Mojave上的Node.js v10中使filter() + forEach()的运行速度比for of快:

const COLLECTION_SIZE = 10000;
const RUNS = 10000;
const collection = Array.from(Array(COLLECTION_SIZE), (e, i) => i);

function forOf() {
    for (const item of collection) {
        if (item % 2 === 0) {
            continue;
        }
        // do something
    }
}

function filterForEach() {
    collection
        .filter(item => item % 2 === 0)
        .forEach(item => { /* do something */ });
}

const fns = [forOf, filterForEach];

function timed(fn) {
    if (!fn.times) fn.times = [];

    const i = fn.times.length;
    fn.times[i] = process.hrtime.bigint();
    fn();
    fn.times[i] = process.hrtime.bigint() - fn.times[i];
}

for (let r = 0; r < RUNS; r++) {
    for (const fn of fns) {
        timed(fn);
    }
}

for (const fn of fns) {
    const times = fn.times;
    times.sort((a, b) => a - b);
    const median = times[Math.floor(times.length / 2)];
    const name = fn.constructor.name;
    console.info(`${name}: ${median}`);
}

时间(以纳秒为单位):

forOf: 81704
filterForEach: 32709

for of在我进行的所有测试中始终较慢,始终慢50%左右。这就是这个答案的重点:不要相信每个解释器的实现细节,因为随着时间的推移,它会(并且将会)改变。除非您是为嵌入式或高效/低延迟系统而开发的(除非您需要与硬件尽可能接近),否则请首先了解您的算法复杂性。

答案 2 :(得分:1)

查看该语句的每个部分被调用多少次的简单方法是像这样添加日志语句并在Chrome控制台中运行

var arr = [1,2,3,4];
arr.filter(a => {console.log("hit1") ;return a%2 != 0;})
   .forEach(a => {console.log("hit2")});

在这种情况下,“ Hit1”应打印到控制台四次。如果要迭代太多次,我们将看到4次“ h​​it2”输出,但是运行此代码后,它只会输出两次。因此,您的假设是部分正确的,即第二次迭代不会遍历整个集合。但是,它会在.filter中对整个集合进行一次迭代,然后再次在.filter

中对与条件匹配的部分进行迭代。

MDN开发人员文档here中特别值得一看的地方是“ Polyfill”部分,其中概述了精确的等效算法,您可以看到.filter()在这里返回变量{{1} },将执行res

因此,虽然总体上对集合进行了两次迭代,但在.forEach部分中,它仅对与.forEach条件匹配的集合部分进行了迭代。