首页 > web前端 > js教程 > JavaScript 中的循环展开?

JavaScript 中的循环展开?

王林
发布: 2024-07-24 13:18:52
原创
982 人浏览过

Loop Unrolling in JavaScript?

JavaScript 可能会让人感觉与其运行的硬件非常脱离,但低级思考在有限的情况下仍然有用。

Kafeel Ahmad 最近发表的关于循环优化的文章详细介绍了许多循环性能改进技术。那篇文章让我思考了这个话题。

过早的优化

为了解决这个问题,这是一种很少有人在 Web 开发中需要考虑的技术。此外,过早关注优化可能会使代码更难编写、更难维护。了解底层技术可以让我们深入了解我们的工具和一般工作,即使我们无法直接应用这些知识。

什么是循环展开?

循环展开基本上复制了循环内的逻辑,因此您可以在每个循环期间执行多个操作。在特定情况下,使循环中的代码更长可以使其更快

通过有意分组而不是逐一执行某些操作,计算机可能能够更有效地运行。

展开示例

让我们举一个非常简单的例子:对数组中的值求和。

// 1-to-1 looping
const simpleSum = (data) => {
  let sum = 0;
  for(let i=0; i < data.length; i += 1) {
    sum += data[i];
  }
  return sum;
};

const parallelSum = (data) => {
  let sum1 = 0;
  let sum2 = 0;
  for(let i=0; i < data.length; i += 2) {
    sum1 += data[i];
    sum2 += data[i + 1];
  }
  return sum1 + sum2;
};
登录后复制

乍一看这可能看起来很奇怪。我们正在管理更多变量并执行简单示例中不会发生的其他操作。这怎么可能更快?!

衡量差异

我对各种数据大小和多次运行以及顺序或交错测试进行了一些比较。 parallelSum 的性能各不相同,但几乎总是更好,除了非常小的数据大小的一些奇怪结果之外。我使用 RunJS 对此进行了测试,它基于 Chrome 的 V8 引擎构建。

不同的数据大小给出了非常粗略的这些结果:

  • 小(<10k):几乎没有任何差异
  • 中(10k-100k):通常快约 20-80%
  • 大(> 1M):始终快两倍

然后我创建了一个包含 100 万条记录的 JSPerf 来尝试跨不同的浏览器。自己尝试一下吧!

Chrome 运行 parallelSum 的速度是 simpleSum 的两倍,正如 RunJS 测试所预期的那样。

Safari 在百分比和每秒操作数方面几乎与 Chrome 相同。

同一系统上的 Firefox 对于 simpleSum 的性能几乎相同,但 parallelSum 只快了 15% 左右,而不是快两倍。

这种变化让我寻找更多信息。虽然这还不是确定的,但我发现了 2016 年的 StackOverflow 评论,讨论了循环展开的一些 JS 引擎问题。这是对引擎和优化如何以我们意想不到的方式影响代码的有趣观察。

变化

我也尝试了第三个版本,它在一次操作中添加了两个值,以查看一个变量和两个变量之间是否存在明显差异。

const parallelSum = (data) => {
  let sum = 0
  for(let i=0; i < data.length; i += 2) {
    sum += data[i] + data[i + 1];
  }
  return sum;
};
登录后复制

简短回答:不。两个“并行”版本在彼此报告的误差范围内。

那么,它是如何运作的?

虽然 JavaScript 是单线程的,但是当满足某些条件时,底层的解释器、编译器和硬件可以为我们执行优化。

在简单的示例中,操作需要 i 值来知道要获取哪些数据,并且需要更新 sum 的最新值。由于这两者在每个循环中都会发生变化,因此计算机必须等待循环完成才能获取更多数据。虽然 i += 1 的作用对我们来说似乎是显而易见的,但计算机大多理解“值会改变,稍后再检查”,因此它很难优化。

我们的并行版本为 i 的每个值加载多个数据条目。我们仍然依赖于每个循环的总和,但每个周期我们可以加载和处理两倍的数据。但这并不意味着它的运行速度两倍

更深层次的潜水

为了理解为什么循环展开有效,我们研究计算机的低级操作。具有超标量架构的处理器可以有多个管道来执行同时操作。它们可以支持无序执行,因此彼此不依赖的操作可以尽快发生。对于某些操作,SIMD 可以同时对多条数据执行一项操作。除此之外,我们开始讨论缓存、数据获取和分支预测......

但这是一篇 JavaScript 文章!我们不会走得那么深。如果您想了解有关处理器架构的更多信息,Anandtech 有一些出色的深度潜水。

限制和缺点

循环展开并不是魔法。由于程序或数据大小、操作复杂性、计算机体系结构等原因,会出现限制和收益递减。但我们只测试了一两个操作,现代计算机通常支持四个或更多线程。

为了尝试一些更大的增量,我制作了另一个包含 1、2、4 和 10 条记录的 JSPerf,并在运行 macOS 14.5 Sonoma 的 Apple M1 Max MacBook Pro 和运行 Windows 11 的 AMD Ryzen 9 3950X PC 上运行。

一次 10 条记录比基本循环快 2.5-3.5 倍,但仅比在 Mac 上处理 4 条记录快 12-15%。在 PC 上,我们仍然看到 1 到 2 条记录之间的性能提高了 2 倍,但 10 条记录仅比 4 条记录快 2%,这对于 16 核处理器来说是我无法预测的。

平台和更新

这些不同的结果提醒我们要小心优化。针对计算机进行优化可能会在功能较差或不同的硬件上带来更糟糕的体验。当开发人员在快速、强大的机器上工作时,较旧或入门级硬件的性能或功能问题是一个常见问题,而且我在职业生涯中多次遇到过这个问题。

对于某些性能规模,HP 目前提供的入门级 Chromebook 配备 Intel Celeron N4120 处理器。这大致相当于我的 2013 Core i5-4250U MacBook Air。在综合基准测试中,它的性能仅为 M1 Max 的九分之一。在运行最新版本 Chrome 的 2013 款 MacBook Air 上,4 记录功能比 10 记录功能快,但仍然只比单记录功能快 60%!

浏览器和标准也在不断变化。例行的浏览器更新或不同的处理器架构可能会使优化的代码比常规循环慢。当您发现自己进行深度优化时,您可能需要确保您的优化与消费者相关,并且保持相关性

这让我想起了 Nicholas Zakas 写的《高性能 JavaScript》一书,我在 2012 年读过这本书。这是一本很棒的书,包含了很多见解。然而,到 2014 年,书中指出的许多重大性能问题已通过浏览器引擎更新得到解决或大幅减少,我们能够将更多精力集中在编写可维护的代码上。

如果您想保持性能优化的领先地位,请为更改和定期验证做好准备。

过去的教训

在研究这个主题时,我遇到了 2000 年的 Linux 内核邮件列表主题,内容涉及删除一些循环展开优化,最终提高了应用程序性能。它包括这个仍然相关的点(强调我的):

最重要的是,我们对什么快、什么慢的直观假设常常是错误的,特别是考虑到过去几年 CPU 发生了多大的变化。
– Theodore Ts'o

结论

有时您可能需要从循环中挤出性能,如果您正在处理足够的项目,这可能是您这样做的方法之一。了解此类优化固然很好,但对于大多数工作来说,您并不需要它™。

不过,我希望您喜欢我的漫谈,并且也许将来您会记住有关性能优化注意事项的信息。

感谢您的阅读!

以上是JavaScript 中的循环展开?的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板