【问题标题】:How does java.util.Collections.contains() perform faster than a linear search?java.util.Collections.contains() 如何比线性搜索执行得更快?
【发布时间】:2012-10-10 17:20:52
【问题描述】:

我一直在用一堆不同的方法来搜索集合、集合集合等。做了很多愚蠢的小测试来验证我的理解。这是一个让我感到困惑的(源代码在下面)。

简而言之,我正在生成 N 个随机整数并将它们添加到列表中。该列表未排序。然后我使用Collections.contains() 在列表中查找一个值。我有意寻找一个我知道不会存在的值,因为我想确保探测到整个列表空间。我为这个搜索计时。

然后我手动进行另一个线性搜索,遍历列表的每个元素并检查它是否与我的目标匹配。我也为这个搜索计时。

平均而言,第二次搜索的时间比第一次长 33%。按照我的逻辑,第一次搜索也必须是线性的,因为列表是未排序的。我能想到的唯一可能性(我立即放弃)是 Java 正在制作我的列表的排序副本只是为了搜索,但是(1)我没有授权使用内存空间和(2)我认为使用如此大的 N 会节省更多时间。

因此,如果两个搜索都是线性的,则它们应该花费相同的时间。 Collections 类以某种方式优化了此搜索,但我不知道如何。那么……我错过了什么?

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (Integer i : ints) {
            // nothing
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}

编辑:以下是此代码的新版本。有趣的是,现在我的手动线性循环比contains 方法执行了 16% faster(注意:两者都旨在有意搜索整个列表空间,所以我知道它们的数量相等迭代)。我无法解释这 16% 的收益……更多的困惑。

import java.util.*;

public class ListSearch {

    public static void main(String[] args) {

        int N = 10000000; // number of ints to add to the list
        int high = 100; // upper limit for random int generation

        List<Integer> ints;
        int target = -1; // target will not be found, forces search of entire list space

        long start;
        long end;

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 1)... ");
        if (ints.contains(target)) {
            System.out.println("hit");
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");

        System.out.println();

        ints = new ArrayList<Integer>();
        start = System.currentTimeMillis();
        System.out.print("Generating new list... ");
        for (int i = 0; i < N; i++) {
            ints.add(((int) (Math.random() * high)) + 1);
        }
        end = System.currentTimeMillis();
        System.out.println("took "  + (end-start) + "ms.");
        start = System.currentTimeMillis();
        System.out.print("Searching list for target (method 2)... ");
        for (int i = 0; i < N; i++) {
            if (ints.get(i) == target) {
                System.out.println("hit");
            }
        }
        end = System.currentTimeMillis();
        System.out.println(" Took "  + (end-start) + "ms.");
    }
}

【问题讨论】:

  • 您是否意识到您的第二个“搜索”甚至不是搜索?它只是迭代列表的元素...
  • 是的,实际上我几分钟前才意识到这一点,现在我正在更多地使用我的代码。对于那个很抱歉。可能需要尽快运行,但稍后会更新此帖子。

标签: java collections linear-search


【解决方案1】:

您的比较代码有问题,这会扭曲您的结果。

这确实搜索target

    if (ints.contains(target)) {
        // nothing
    }

但事实并非如此!

    for (Integer i : ints) {
        // nothing
    }

您实际上只是在不测试列表元素的情况下迭代它们。

话虽如此,由于以下一个或多个原因,第二个版本比第一个版本慢:

  • 第一个版本将使用简单的for 循环和索引来迭代后备数组。第二个版本等价于:

    Iterator<Integer> it = ints.iterator();
    while (it.hasNext()) {
        Integer i = (Integer) it.next();
    }
    

    换句话说,每次循环都涉及 2 次方法调用和一次类型转换1

  • 第一个版本将在匹配后立即返回 true。由于您的实现中的错误,第二个版本每次都会迭代 整个 列表。事实上,考虑到Nhigh 的选择,这种影响很可能是导致性能差异的主要原因

1 - 实际上,JIT 编译器将如何处理所有这些并不完全清楚。它可以理论上内联方法调用,推断不需要 typcaset,甚至优化整个循环。另一方面,有一些因素可能会抑制这些优化。例如,ints 被声明为 List&lt;Integer&gt;,这可能会禁止内联...除非 JIT 能够推断出实际类型始终相同。


您的结果也可能由于其他原因而失真。您的代码没有考虑 JVM 预热。阅读此问题了解更多详情:How do I write a correct micro-benchmark in Java?

【讨论】:

  • 谢谢斯蒂芬。关于您的第二个项目符号,您是正确的,我忽略了在第二种情况下进行实际搜索,但是您可能错过了我说我每次都故意尝试迭代 整个 列表的部分。为了进一步实现这一目标,我正在寻找一个我知道不会找到的值。我已经更新了我的帖子以包含第二个程序。在这个方法中,手动线性搜索(整个列表)比contains 方法(也搜索整个列表)执行 16%。你能解释一下这个新发现吗?再次感谢。
  • 一个原因是您使用== 而不是.equals() 来比较这些值。那是不可靠的……它会因大整数而中断。而且您仍然没有解决 JVM 预热问题,这无论如何都会使您的结果令人怀疑。
  • 我没有在这里展示它,但我做的一件事是将整个生成/搜索块包装在一个大的重复循环中,以强制每个块运行多次。我确实注意到,在前几次运行之后,报告的时间稳定下来并且不再发生太大变化。不确定这是否算作 JVM 预热。但是,我只是将== 更改为.equals(),果然这大大减慢了搜索速度,现在contains 版本再次表现出色。
【解决方案2】:

这里有区别:

当您使用contains 时,它使用对象的内部数组并进行如下搜索:

    for (int i = 0; i < size; i++)
        if (searchObject.equals(listObject[i]))
            return true;
    return false;

这里尝试获取ith元素时,直接从内部数组中获取第i个元素对象。

当你自己写的时候,你是这样写的:

    for (Integer i : ints) {
        // nothing
    }

相当于:

   for(Iterator<Integer> iter= ints.iterator(); iter.hasNext(); ) {
         Integer i = iter.next();
   }

执行的步骤比contains 多得多。

【讨论】:

  • 谢谢,这听起来可信且有道理。如果您不介意我的询问,它记录在哪里?
  • The111:大多数时候,我指的是源代码本身 :) 您可能想要打开任何集合实现的源代码,例如ArrayList 并检查方法。
  • 不好意思承认我从来没有考虑过。谢谢。
  • 这不适用于他的案件,因为他没有在他的申请中进行任何验证。
  • +1 对您关于仅检查来源的评论,但 -1 对答案。您提出的循环扩展没有意义,增强的 for 循环扩展为 Iterator,在未知数据结构上使用 get 来迭代它是非常不明智的。
【解决方案3】:

所以我不完全确定您正在测试任何东西。 Javac(编译器)足够聪明,可以意识到您的 for 循环和 if 语句中没有任何代码。在这种情况下,Java 将从其编译中删除该代码。您可能会获得时间回来的原因是因为您实际上是在计算打印字符串所需的时间。系统输出时间可能会因系统正在执行的操作而有很大差异。编写时序测试时,任何 I/O 都可能创建无效测试。

首先,我将从您的计时中删除字符串打印。

其次,ArrayList.contains 是线性的。它不像你正在做的那样使用特殊的 for 循环。您的循环有一些从集合中获取迭代器然后对其进行迭代的开销。这就是特殊 for 循环在幕后工作的方式。

希望这会有所帮助。

【讨论】:

  • 谢谢。这是有趣的信息。但在这种情况下,我认为编译器不会删除循环,因为在早期的测试中,我在那里做了一个简单的操作,而这些测试也出现了类似的时间。另外,刚才我把所有的打印行都注释掉了,看起来程序运行起来仍然需要同样的时间(当然,这大约是半秒,所以很难估计......但如果它真的在做什么都不是,它会是瞬间的,而不是一秒钟的一小部分)。
猜你喜欢
  • 2018-12-19
  • 2016-01-30
  • 1970-01-01
  • 1970-01-01
  • 2014-03-20
  • 1970-01-01
  • 2016-09-09
  • 1970-01-01
  • 2016-02-02
相关资源
最近更新 更多