【问题标题】:LinkedList vs ArrayList: get, add, remove performance comparison - unexpected results for add methodLinkedList vs ArrayList:get、add、remove性能比较——add方法的意外结果
【发布时间】:2021-02-06 09:34:16
【问题描述】:

据我所知,ArrayList 在获取时应该更快,在添加和删除时应该更慢。鉴于此 - 如果这段代码产生完全不同的结果,您能否给我一个提示:删除时间对于 ArrayList 和 LinkedList 几乎相同,而对于 ArrayList,添加速度更快...

import java.util.ArrayList;
import java.util.LinkedList;

public class ListsTest {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        LinkedList linkedList = new LinkedList();

        // ArrayList add
        long startTime = System.nanoTime();

        for (int i = 0; i < 100000; i++) {
            arrayList.add(i);
        }
        long endTime = System.nanoTime();
        long duration = endTime - startTime;
        System.out.println(" ArrayList add: " + duration);

        // LinkedList add
        startTime = System.nanoTime();

        for (int i = 0; i < 100000; i++) {
            linkedList.add(i);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println("LinkedList add: " + duration);

        // ArrayList get
        startTime = System.nanoTime();

        for (int i = 0; i < 10000; i++) {
            arrayList.get(i);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println(" ArrayList get: " + duration);

        // LinkedList get
        startTime = System.nanoTime();

        for (int i = 0; i < 10000; i++) {
            linkedList.get(i);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println("LinkedList get: " + duration);


        // ArrayList remove
        startTime = System.nanoTime();

        for (int i = 9999; i >=0; i--) {
            arrayList.remove(i);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println(" ArrayList remove: " + duration);

        // LinkedList remove
        startTime = System.nanoTime();

        for (int i = 9999; i >=0; i--) {
            linkedList.remove(i);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println("LinkedList remove: " + duration);
    }
}

我的结果:

 ArrayList add: 13540332
LinkedList add: 93488785
 ArrayList get: 676937
LinkedList get: 335366109
 ArrayList remove: 529793354
LinkedList remove: 410813052

编辑: 正如在一些 cmets 中提到的,重要的是我们是否添加/删除/获取到/从列表的末尾或我们是否使用随机索引。使用随机索引时,所有结果都是“正确的”:

arrayList.get((int) (Math.random()*arrayList.size()));

同样的问题在这里得到解决: https://coderanch.com/t/669483/certification/ArrayList-LinkedList-speed-comparison

【问题讨论】:

  • Java 中的微基准测试确实很难准确进行,原因有很多,例如 JVM 是一个黑匣子,JIT 编译部分代码而其他部分不编译,等等等等。例如,您一个接一个地进行测试这一事实可能已经影响了结果
  • 添加到 ArrayList 的末尾非常快,因为不需要复制现有条目,除非偶尔调整列表大小几次。当您向列表的开头移动时,添加速度会变慢。

标签: java


【解决方案1】:

技术答案:

从顶层的角度来看,您的代码看起来相当不错,并且会被大多数Java 开发人员天真地用于编写一些(微)基准测试。

事实上,Java 中的(微)基准测试是一件非常棘手的事情,因为JVM 是一个大黑匣子,而且JIT 正在对您的代码进行大量优化 - 即使在执行时也是如此它! - Java的美女与野兽。

例如:

Java 是 - 或曾经 - 被认为是一种非常慢的语言,因为它使用了解释器/JIT。这些只会在实际执行之前的片刻读取和编译您的指令。事实上,这仅适用于前 ~10.000 次执行(默认值)。当Java 检测到代码和平执行超过 10.000 次时,它会对其进行更深入的分析并“预编译”它。然后在下一次执行而不是再次解释它时,使用预编译的代码来更快地执行它。

因此它的名称为"Hotspot"-JVM - 它会识别代码的“热点”并相应地对其进行优化(以便在代码执行期间动态执行分析,而不仅仅是静态代码分析的前期)。顺便说一句:这个阈值可以通过 JVM 的-XX:CompileThreshold= 选项来控制。

第二件事是,Java 非常适合阅读您的代码并查找未使用或无效的指令。当Java 注意到它们时,它会重组您的代码以提高效率,甚至会从代码库中完全删除不必要的指令。

这只是JavaJVM 如何优化代码的两个示例,还有更多!太多了,无法在此处列出所有内容,但是那里有很多很棒的资源可以参考并详细解释它(one short overview is linked here)。还要注意使用的技术和优化的成功,因此不同的JVMvendors(Oracle、IBM、SAP、Red Hat,...)之间也有所不同。

因此,要在Java 中编写无偏见的微基准测试,JMH (Java Microbenchmark Harness) 项目存在并且应该用于此目的。它为您提供了许多关于JVM 优化工作的见解,以及如何编写测试来衡量它们的影响(您可以使用JMH 提供的特殊说明来控制其中一些) - 或对您的测试进行基准测试代码不受任何优化的影响。

此技术基准测试还取决于执行代码的实际机器。

一切都很重要:

  • 操作系统(操作系统)
  • Java 版本
  • 正在使用 JVM(供应商)
  • JVM 设置
  • JVM的内存分配过程
  • 垃圾收集器 (GC) 的调用
  • 已安装的硬件和驱动程序(CPU、RAM、HDD/SSD)
  • 操作系统和后台活动的当前负载(Spotify/Netflix 在后台运行?)
  • 还有更多...

JMH 还包含方法和策略(如“JVM 预热运行”)来减少这种独特的负载峰值,以获得公正的结果 - 但总会对测量产生影响。 em>

因此,无法在 2 台不同的机器之间比较具体的基准测试结果 - 但它们会大致了解代码的性能如何工作(特别是在单台机器上比较 2 种不同的算法时 - 就像你的情况一样)。


理论答案:

正如您已经提到的,有 Big-O 符号来分析和评估算法性能。这将查看算法的结构并根据它评估其性能 - 忽略上面技术答案中描述的困难,得出“定性”而非“绝对”估值。它将查看最佳平均最坏情况,然后据此进行评级。

根据Big-O 模型,您已经确定了ArrayList 相对于LinkedList 的理论优势和优势。但是就像您一样,您也注意到数据结构(因此实现的算法)的性能取决于它们使用的真实数据 - 无论它更多地反映了最佳、平均还是最坏的情况。

例如,在您的微基准测试中,您反映的更像是最佳情况,其中元素仅被添加到列表的末尾(以及从那里删除而不是从中间删除 -> ArrayList 不需要重新排列它的内部数组以避免其中的间隙)。如果您更改代码/基准以将元素添加到列表的头部(或中间的某个位置),ArrayList 的性能也会急剧下降。

因此,这种最佳、平均和最差情况可以被视为在JMH 中作为单独的基准来实现,如答案的技术部分中所述,以便可以比较每种情况的性能。

但是,如果您确定数据的外观并且可以非常准确地指定它,那么如果基准也能反映您的真实数据,那么它们将是最有价值的。如果您无法指定它,您可以实现所有场景以获得总体概览。


编辑:

这当然只是算法的执行观点 - 看起来你只对它感兴趣。

为了完成答案:我只想提一下,还有可能查看算法/数据结构的内存消耗。如果这是您需要考虑的要求,也可能值得一看。例如,它也已经是discussed here,当您对它感兴趣时,它会为您提供一个很好的概览。

【讨论】:

    【解决方案2】:

    Arraylist 只是一个数组 [x1,x2,x3,x4,null,null,null,null] .. 它必须有一定的大小

    • on add O(1) .. arraylist 添加到列表末尾确实是 O(1) 操作,但是由于数组必须具有特定的大小,所以不断添加它会导致不时构建新数组更大的尺寸,你的元素将被复制到这个新数组中,平均为 O(N/2) 操作,然后添加到列表的末尾将再次成为 O(1)。

    这是一个很好的做法,如果你知道你的数组列表的大小,你应该用这个大小初始化数组列表,这样添加到列表的末尾将是 O(1) 操作。效率更高,速度更快

    • on get O(1) .. 因为数组可以通过索引直接到达它的元素

    • 从索引 O(N) 中删除时.. 构建一个新数组并将元素复制到新数组中,忽略要删除的元素

    链表是一个节点序列,每个节点都有一个指向下一个元素和前一个元素的指针(双链表),一个节点只是一个有值的类,指向下一个节点的指针,指向前一个节点的指针

    • 要添加到特定索引 O(N) ,您将从第一个节点开始并指向下一个节点,依此类推,直到找到您的索引,然后它将使用您的值创建一个节点并将其挂钩通过设置其指针来获得所需的链表索引

    • 要获得 O(N),您将从第一个节点开始,并循环遍历节点直到到达您的索引

    • 要从特定索引中删除 O(N),因为您将遍历节点直到到达您的节点,然后您将完全删除该节点并更改其兄弟节点的指针

    如您所见,您在代码上完成的所有操作都是 O(N),除了 arraylist 的 get 是 O(1),这就是为什么它们的所有时间都彼此接近,除了 get Arraylist 是低时间间隔

    【讨论】:

    • OP 对 Arraylist 的添加是 O(1),而不是 O(N),因为它们添加到列表的末尾。
    • 是的,但在他的代码中,他正在添加到特定索引。不是列表的结尾
    • 他正在添加一个特定的索引,结果证明它是列表的末尾 - O(N) 是因为平均而言,必须复制 N/2 个项目。但是在列表的末尾,不需要复制任何项目。在列表的开头添加速度较慢,在末尾添加速度更快。
    • 更正:我刚刚注意到您完全错误地添加到索引。没有为add 方法提供索引。该代码通过仅提供要添加的值以“附加”模式添加。 - 不过没关系。通过将列表中的下一个索引指定为插入位置来添加到列表的末尾将花费相同的时间。
    • 谢谢你刚刚注意到,我修改了我的答案
    猜你喜欢
    • 2011-12-16
    • 1970-01-01
    • 2013-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-21
    • 2014-07-28
    • 1970-01-01
    相关资源
    最近更新 更多