java: List.contains() 与手动搜索的性能差异

Posted

技术标签:

【中文标题】java: List.contains() 与手动搜索的性能差异【英文标题】:java: List.contains() performance difference with manual searching 【发布时间】:2013-12-25 06:12:42 【问题描述】:

我试图演示List.contains() 和手动搜索执行时间之间的区别,结果非常棒。这是代码,

public static void main(String argv[]) 
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("a");
    list.add("a");
    list.add("a");
    list.add("a");
    list.add("a");
    list.add("b");

    long startTime = System.nanoTime();

    list.contains("b");

    long endTime = System.nanoTime();
    long duration = endTime - startTime;

    System.out.println("First run: "+duration);

    startTime = System.nanoTime();
    for(String s: list)
        if(s.equals("b"))
            break;
    
    endTime = System.nanoTime();

    duration = endTime - startTime;
    System.out.println("Second run: "+duration);


输出:

首次运行:7500

第二次运行:158685

    contains() 函数如何产生如此大的差异?

    它使用哪种搜索算法?

    如果列表包含搜索到的元素,它会在第一个元素处终止搜索吗?

【问题讨论】:

这种性能差异让我感到惊讶。在 Java 中,ArrayList#contains() 调用 ArrayList#indexOf(),它只是遍历支持列表的数组。因此,据我所知,性能差异应该很小。您是否使用更大的列表进行了测试?也许时差是一个恒定的时差,与列表的大小无关? 不,实际上我没有。大是指多大。喜欢 1000 个元素? 我认为torrestomp给出的答案正是我想说的。是的,包含 1000 个元素的数组应该足够了。您可以从一堆文本中创建随机-ish 长字符串数组,然后对每个文本进行比较。比如说,20 个这样的测试的整理结果应该是满足个人好奇心的一个不错的基准:-) 【参考方案1】:

首先,相信来自这样一个单一测试的结果是不明智的。有太多可变因素、需要考虑的缓存影响以及其他类似的事情 - 您应该考虑编写一个在某种程度上使用随机化试验的测试,并执行数百万次不同的检查,而不仅仅是一次。

也就是说,我希望您的结果保持不变; ArrayList 使用自己的 indexOf() 方法实现 contains(),该方法直接在它存储的底层数组上循环。你可以自己看看这个here

另一方面,foreach 循环需要实例化一个Iterator,通过它的所有方法访问数组,而且通常比ArrayList 自己的直接实现做的工作要多得多。不过,您应该再次对其进行更彻底的基准测试!

【讨论】:

很好的解释:),那么我的问题有点毫无意义。因为 contains 除了迭代之外没有其他内容。【参考方案2】:

写correct microbenchmark 很难。如果您使用更好的基准测试,您可能会发现两种方法之间的差异很小 - 至少,以下基准测试更加稳健,并且两种方法之间的执行时间仅相差 10%:

public abstract class Benchmark 

    final String name;

    public Benchmark(String name) 
        this.name = name;
    

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() 
        try 
            int nextI = 1;
            int i;
            long duration;
            do 
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
             while (duration < 1000000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
         catch (Throwable e) 
            throw new RuntimeException(e);
        
    

    @Override
    public String toString() 
        return name + "\t" + time() + " ns";
    

    public static void main(String[] args) throws Exception 
        final List<String> list = new ArrayList<String>();
        for (int i = 0; i < 1000; i++) 
            list.add("a");
        

        Benchmark[] marks = 
            new Benchmark("contains") 
                @Override
                int run(int iterations) throws Throwable 
                    for (int i = 0; i < iterations; i++) 
                        if (list.contains("b")) 
                            return 1;
                        
                    
                    return 0;
                
            ,
            new Benchmark("loop") 
                @Override
                int run(int iterations) throws Throwable 
                    for (int i = 0; i < iterations; i++) 
                        for (String s : list) 
                            if (s.equals("b")) 
                                return 1;
                            
                        
                    
                    return 0;
                
            
        ;

        for (Benchmark mark : marks) 
            System.out.println(mark);
        
    

打印(在我过时的笔记本上,在服务器模式下的 Java 7 Oracle JVM 上):

contains    10150.420 ns
loop        11363.640 ns

循环稍大的开销很可能是迭代器检查并发修改和每次访问时检查列表末尾两次造成的,详见java.util.ArrayList.Itr.next()的源代码。

编辑:对于非常短的列表,差异更加明显。例如长度为 1 的列表:

contains    15.316 ns
loop        69.401 ns

不过,您的测量值表明的比例远不及 20:1 ...

【讨论】:

【参考方案3】:

从code 可以看出contains 需要O(n) 次迭代。如果您将for 循环重新实现为:

for(int i=0; i < list.size(); i++)
    if(list.get(i).equals("b"))
        break;

您会发现搜索时间显着缩短。因此,您可以将时间开销归咎于 List 迭代器。 Iterator 实例化和对 nexthasNext 方法的调用增加了一些毫秒。

【讨论】:

对不起,是的。你是对的,事实上确实如此。但是两个结果之间仍然存在差距 是的,有一个差距,但只有几毫秒。这是因为热身等原因。

以上是关于java: List.contains() 与手动搜索的性能差异的主要内容,如果未能解决你的问题,请参考以下文章

Java List.contains(ArrayList<String> 字段值等于 x)

Java-List元素判断

Java-List元素判断

java 判断元素是否在数组内

list.contains方法既然是调用equ 方法 还用重写 hashcod吗

list.contains