为什么迭代通过LinkedList缓慢?

时间:2016-08-05 09:00:47

标签: java performance list

Java 8

我有一个很大的集合(大约100M元素),我需要迭代它并做一些动作。有两种选择:

  1. 迭代一遍并完成整个工作(这会使代码复杂化)

  2. 在第一次迭代中迭代两次并完成一半的工作,在第二次迭代中休息(这将显着简化代码)

  3. 所以,我认为迭代不是那么昂贵,并写了一个简单的例子来衡量它(我不经常写基准,因此看起来有点傻):

        Collection<Double> col = new LinkedList<>();
        for(int i = 0; i < 30000000; i++){
            col.add(Math.sqrt(i + 1));
        }
        long start1 = System.nanoTime();
        Double res = 0.0;
        for(Double d : col){
            res += d + d;
        }
        long end1 = System.nanoTime();
        System.out.println(end1 - start1);
        System.out.println("=================================");
        long start2 = System.nanoTime();
        Double res2 = 0.0;
        for(Double d : col){
            res2 += d;
        }
        for(Double d : col){
            res2 += d;
        }
        long end2 = System.nanoTime();
        System.out.println(end2 - start2);
    

    平均结果如下:

    1107881047首先

    2133450162秒(慢两倍)

    因此,迭代是一个非常缓慢的过程。但我不明白为什么?我认为我们做的工作几乎相同,所以表现会有很大不同。

    值得注意的是,如果我使用ArrayList而不是链表,结果是:

    3858616604首先

    422297749秒(比第一个快10倍,比上例更快两倍)。

    难道你不能简单地解释一下这种性能差异吗?

4 个答案:

答案 0 :(得分:5)

首先,对于基准测试,您可以更好地使用JMH tool

现在,对原来的问题。迭代ArrayList时,基本上对数组执行顺序扫描,该数组是一个连续的内存块。 CPU可以完美地预取从主内存到CPU缓存的内容。因此它非常快。

如果是LinkedList,您必须通过对象引用从一个元素转到另一个元素。通常情况下,每个节点的对象可以驻留在内存中的任何位置。因此,您必须不断地从一个内存位置跳转到完全不相关的内存位置。 CPU无法预测,无法从主内存中获取数据。因此,您一直在等待数据。

答案 1 :(得分:3)

我认为您看到的性能影响有几个原因。

  1. 自动拳击
  2. 算法
  3. 自动拳击

    每次你采用原始类型(例如intfloat等)并将其放入等价的类中(即IntegerFloat等),原始值被装箱。当您需要将其用作基元时,需要将其取消装箱。这里的要点是需要CPU周期和时间才能完成此任务。

    This SO post可以提供有关自动装箱的更多详细信息。

    算法

    简单来说,您使用的只在列表上迭代一次的算法是O(n),而您使用的需要双重迭代的实现现在是O(2n)。两者仍然是O(n),但是要明白Big-O是渐近的上界,而不是衡量“表现”。应该仍然清楚的是,在同一列表上迭代两次将只需要在列表上迭代一次所需的工作量的两倍。

    算法和自动装箱结合起来产生了明显的效果,你可以在这样的代码中看到:

    for(int i = 0; i < reallyLargeNumberHere; i++){
        col.add(Math.sqrt(i + 1));
    }
    

    sqrt计算本身很昂贵,需要自动装箱才能添加每个元素。然而,对大型名单的双重迭代将成为主要罪魁祸首。

    简而言之

    1. 你正在迭代一个大集合,你在第二个实现中执行了两次;你现在必须支付两次O(n)罚款。
    2. 你遭受了自动拳击造成的打击。
    3. sqrt算法本身很慢。
    4. 添加/删除节点时链接列表实现为O(1),但迭代/搜索不是这样 - 它们往往是O(n)
    5. 链接列表对CPU缓存不友好,因为它们不是在连续的内存块中分配的,就像原始数组一样。
    6. 第5点是关于CPU缓存它认为即将使用的内容的能力,但是存储在不同内存区域的数据会使这项工作变得更加昂贵,并且不太可能按预期提高性能。 (当阅读有关CPU管道和缓存命中/未命中时,this SO post将是相关的。)

      了解您的程序大多数当时正在做什么非常重要,这样您就可以选择合适的数据结构和算法来匹配。匹配不当的数据结构和算法将使您轻松避免性能问题。

      Big-O cheat-sheet可能会有所帮助。

      最后,虽然为了您的简单目的,System.nanoTime的使用可能没问题,但如果您想要做更严肃的事情,则应考虑使用专门用于对代码进行基准测试的工具。

答案 2 :(得分:1)

有很多拆箱/装箱正在发生,这确实会影响性能。

但除此之外,您必须知道阵列和链接列表之间的区别。

在数组中,为数组分配了一个连续的内存块。因此可以随机访问元素。

因此,当我想要说出一个大数组的第5000个元素时,它会在瞬间返回它。

时间复杂度

访问元素= O(1)

但是在链表中,您无法随机访问元素。要访问元素,您需要遍历链接列表,直到所需元素到来。

因此,如果我访问第3个元素,内部就会发生这种情况 1-> 2-> 3,返回第3个元素。

第一个元素没有关于第3个元素的任何信息,它只有链接到第二个元素。

时间复杂度

访问元素= O(n)

当您决定使用何种类型的集合时,您需要了解数据结构和时间复杂性的概念。因为在大型数据集中,它可能会导致巨大的性能差异。

您可以参考此链接了解时间复杂性: http://bigocheatsheet.com/

答案 3 :(得分:1)

在第一次迭代时,它需要O(n),第二次迭代需要O(2n),因为我在你的代码中看到的是迭代整个列表两次而不是迭代两半。

//First:
for(Double d : col){
    res += d + d;
}

//Second
for(Double d : col){
    res2 += d;
}
for(Double d : col){
    res2 += d;
}

注意:即使您修改代码以在两次迭代中完成半部分,它也永远不会比单次迭代更快。它会更慢或相等。

为什么LinkedList的迭代很慢?

您必须根据自己的需要使用数据结构。例如LinkedList - &gt;它的插入和删除速度非常快,但迭代速度非常慢。

ArraylList更快吗?

是的,它更快,最快的数据结构是数组。因为,对于数组,要在索引x处获取项目,索引x处元素的地址计算为[[数组对象的地址]] + x * [[item_size_in_memory]](可能不是这个但更接近)。< / p>

你可以做什么?

1)您可以更新代码以使用数组而不是使用LinkedLists。正如我所见,你必须迭代非常大的元素,所以你必须以某种方式通过比LinkedLists更快地使用数组或数据结构来获得完成任务的速度。

2)尝试使用LinkedList迭代器。我没试过,但迭代器可能比正常迭代更快。例如:

LinkedList<String> linkedList = new LinkedList<String>();
linkedList.add("eBay");
linkedList.add("Paypal");
linkedList.add("Google");
linkedList.add("Yahoo");
linkedList.add("IBM");
linkedList.add("Facebook");
// ListIterator approach
System.out.println("ListIterator Approach: ");
ListIterator<String> listIterator = linkedList.listIterator();
while (listIterator.hasNext()) {
    System.out.println(listIterator.next());
}

注意:我从this link获取了样本。