【问题标题】:Why is looping through an Array so much faster than JavaScript's native `indexOf`?为什么循环遍历数组比 JavaScript 的原生 `indexOf` 快得多?
【发布时间】:2011-10-04 16:57:38
【问题描述】:

为什么循环遍历数组比 JavaScript 的原生 indexOf 快​​得多?是否有错误或我没有考虑到的东西?我预计原生实现会更快。

                For Loop        While Loop      indexOf
Chrome 10.0     50,948,997      111,272,979     12,807,549
Firefox 3.6     9,308,421       62,184,430      2,089,243
Opera 11.10     11,756,258      49,118,462      2,335,347   

http://jsben.ch/#/xm2BV

【问题讨论】:

  • 如果你用字符串来尝试,这个结果会变得更加可疑。我使用 while 循环运行了一些简单的测试 jsperf.com/indexof-vs-while-loop 可以随意改进测试,但在得出结论之前,值得考虑字符串比较甚至更大的数组。
  • Frug,你使用一个固定顺序的数组,称它为“随机”...相关XKCD xkcd.com/221
  • 表中的数字代表什么?这是完成操作所需的平均时间,还是在给定时间内完成操作的次数?还是别的什么?

标签: javascript performance


【解决方案1】:

5 年后,浏览器发生了很多变化。现在,indexOf 的性能有所提高,并且绝对优于任何其他自定义替代方案。

Chrome 版本 49.0.2623.87(64 位)

【讨论】:

  • 从而证明问题无效。
  • @a3.14_Infinity 已过时可能并非无效
  • @pierre-maouni 你用什么工具来得到这样的结果?还是你自己设计的?
  • 你能链接到你为此使用的测试吗?屏幕截图本身并没有显示实现,鉴于 jsperf.com/thor-indexof-vs-for> 的结果,我得到了截然不同的结果。
  • @agweber,这个测试在 indexOf 周围创建了一个“for 循环”,并将它与另一个包含一个带中断的 for 进行比较......我们在这里谈论的是 indexOf 本身与 for/while
【解决方案2】:

好的,看看这里的其他基准测试,我对大多数开发人员似乎进行基准测试的方式摸不着头脑。

抱歉,但是这样做的方式会导致非常错误的结论,所以我必须对所提供的答案进行一些元分析。

这里的其他基准有什么问题

测量在一个永远不会改变的数组中找到元素 777 的位置,总是导致索引 117 似乎很不合适,原因很明显,我很难解释原因。您无法从这样一个过于具体的基准中合理地推断出任何东西!我能想到的唯一类比是对一个人进行人类学研究,然后将这些发现称为对这个人所居住国家的整个文化的概括概述。其他基准不是好多了。

更糟糕的是:接受的答案是一张没有指向所使用基准的链接的图像,因此我们无法控制该基准的代码是否正确(我希望 这是一个 jsperf 链接的屏幕截图,该链接最初出现在问题中,后来被编辑掉以支持新的 jsben.ch 链接)。它甚至不是对原始问题的解释:为什么一个比另一个表现更好(首先是一个备受争议的陈述)。

首先,您应该知道not all benchmarking sites are created equal - 由于它们自己的框架会干扰计时,因此某些类型的测量可能会增加重大错误。

现在,我们应该比较对数组进行线性搜索的不同方法的性能。想一想算法本身:

  • 查看数组中给定索引的值。
  • 将该值与另一个值进行比较。
    • 如果相等,返回索引
    • 如果不相等,则移动到下一个索引并比较下一个值。

这就是整个线性搜索算法,对吧?

因此,一些链接基准比较了已排序和未排序的数组(有时被错误地标记为“随机”,尽管每次迭代的顺序相同 - relevant XKCD)。很明显,这不会以任何方式影响上述算法 - 比较运算符不会看到所有值都单调增加。

是的,在将 线性搜索二元interpolation search 算法的性能进行比较时,有序数组与未排序数组可能很重要,但没有人就是这样!

此外,显示的所有基准测试都使用一个固定长度的数组,其中有一个固定的索引。所有这一切都告诉您indexOf 以多快的速度找到该精确长度的精确索引 - 如上所述,您无法从中概括任何内容。

这是或多或少地将问题中链接的基准复制到 perf.zone(比 jsben.ch 更可靠)的结果,但进行了以下修改:

  • 我们每次运行都会选择数组的一个随机值,这意味着我们假设每个元素都像其他任何元素一样被选择
  • 我们针对 100 个和 1000 个元素进行基准测试
  • 我们比较整数和短字符串。

https://run.perf.zone/view/for-vs-while-vs-indexof-100-integers-1516292563568

https://run.perf.zone/view/for-vs-while-vs-indexof-1000-integers-1516292665740

https://run.perf.zone/view/for-vs-while-vs-indexof-100-strings-1516297821385

https://run.perf.zone/view/for-vs-while-vs-indexof-1000-strings-1516293164213

这是我机器上的结果:

https://imgur.com/a/fBWD9

如您所见,结果会因基准测试和所使用的浏览器而急剧发生变化,并且每个选项都至少在以下一种情况下胜出:缓存长度与非缓存长度,而循环 vs for 循环 vs indexOf.

所以这里没有通用的答案,随着浏览器和硬件的变化,这肯定会在未来发生变化。

您甚至应该对此进行基准测试吗?

应该注意的是,在开始构建基准之前,您应该首先确定线性搜索部分是否是瓶颈!可能不是,如果是,更好的策略可能是使用不同的数据结构来存储和检索数据,和/或不同的算法。

这并不是说这个问题无关紧要——它很少见,但线性搜索性能很重要可能发生;我碰巧有一个例子:通过嵌套对象(使用字典查找)或嵌套数组(需要线性搜索)构造的prefix trie 来确定构造/搜索的速度。

As can be seen this github comment,基准测试涉及各种浏览器和平台上的各种现实和最佳/最坏情况的有效载荷。只有在经历了所有这些之后,我才能得出关于预期性能的结论。在我的情况下,对于大多数实际情况,通过数组的线性搜索比字典查找更快,但最坏情况下的性能更差到冻结脚本(并且易于构建)的程度,因此实现被标记为一种“不安全”的方法,向其他人表明他们应该考虑使用代码的上下文。

Jon J's answer 也是退一步思考真正问题的好例子。

当你必须做微基准测试时该怎么办

所以让我们假设我们知道我们做了功课并确定我们需要优化我们的线性搜索。

那么重要的是我们期望找到元素的最终索引(如果有的话)、正在搜索的数据类型,当然还有要支持的浏览器。

换句话说:找到任何索引的可能性相同(均匀分布),还是更有可能以中间为中心(正态分布)?将在开始或接近结束时找到我们的数据?我们的值是否保证在数组中,或者只有一定百分比的时间?百分比是多少?

我是在搜索一个字符串数组吗?对象编号?如果它们是数字,它们是浮点值还是整数?我们是否正在尝试针对使用 IE10 的旧智能手机、最新笔记本电脑或学校台式机进行优化?

这是另一件重要的事情:不要针对最佳情况进行优化,而是针对现实的最坏情况性能进行优化。如果您正在构建一个 Web 应用程序,其中 10% 的客户使用非常旧的智能手机,请为此进行优化;他们的体验将是无法忍受的糟糕表现,而微优化则浪费在最新一代的旗舰手机上。

问自己这些问题,这些问题是关于您要应用线性搜索的数据以及您执行此操作的上下文。然后制作符合这些标准的测试用例,并在代表您支持的目标的浏览器/硬件上对其进行测试。

【讨论】:

    【解决方案3】:

    可能是因为实际的 indexOf 实现不仅仅是循环遍历数组。你可以在这里看到它的 Firefox 内部实现:

    https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf

    出于理智目的,有几件事可以减慢循环速度:

    • 数组正在重新转换为对象
    • fromIndex 正在转换为数字
    • 他们使用Math.max 而不是三元组
    • 他们正在使用Math.abs

    【讨论】:

    • 结果是惊人的......我的意思是甚至没有接近 0_0。无论如何将数组转换为对象?说真的,它已经是一个对象。那里的动机是什么?这似乎是一个边缘案例。
    • 仔细想想,while 循环成功地避免了需要Math.maxMath.abs。还有一点技巧,您可能可以添加fromIndex 支持。
    • 贴在上面的代码是一个polyfill,它不是实现中的实际代码。
    • @Barmar -- 很公平,但是 polyfill 尝试与原始实现非常相似,并且为了确定性能,通常是一个不错的参考。
    • @Barmar -- 这不是我要说的重点。无论具体机制如何,polyfill 都需要执行与内部实现相同的完整性检查。为此,它回答了“为什么原生较慢”的原始问题。
    【解决方案4】:

    indexOf 执行了一系列 for 循环和 while 循环忽略的类型检查和验证。

    这里是 indexOf 算法:

    https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf

    编辑:我的猜测是 indexOf 对于大数组来说更快,因为它在循环之前缓存了数组的长度。

    【讨论】:

    【解决方案5】:

    使用我所做的编辑再次运行测试。

    我增加了数组的大小,并使您要搜索的索引也更大。似乎在大型数组中 indexOf 可能是一个更快的选择。

    http://jsben.ch/#/xm2BV

    编辑:根据更多的测试,indexOf 似乎比我正在使用的 Safari 版本 (5.0.3) 中的 for 循环运行得更快,而在几乎所有其他版本中运行得更慢。

    【讨论】:

    【解决方案6】:

    可能值得注意的是,如果您要做的只是保留项目列表并检查是否存在(例如,避免将重复的 ID 添加到数组中),那么保留带有反映的键的 OBJECT 会快得多每个 ID。如果您认为我错了,请将以下内容与数组 + indexOf 进行比较。对象方法是 181 毫秒,而数组 indexOf 方法是 1 分钟。

    var objs = []
    var i_uid = {} // method 1
    var a_uid = [] // method 2
    var total_count = 100000, idLen = 5
    var ts, te, cObj = 0
    
    // method 1
    ts = new Date()
    while (cObj < total_count) {
        var u = uid(idLen),
            o = {
                uid: u,
                text: 'something',
                created: new Date()
            }
        if (!i_uid[u]) { // ensure unique uids only
            objs.push(o)
            i_uid[u] = cObj // current array position as placeholder
            cObj++
        }
        else {
            console.log('unique violation [duplicate uid', u, ']')
        }
    }
    te = new Date()
    console.log('loaded ' + total_count + ' with object method in', (te - ts), 'ms')
    
    i_uid = {} // free-up
    cObj = 0 // reset
    objs = [] // reset
    
    // method 2
    ts = new Date()
    while (cObj < total_count) {
        var u = uid(idLen),
            o = {
                uid: u,
                text: 'something',
                created: new Date()
            }
        if (a_uid.indexOf(u) == -1) { // ensure unique uids only
            objs.push(o)
            a_uid.push(u)
            cObj++
        }
        else {
            console.log('unique violation [duplicate uid', u, ']')
        }
    }
    te = new Date()
    console.log('loaded ' + total_count + ' with array + indexOf method in', (te - ts), 'ms')
    
    function uid(l) {
        var t = '',
            p = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
            pl = p.length
        for (var i = 0; i < l; i++)
            t += p.charAt(Math.floor(Math.random() * pl))
        return t
    }
    

    【讨论】:

      【解决方案7】:

      我们每次都可以信任一个普通的 for 循环。

      我编写了下面的代码来发现这个问题的答案。它也是可运行的。 它表明在考虑性能时,普通的for loop 是最好的解决方案。

      (代码也可以在jsfiddle找到)

      console.clear()
      
      let a = []
      // populating array data
      for (let i = 0; i < 100000; i++) {
      	a.push(i)
      }
      
      let testNum = 90000
      let found
      let totalMS4ForLoop = 0
      let totalMS4IndexOf = 0
      let start
      let end
      // simulating 10000 requests which are come consecutively
      for (o = 0; o < 10000; o++) {
      
        start = Date.now()
        for (let i = 0; i < a.length; i++) {
          if (a[i] == testNum) { found = a[i]; break }
        }
        end = Date.now()
        totalMS4ForLoop += end - start
      
        start = Date.now()
        found = a[a.indexOf(testNum)]
        end = Date.now()
        totalMS4IndexOf += end - start
      
      }
      
      console.log("10000 for-loop executions took total " + totalMS4ForLoop + " ms.")
      console.log("10000 indexOf executions took total " + totalMS4IndexOf + " ms.")

      【讨论】:

      • 对于我的笔记本电脑,10000 次 for 循环执行总共花费了 1719 毫秒。 10000 次 indexOf 执行共耗时 1995 毫秒。
      猜你喜欢
      • 1970-01-01
      • 2012-07-21
      • 1970-01-01
      • 2016-12-26
      • 2011-07-17
      • 2012-07-31
      • 1970-01-01
      • 2018-12-18
      相关资源
      最近更新 更多