为什么ArrayList严重优于LinkedList?

时间:2018-04-06 12:04:21

标签: java performance list

我做了一些研究并撰写了以下文章:http://www.heavyweightsoftware.com/blog/linkedlist-vs-arraylist/并想在这里发帖提问。

class ListPerformanceSpec extends Specification {
    def "Throwaway"() {
        given: "A Linked List"
        List<Integer> list
        List<Integer> results = new LinkedList<>()

        when: "Adding numbers"
        Random random = new Random()
        //test each list 100 times
        for (int ix = 0; ix < 100; ++ix) {
            list = new LinkedList<>()
            LocalDateTime start = LocalDateTime.now()

            for (int jx = 0; jx < 100000; ++jx) {
                list.add(random.nextInt())
            }

            LocalDateTime end = LocalDateTime.now()
            long diff = start.until(end, ChronoUnit.MILLIS)
            results.add(diff)
        }

        then: "Should be equal"
        true
    }

    def "Linked list"() {
        given: "A Linked List"
        List<Integer> list
        List<Integer> results = new LinkedList<>()

        when: "Adding numbers"
        Random random = new Random()
        //test each list 100 times
        for (int ix = 0; ix < 100; ++ix) {
            list = new LinkedList<>()
            LocalDateTime start = LocalDateTime.now()

            for (int jx = 0; jx < 100000; ++jx) {
                list.add(random.nextInt())
            }

            long total = 0

            for (int jx = 0; jx < 10000; ++jx) {
                for (Integer num : list) {
                    total += num
                }
                total = 0
            }

            LocalDateTime end = LocalDateTime.now()
            long diff = start.until(end, ChronoUnit.MILLIS)
            results.add(diff)
        }

        then: "Should be equal"
        System.out.println("Linked list:" + results.toString())
        true
    }

    def "Array list"() {
        given: "A Linked List"
        List<Integer> list
        List<Integer> results = new LinkedList<>()

        when: "Adding numbers"
        Random random = new Random()
        //test each list 100 times
        for (int ix = 0; ix < 100; ++ix) {
            list = new ArrayList<>()
            LocalDateTime start = LocalDateTime.now()

            for (int jx = 0; jx < 100000; ++jx) {
                list.add(random.nextInt())
            }

            long total = 0

            for (int jx = 0; jx < 10000; ++jx) {
                for (Integer num : list) {
                    total += num
                }
                total = 0
            }

            LocalDateTime end = LocalDateTime.now()
            long diff = start.until(end, ChronoUnit.MILLIS)
            results.add(diff)
        }

        then: "Should be equal"
        System.out.println("Array list:" + results.toString())
        true
    }

}

为什么当LinkedList应该更快时,ArrayList的顺序访问比LinkedList高出28%?

我的问题与When to use LinkedList over ArrayList?不同,因为我不会问何时选择它,而是为什么它会更快。

4 个答案:

答案 0 :(得分:4)

基于数组的列表,如Java ArrayList,与基于链接的列表(LinkedList)相比,使用相同数据量的内存要少得多,并且此内存按顺序组织。这实质上减少了CPU缓存与侧面数据的混乱。一旦访问RAM需要比L1 / L2缓存访问多10-20倍的延迟,这就会造成足够的时间差异。

您可以在this one等书籍或类似资源中阅读有关这些缓存问题的更多信息。

OTOH,基于链接的列表在操作中优于基于数组的列表,如插入列表中间或删除列表。

对于同时具有内存经济性(快速迭代)和快速插入/删除的解决方案,应该考虑组合方法,如内存中的B⁺树,或者按比例增加大小的数组列表。< / p>

答案 1 :(得分:3)

  

为什么当LinkedList应该更快时,ArrayList的顺序访问比LinkedList高出28%?

您可以假设,但不提供任何支持。但这并不是一个惊喜。 ArrayList有一个数组作为底层数据存储。按顺序访问它非常快,因为您确切知道每个元素的确切位置。当阵列增长超过一定大小并需要扩展时,唯一的减速就会出现,但这可以进行优化。

真正的答案可能是:检查Java源代码,并比较ArrayListLinkedList的实现。

答案 2 :(得分:3)

来自LinkedList源代码:

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

来自ArrayList源代码:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

 private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

因此,链表必须为添加的每个元素创建新节点,而数组列表则不会。 ArrayList不会为每个新元素重新分配/调整大小,因此大多数时间数组列表只是在数组中设置对象并增加大小,而链表则可以做更多工作。

您还评论道:

  

当我在大学写一个链表时,我一次分配了一些块,然后将它们整理出来。

我认为这不适用于Java。 你不能在Java中做指针技巧,所以你必须分配很多小数组,或者提前创建空节点。在这两种情况下,开销可能会高一些。

答案 3 :(得分:2)

一种解释是你的基本假设(乘法比内存提取慢)是值得怀疑的。

基于this document,AMD推土机需要1个时钟周期来执行64位整数乘法指​​令(寄存器x寄存器),其中6个周期的延迟 1 。相比之下,注册负载的存储器需要1个时钟周期,具有4个周期的延迟。但是这假设您获得了内存提取的缓存命中。如果出现缓存未命中,则需要添加多个周期。 (根据this source,L2缓存未命中20个时钟周期。)

现在这只是一个架构,其他架构会有所不同。我们还需要考虑其他问题,例如可以重叠的乘法数量的约束,以及编译器如何组织指令以使它们最小化指令依赖性。但事实仍然是,对于典型的现代流水线芯片架构,CPU可以执行整数倍,执行内存以执行寄存移动,如果内存提取中有更多缓存未命中,则更快

您的基准测试使用的是包含100,000 Integer个元素的列表。当您查看所涉及的内存量以及表示列表和元素的堆节点的相对位置时,链接列表案例将使用更多的内存,并且相应地具有更差的内存局部性。这将导致内循环的每个周期更多的缓存未命中,并且性能更差。

您的基准测试结果对我来说并不奇怪 2

另一点需要注意的是,如果使用Java LinkedList,则使用单独的堆节点来表示列表节点。如果元素类具有可用于链接元素的自己的next字段,则可以更有效地实现自己的链接列表。但是,带来了自己的局限;例如一个元素一次只能在一个列表中。

最后,正如@maaartinus指出的那样,在Java ArrayList的情况下不需要完整的IMUL。当读取或写入ArrayList的数组时,索引乘法将是x 4或x 8,并且可以由具有标准寻址模式之一的MOV执行; e.g。

MOV EAX, [EDX + EBX*4 + 8]

这种乘法可以通过以比64位IMUL小得多的延迟进行移位(在硬件级别)。

1 - 在此上下文中,延迟是指令结果可用之前的延迟周期数...到取决于它的下一条指令。诀窍是订购指令,以便在延迟期间完成其他工作。

2 - 如果有的话,我很惊讶LinkedList似乎做得很好。也许调用Random.nextInt()并自动装箱结果会占据循环次数?