【问题标题】:removeAll ArrayList vs LinkedList performanceremoveAll ArrayList vs LinkedList 性能
【发布时间】:2016-03-16 23:40:54
【问题描述】:

我对此计划有疑问。

public class Main {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<String>();
        for(int i=0; i<100; i++){
            arrayList.add("ValueA");
            arrayList.add("ValueB");
            arrayList.add(null);
            arrayList.add("ValueC");
            arrayList.add(null);
            arrayList.add(null);            
        }
        long startTime = System.nanoTime();
        arrayList.removeAll(Collections.singleton(null));
        long endTime = System.nanoTime();
        System.out.println("ArrayList removal took: " + (endTime - startTime) + "ms");          

        List<String> linkedList = new LinkedList<String>();
        for(int i=0; i<100; i++){
            linkedList.add("ValueA");
            linkedList.add("ValueB");
            linkedList.add(null);
            linkedList.add("ValueC");
            linkedList.add(null);
            linkedList.add(null);
        }

        startTime = System.nanoTime();
        linkedList.removeAll(Collections.singleton(null));
        endTime = System.nanoTime();
        System.out.println("LinkedList removal took: " + (endTime - startTime) + "ms");
    }
}

系统输出为:

ArrayList 删除耗时:377953ms
LinkedList 移除耗时:619807ms

为什么在 removeAll 上linkedList 比arrayList 花费更多时间?

【问题讨论】:

  • 你能给我们更多的数据点吗?特别是,我希望看到ArrayListLinkedList 的两条趋势线
  • 你为什么希望它更快?
  • 我不认为removeAll() 是一个特别好的基准,因为您只是在清空整个数据结构。更好的基准是删除随机元素。
  • @TimBiegeleisen,removeAll(Collection) 并没有真正清空整个 DS...而且他正在使用它...
  • 好点,我收回我说的话。删除上述代码中大小为 1000 的集合的第一个元素对于 ArrayListLinkedList 需要相同的时间,但删除中间元素对于后者比前者需要更多时间。

标签: java performance arraylist linked-list


【解决方案1】:

正如 Milkmaid 所说,这不是你应该做基准测试的方式,但我相信你得到的结果仍然有效。

让我们看看“幕后”,看看这两种实现:

ArrayList.removeAll 呼叫batchRemove

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

如您所见,ArrayList 首先“碎片整理”底层数组,方法是用随后出现的元素覆盖需要删除的元素(complement 作为 false 传递,因此只有不是 @ 987654327@被复制):

if (c.contains(elementData[r]) == complement)
    elementData[w++] = elementData[r];

下面的if (r != size) 处理从c.contains 抛出异常的情况,它使用“魔术函数”System.arraycopy 将其余元素从当前索引复制到最后 - 这部分运行本机代码,应该相当快,这就是我们可以忽略它的原因。

最后一个 if:if (w != size) {...} 只是将 nulls 分配给列表的其余部分,以便 GC 可以收集符合条件的对象。

操作总数为O(n),每个操作使用对数组的直接访问。

现在让我们看一下相当短的 LinkedList 的实现:

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    boolean modified = false;
    Iterator<?> it = iterator();
    while (it.hasNext()) {
        if (c.contains(it.next())) {
            it.remove(); // <-- calls the iterator remove method
            modified = true;
        }
    }
    return modified;
}

如您所见,该实现使用迭代器来删除元素,方法是调用:it.remove();

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        AbstractList.this.remove(lastRet); // <-- this is what actually runs
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
    }
}

依次调用:

public E remove(int index) {
    rangeCheck(index);
    checkForComodification();
    E result = l.remove(index+offset); // <-- here
    this.modCount = l.modCount;
    size--;
    return result;
}

调用者:

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index)); // <-- here
}

调用:

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

总结一下

虽然理论上 LinkedList 中的 remove 操作应该是 O(1) 而 ArrayList 实现应该采用 O(n),但在处理批量删除时,ArrayList 的实现更加简洁,通过移动对象来覆盖一次完成所有操作我们删除的那些(碎片整理),而 LinkedList 的实现递归地为它删除的每个元素调用 5 个不同的方法(每个方法都运行自己的安全检查......),这最终会导致您经历的巨大开销。

【讨论】:

    【解决方案2】:

    首先,100 个元素不足以测试性能。但从理论上来说: 数组中的数据(通常)一个接一个地存储在内存中。在链表中,您有值以及指向另一个对象的指针。这意味着当您删除数组时,您只需通过连接的内存。 O contre 如果您从链接列表中删除,您必须通过随机块内存取决于指针。数组和链表有更多的区别。就像添加元素删除元素等。这就是我们有数组和链表的原因。看这里Array vs Linked list

    【讨论】:

      【解决方案3】:

      这个问题的答案归结为 for 循环的执行时间不同。当您深入研究这两个对象的removeAll() 代码时,您会看到ArrayListremoveAll() 调用batchRemove(),如下所示:

      private boolean batchRemove(Collection<?> c, boolean complement) {
          final Object[] elementData = this.elementData;
          int r = 0, w = 0;
          boolean modified = false;
          try {
              for (; r < size; r++)
                  if (c.contains(elementData[r]) == complement)
                      elementData[w++] = elementData[r];
          } finally {
              // Preserve behavioral compatibility with AbstractCollection,
              // even if c.contains() throws.
              if (r != size) {
                  System.arraycopy(elementData, r,
                                   elementData, w,
                                   size - r);
                  w += size - r;
              }
              if (w != size) {
                  // clear to let GC do its work
                  for (int i = w; i < size; i++)
                      elementData[i] = null;
                  modCount += size - w;
                  size = w;
                  modified = true;
              }
          }
          return modified;
      }
      

      另一方面,当您调用LinkedListremoveAll() 时,它会调用AbstractCollectionremoveAll(),如下所示:

      public boolean removeAll(Collection<?> c) {
          Objects.requireNonNull(c);
          boolean modified = false;
          Iterator<?> it = iterator();
          while (it.hasNext()) {
              if (c.contains(it.next())) {
                  it.remove();
                  modified = true;
              }
          }
          return modified;
      }
      

      很明显,在ArrayList 的情况下,与LinkedList 中基于Iteratorfor 循环相比,执行了一个简单的for 循环。

      Iterator 更适合 LinkedList 这样的数据结构,但它仍然比传统的 for 循环更慢。

      您可以详细了解这两个循环here 的性能差异。

      【讨论】:

        猜你喜欢
        • 2016-03-17
        • 2011-01-25
        • 2017-10-20
        • 2021-12-19
        • 2014-09-17
        • 1970-01-01
        • 2016-12-19
        • 2021-12-16
        • 1970-01-01
        相关资源
        最近更新 更多