【问题标题】:Javascript Performance: While vs For LoopsJavascript 性能:While vs For 循环
【发布时间】:2013-09-09 12:09:31
【问题描述】:

前几天在一次技术面试中,被问到的一个问题是“如何优化 Javascript 代码”?

令我惊讶的是,他告诉我 while 循环通常比 for 循环快。

这是真的吗?如果是,为什么会这样?

【问题讨论】:

  • 正确的答案应该是,担心这些细节通常是没有意义的,因为在下次签入 V8 或 SpiderMonkey 时,您为此类优化所做的任何努力都可能完全浪费。
  • jsperf.com/while-vs-for 可能很有趣。
  • @Pointy:我就是这么说的。类似于 C/C++ 编译器,它们将以相同的方式优化 ++i 和 i++ 循环。
  • 据我所知,目前在 JavaScript 中,for 循环被编译为与 while 几乎相同的 expr1; while (expr2) { /* ... */ expr3;} 形式,所以如果你的 @ 987654325@ 循环设法减少了其中一个步骤,它可能会更快,但通常不会,或者差异很小,不值得担心。
  • 这取决于使用的 JS 引擎,所以是的,不是的。找出答案的唯一方法是测试您正在优化的特定引擎,此时您确实解决了错误的问题。

标签: javascript optimization


【解决方案1】:

您应该反驳说,否定的while 循环会更快!请参阅:JavaScript loop performance - Why is to decrement the iterator toward 0 faster than incrementing

whilefor 中,这两个来源通过在不同的浏览器中运行各种循环并以毫秒为单位比较结果,很好地记录了速度现象: https://blogs.oracle.com/greimer/entry/best_way_to_code_a 和: http://www.stoimen.com/blog/2012/01/24/javascript-performance-for-vs-while/.

从概念上讲,for 循环基本上是一个封装的while 循环,专门用于递增或递减(根据某种顺序或某种长度在逻辑上进行)。例如,

for (let k = 0; ++k; k < 20) {…}

可以通过将其设为负 while 循环来加速:

var k = 20;
while (--k) {…}

从上面链接中的测量结果可以看出,节省的时间确实加起来非常大。

【讨论】:

  • 这篇文章也很有意思:incredible-web.com/blog/…
  • 我使用while(--k) {} 得到了一些不好的结果,但实际上我比使用while(k &gt; 0) { -- k; } 的Chrome 中的for 循环结果更快。在jsben.ch 中设置快速测试以亲自查看。
  • “可以通过将其设为负 while 循环来加速” - 这取决于程序在运行时使用的解释器类型。当定量逻辑不是我想要的时,我只是使用一个 while 循环。甚至您发送的 SO 问题也在“过去”的一个答案中说。现在的编译器和解释器要聪明得多。
【解决方案2】:

虽然这是对速度和效率的分钟检测的一个很好的答案,但我不得不离题到 @Pointy 原始声明。

正确的答案是它通常是没有意义的 担心这样的细节,因为你付出的任何努力 下次签入时,优化可能会完全浪费 到 V8 或 SpiderMonkey

由于 Javascript 是由客户端确定的,并且最初必须针对每个浏览器进行编码以实现完全的跨浏览器兼容性(在 ECMA 参与之前,情况更糟),因此速度差异在这一点上甚至可能不是一个合乎逻辑的答案,因为Javascript 在浏览器及其编译器引擎上的显着优化和采用。

我们甚至没有谈论非严格的脚本编写,例如 GAS 中的应用程序,因此虽然答案和问题很有趣,但它们在现实世界的应用程序中很可能是微不足道的而不是有用的。

要阐述这个主题,您首先需要了解这个主题的最初来源以及编译与解释。让我们简要回顾一下语言的演变历史,然后再回到编译与解释。虽然不是必读,但您可以阅读 Compiling vs Interpeting 以获得快速答案,但为了深入了解,我建议您通读 Compiling vs Interpreting 和 the Evolution of Programming(展示它们今天的应用方式)。

编译与解释

编译语言编码是一种编程方法,您可以在其中以编译器理解的可编译方式编写代码,当今一些更受认可的语言是 Java、C++ 和 C#。编写这些语言的目的是让编译器程序将代码翻译成目标机器使用的机器码或字节码。

解释代码
是在执行时处理 Just In Time (JIT) 而不先编译的代码,它会跳过这一步并允许更快的编写、调试、添加/更改等。它也永远不会存储脚本的解释以供将来使用,它会在每次调用方法时重新解释脚本。解释的代码在定义的和预期的程序运行时环境(对于 javascript 通常是浏览器)中运行,一旦被环境解释,就会输出到所需的结果。解释脚本绝不意味着是独立软件,并且总是希望插入有效的运行时环境以进行解释。这就是脚本不可执行的原因。他们永远不会直接与操作系统通信。如果您查看正在发生的系统进程,您将永远不会看到正在处理您的脚本,而是会看到正在处理的程序正在其运行时环境中处理您的脚本。

因此,用 Javascript 编写 hello 脚本意味着浏览器会解释代码,定义 hello 是什么,当发生这种情况时,浏览器会将这段代码转换回机器级代码,说我有这个脚本并且我的环境想要显示单词你好,然后机器将其处理成脚本的可视化表示。这是一个恒定的过程,这就是为什么您在计算机中拥有 处理器 以及在系统上发生的一个恒定的处理动作。没有什么是一成不变的,无论情况如何,流程都在不断地执行。

编译器
通常将代码编译成定义的字节码系统或机器代码语言,现在是代码的静态版本。除非重新编译源代码,否则机器不会重新解释它。这就是为什么您会在编译后看到运行时错误,然后程序员必须在源代码中调试并重新编译。解释器预期的脚本(如 Javascript 或 PHP)只是在运行之前未编译的指令,因此源代码很容易编辑和修复,无需额外的编译步骤,因为编译是实时完成的。

并非所有编译后的代码都是一样的
一个简单的说明方法是视频游戏系统。 Playstation 与 Xbox。 Xbox 系统是为支持 .net 框架而构建的,以优化编码和开发。 C# 将此框架与公共语言运行时结合使用,以便将代码编译为字节码。字节码不是对已编译代码的严格定义,它是过程中的一个中间步骤,它允许更快地编写代码并在更大范围内为程序编写代码,然后在运行时使用代码执行时对其进行解释,你猜对了, Just In Ttime (JIT)。不同之处在于该代码仅被解释一次,一旦编译,程序将不会再次重新解释该代码,除非重新启动。

解释性脚本语言永远不会编译代码,因此解释性脚本中的函数会不断地被重新处理,而编译后的字节码的函数会被解释一次,并且指令会被存储,直到程序的运行时停止。好处是字节码可以移植到另一台机器的架构中,前提是您拥有必要的资源。这就是为什么您必须在系统中安装 .net 以及可能的更新和框架才能使程序正常运行。

Playstation 的机器不使用 .net 框架。您将需要使用 C++ 编写代码,C++ 旨在针对特定系统架构进行编译和组装。代码永远不会被解释,并且需要完全正确才能运行。您永远无法像中间语言那样轻松移动这种类型的语言。它是专门为该机器的架构设计的,永远不会被解释为其他方式。

因此,您会发现即使编译语言本身也不是编译语言的最终最终版本。编译语言在其严格定义中意味着完全编译以供使用。解释性语言旨在由程序解释,但也是编程中最可移植的语言,因为只需要安装一个理解脚本的程序,但由于不断被解释,它们也使用最多的资源。中间语言(如 Java 和 C#)是这两种语言的混合体,部分编译但也需要外部资源才能正常运行。一旦运行,它们就会再次编译,这是在运行时的一次性解释。

编程的演变

机器码
编码的最低形式,此代码在其表示中是严格的二进制(我不会进入三进制计算,因为它基于此讨论的理论和实际应用)。计算机理解自然值,开/关真/假。这是机器级的数字代码,不同于下一级,汇编代码。

汇编代码
直接下一级代码是汇编语言。这是将语言解释为由机器使用的第一点。此代码旨在解释助记符、符号和操作数,然后以机器级代码发送到机器。理解这一点很重要,因为当您第一次开始编程时,大多数人都会假设它是 thisthat 意味着我编译或解释。 除了低级机器代码之外,没有任何编码语言要么只编译指令,要么只解释指令!!!

我们在“并非所有编译的代码都是平等的”中讨论了这一点。汇编语言是第一个例子。机器代码是机器可以读取的,而汇编语言是人类可以读取的。随着计算机处理速度更快,通过更好的技术进步,我们的低级语言在本质上开始变得更加简洁,不需要手动实现。汇编语言曾经是高级编码语言,因为它是对机器进行编码的更快方法。它本质上是一种语法语言,一旦汇编(编译的最低形式)直接转换为机器语言。 汇编器是编译器,但并非所有编译器都是汇编器。

高级编码
高级编码语言是比汇编高一级但甚至可能包含更高级别的语言(这将是字节码/中间语言)。这些语言从那里定义的语法结构编译成所需的机器代码、要解释的字节码,或者是前一种方法与允许内联编写汇编的特殊编译器相结合的混合体。像它的前身 Assembly 一样的高级编码旨在减少开发人员的工作量并消除冗余任务(如构建可执行程序)中出现严重错误的任何机会。在当今世界中,您很少会看到开发人员在汇编中工作只是为了获得大小的好处而处理数据。开发人员通常会遇到一种情况,例如在视频游戏机开发中,他们需要在此过程中提高速度。因为高级编码编译器是寻求简化开发过程的工具,所以它们可能无法 100% 地以最有效的方式编译代码以用于该系统架构。在这种情况下,将编写汇编代码以最大限度地利用系统资源。但是你永远不会看到有人用机器码写东西,除非你遇到一个怪人。

总结

如果你做到了这一步,恭喜!你只是坐下来听了比我妻子听得更多的关于这件事的事,一辈子。 OP 的问题是关于 while 与 for 循环的性能。在今天的标准中,这是一个有争议的问题的原因有两个。

原因一
解释 Javascript 的日子已经一去不复返了。所有主要浏览器(是的,甚至 Opera 和 Netscape)都使用 Javascript 引擎,该引擎用于在执行脚本之前对其进行编译。 JS 开发人员在查看语言中的本机函数时,就非调用方法讨论的性能调整是过时的研究方法。在成为 DOM 的一部分之前,代码已经为此进行了编译和优化。该页面启动时不会再次解释它,因为该页面是运行时环境。 Javascript 已经真正成为一种中间语言,而不是解释脚本。它永远不会被称为中间脚本语言的原因是因为 Javascript 永远不会被编译。这是唯一的原因。除此之外,它在浏览器环境中的功能是 Bytecode 的缩小版本。

原因二 您编写脚本或脚本库的机会几乎为零,因为它需要与网站上的桌面应用程序一样多的处理能力。为什么?因为 Javascript 的创建从未打算成为一种包罗万象的语言。它的创建只是为了提供一种中级语言编程方法,允许完成 HTML 和 CSS 不提供的流程,同时缓解需要专用高级编码语言,特别是 Java 的开发困难。

Web 开发早期的大部分时间都不支持 CSS 和 JS。直到 1997 年左右,CSS 还不是一个安全的集成,而 JS 的战斗时间更长。除了 HTML 之外的所有内容都是网络世界中的一种补充语言。

HTML 专门用于作为网站的构建块。您永远不会编写 javascript 来完全构建网站。最多你会做 DOM 操作,但要建立一个网站。

您永远不会在 JS 中为您的网站设置样式,因为它不实用。 CSS 处理该过程。

您永远不会使用 Javascript 进行存储,除了临时存储。你会使用数据库。

那么我们还剩下什么?越来越只是功能和流程。 CSS3 及其未来的迭代将采用 Javascript 的所有样式方法。您已经通过动画和伪状态(悬停、活动等)看到了这一点。

此时,在 Javascript 中优化代码的唯一有效论据是编写糟糕的函数、方法和操作,这些函数、方法和操作可以通过优化用户的公式/代码模式得到帮助。只要您学习正确且高效的编码模式,Javascript 在当今时代就不会损失其原生函数的性能。

【讨论】:

  • +1。如果你不相信这个答案,你应该看this video
  • @John 我希望你没有发表评论。让我回去写一篇关于这个主题的文章,因为我觉得信息量不够:)顺便说一句很棒的视频链接。
  • @BrianEllis 我敢打赌是你的妻子给了你反对票。哈哈。很好读。
  • 您的预测不正确,我认为 2015 年的预测大多是错误的。 2014 年,我在一家绝对猛烈抨击竞争对手的公司工作:主要是因为我们在服务器之外还利用客户端机器(通过 javascript)来提供显着更好的性能和功能。许多用例之一是利用 canvas 和 webgl 进行图像处理和高效显示快速更新的视觉信息。还有node.js?和同构?如果您正在制作一个小型内容显示应用程序,那么我可能会同意您的价值判断。
【解决方案3】:
for(var k=0; ++k; k< 20){ ... }

可以通过将其设为负数来加速 while 循环:

var k = 20; while(--k){ ... };

更准确的测试是使用 forwhile 的程度相同。唯一的区别是使用 for 循环提供了更多描述。如果我们想变得超级疯狂,我们可以放弃整个街区;

var k = 0;
for(;;){doStuff till break}
//or we could do everything
for (var i=1, d=i*2, f=Math.pow(d, i); f < 1E9; i++, d=i*2, f=Math.pow(d,i)){console.log(f)}

无论如何...在 NodeJS v0.10.38 中,我在四分之一秒内处理了 109 的 JavaScript 循环,for 平均快了约 13% .但这对我未来决定使用哪个循环或我选择在循环中描述的数量确实没有影响。

> t=Date.now();i=1E9;
> while(i){--i;b=i+1}console.log(Date.now()-t);
292
> t=Date.now();i=1E9;
> while(--i){b=i+1}console.log(Date.now()-t);
285
> t=Date.now();i=1E9;
> for(;i>0;--i){b=i+1}console.log(Date.now()-t);
265
> t=Date.now();i=1E9;
> for(;i>0;){--i;b=i+1}console.log(Date.now()-t);
246 

【讨论】:

    【解决方案4】:

    2016 年答案

    在 JavaScript 中,reverse for 循环 是最快的。 For 循环一般比 while 循环快。更加关注可读性

    Here is some bench marking.

    测试的以下循环:

    var i,
      len = 100000,
      lenRev = len - 1;
    
    i = 0;
    while (i < len) {
        1 + 1;
        i += 1;
    }
    
    i = lenRev;
    while (-1 < i) {
        1 + 1;
        i -= 1;
    }
    
    for (i = 0; i < len; i += 1) {
        1 + 1;
    }
    
    for (i = lenRev; - 1 < i; i -= 1) {
        1 + 1;
    }
    

    【讨论】:

    • 不考虑i = len; while (i--),我不认为这是一个完整的比较。
    【解决方案5】:

    2017 年答案

    jsperf for vs foreach on Chrome 59

    在这里您可以看到Array.forEach 在截至撰写日期 (7/31/17) 的最新版 Chrome (59) 上已成为最快的。您可以在此处找到其他浏览器版本的平均时间:https://jsperf.com/for-vs-foreach/66

    这证明了ES引擎优化随时改变什么更高效。

    我的建议是您使用对您的用例更具表现力的那个。

    随着计算机按照摩尔定律呈指数级增长,未来相同幅度内的性能差异将基本无关紧要。

    【讨论】:

      猜你喜欢
      • 2012-10-02
      • 1970-01-01
      • 2012-01-01
      • 2017-07-12
      • 2010-11-13
      • 1970-01-01
      相关资源
      最近更新 更多