【问题标题】:Why is iterating through LinkedList slow?为什么迭代 LinkedList 很慢?
【发布时间】:2016-08-05 09:00:46
【问题描述】:

Java 8

我有一个相当大的集合(大约 1 亿个元素),我需要遍历它并执行一些操作。有两种选择:

  1. 遍历一次并完成整个工作(这会使代码变得非常复杂)

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

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

    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 第二个(比第一个快十倍,比上面的例子快两倍)。

您能不能简单地解释一下这种性能差异?

【问题讨论】:

  • 您的代码中有很多装箱/拆箱,这肯定会导致您遇到的性能下降。如果可能的话,你应该修改你的代码以避免这种情况。
  • @ray 是的,我知道。但问题不在于这一点。我担心 Array/LinkedList 的性能。
  • “我们做的工作量几乎相同”,第二个测试是做两倍的工作量略少于两倍的时间,所以它更快而不是更慢。你会期望它会更快一点,因为它已经预热了 LinkedList 代码。

标签: java performance list


【解决方案1】:

首先,对于基准测试,您最好使用JMH tool

现在,回到原来的问题。当您迭代ArrayList 时,您实际上是对一个数组进行顺序扫描,该数组是一个连续的内存块。 CPU 可以完美地完成从主内存到 CPU 缓存的预取。因此速度非常快。

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

【讨论】:

  • 很有趣,但能保证数组在内存中按顺序存储它们的元素吗?
  • 是的,每个数组都必须是连续的。
【解决方案2】:

我认为您所看到的性能下降有几个原因。

  1. 自动装箱
  2. 算法

自动装箱

每次您采用原始类型(例如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 缓存不友好,因为它们不像原始数组那样分配在连续的内存块中。

第 5 点是关于 CPU 缓存它认为您将要使用的内容的能力,但是位于不同内存区域的数据使这项工作更加昂贵,并且不太可能按预期提高性能。 (在阅读有关 CPU 管道和缓存命中/未命中的信息时,this SO post 将是相关的。)

了解您的程序大部分时间会做什么非常重要,这样您就可以选择合适的数据结构和算法来匹配。不匹配的数据结构和算法会给您带来容易避免的性能问题。

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

最后,虽然为了您的简单目的,使用 System.nanoTime 可能没问题,但如果您想做更严肃的事情,您应该考虑使用专为基准代码设计的工具。

【讨论】:

  • 不,第二个是 O(2n) 并且写入错误,整个列表被迭代两次,而不是一半。
  • @Stil_webmasterR:感谢 Big-O 的收获。那是一个错字,现在已修复。不过,我没有关注您的其余评论。
【解决方案3】:

正在进行很多拆箱/装箱,这确实会影响性能。

但除此之外你必须知道数组和链表的区别。

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

所以当我想得到一个大数组的第 5000 个元素时,它会立即返回它。

时间复杂度

访问元素 = O(1)

但是在链表中,你不能随机访问一个元素。要访问一个元素,您需要遍历链表,直到需要的元素出现。

所以如果我访问第三个元素,内部会发生这样的情况 1->2->3,返回第三个元素。

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

时间复杂度

访问元素 = O(n)

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

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

【讨论】:

    【解决方案4】:

    在第一次迭代中,它需要 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 --> 插入和删除非常快,但迭代非常慢。

    ArraylList 更快吗?

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

    你能做什么?

    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 获取样本。

    【讨论】:

      猜你喜欢
      • 2011-05-07
      • 2018-05-12
      • 1970-01-01
      • 2021-04-29
      • 2018-07-22
      • 2020-11-07
      • 1970-01-01
      • 1970-01-01
      • 2019-07-30
      相关资源
      最近更新 更多