【问题标题】:Why does Java's ArrayList's remove function seem to cost so little?为什么 Java ArrayList 删除功能似乎花费这么少?
【发布时间】:2011-08-31 09:38:52
【问题描述】:

我有一个函数可以处理一个非常大的列表,超过大约 250,000 个项目。对于这些项目中的大多数,它只是替换位置 x 处的项目。但是,对于其中的大约 5%,它必须将它们从列表中删除。

使用 LinkedList 似乎是避免昂贵删除的最明显的解决方案。然而,随着时间的推移,通过索引访问 LinkedList 自然会变得越来越慢。这里的成本是几分钟(而且很多)。

在该 LinkedList 上使用迭代器也很昂贵,因为我似乎需要一个单独的副本以避免在编辑该列表时出现迭代器并发问题。这里的成本是分钟。

但是,这就是我有点震惊的地方。如果我更改为 ArrayList,它几乎可以立即运行。

对于包含 297515 个元素的列表,删除 11958 个元素并修改其他所有元素需要 909 毫秒。我验证了结果列表的大小确实为 285557,正如预期的那样,并且包含我需要的更新信息。

为什么这么快?我查看了 JDK6 中 ArrayList 的源代码,它似乎按预期使用了 arraycopy 函数。我很想理解为什么 ArrayList 在这里工作得这么好,而常识似乎表明这个任务的数组是一个糟糕的主意,需要移动几十万个项目。

【问题讨论】:

  • 也许 arraycopy 真的很好? :) 这是一个非常基本的功能,经常用于您描述的事情。我认为它非常优化。此外,您可以使用Iteratorremove() 方法在迭代时从列表中删除元素。
  • 如果你将它与 LinkedList 进行比较会很有趣。为什么需要 LinkedList 的副本?在我看来,在你的情况下使用迭代器比使用索引迭代更简单(或者你如何迭代?)。
  • 数组复制是一个相当便宜的操作,你还没有进入这个变得非常重要的范围。例如。您复制大约 12000 次大小为 150000 的数组(平均)。这不需要太多时间(这里测试
  • 哦,如果你想要 log(n) 添加、获取和删除,你应该考虑一棵树。
  • @Nikita Beloglazov 提出了一个很好的观点 - 你关于“我似乎需要一个单独的副本以避免在编辑该列表时出现迭代器并发问题”的引用不应该是这样,除非你实际上正在处理多个线程。如果你们都在迭代器上 .remove() ,这是非常安全的;如果您在列表上执行 .remove(n),那是错误的。我怀疑这就是您正在做的事情,并且更改为 Iterator.remove 将使您的所有问题都消失...

标签: java performance optimization arraylist


【解决方案1】:

我运行了一个基准测试,尝试了以下每种过滤列表元素的策略:

  • 将需要的元素复制到新列表中
  • 使用Iterator.remove()ArrayList 中删除不需要的元素
  • 使用Iterator.remove()LinkedList 中删除不需要的元素
  • 就地压缩列表(将所需元素移动到较低位置)
  • ArrayList 上按索引 (List.remove(int)) 删除
  • LinkedList 上按索引 (List.remove(int)) 删除

每次我用 100000 个 Point 的随机实例填充列表并使用过滤条件(基于哈希码)接受 95% 的元素并拒绝剩余的 5%(与问题中所述的比例相同) ,但列表较小,因为我没有时间运行 250000 个元素的测试。)

平均时间(在我的旧 MacBook Pro 上:Core 2 Duo、2.2GHz、3Gb RAM)是:

CopyIntoNewListWithIterator   :      4.24ms
CopyIntoNewListWithoutIterator:      3.57ms
FilterLinkedListInPlace       :      4.21ms
RandomRemoveByIndex           :    312.50ms
SequentialRemoveByIndex       :  33632.28ms
ShiftDown                     :      3.75ms

因此,从LinkedList 中按索引删除元素比从ArrayList 中删除元素的成本高出 300 多倍,并且可能比其他方法(避免线性搜索和 @ 987654333@)

这四种更快的方法之间似乎没有太大区别,但我再次运行这四种方法,得到了 500000 个元素的列表,结果如下:

CopyIntoNewListWithIterator   :     92.49ms
CopyIntoNewListWithoutIterator:     71.77ms
FilterLinkedListInPlace       :     15.73ms
ShiftDown                     :     11.86ms

我猜随着更大尺寸的高速缓存成为限制因素,因此创建列表的第二个副本的成本变得很高。

代码如下:

import java.awt.Point;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public class ListBenchmark {

    public static void main(String[] args) {
        Random rnd = new SecureRandom();
        Map<String, Long> timings = new TreeMap<String, Long>();
        for (int outerPass = 0; outerPass < 10; ++ outerPass) {
            List<FilterStrategy> strategies =
                Arrays.asList(new CopyIntoNewListWithIterator(),
                              new CopyIntoNewListWithoutIterator(),
                              new FilterLinkedListInPlace(),
                              new RandomRemoveByIndex(),
                              new SequentialRemoveByIndex(),
                              new ShiftDown());
            for (FilterStrategy strategy: strategies) {
                String strategyName = strategy.getClass().getSimpleName();
                for (int innerPass = 0; innerPass < 10; ++ innerPass) {
                    strategy.populate(rnd);
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        if (totalTime == null) totalTime = 0L;
                        timings.put(strategyName, totalTime - System.currentTimeMillis());
                    }
                    Collection<Point> filtered = strategy.filter();
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        timings.put(strategy.getClass().getSimpleName(), totalTime + System.currentTimeMillis());
                    }
                    CHECKSUM += filtered.hashCode();
                    System.err.printf("%-30s %d %d %d%n", strategy.getClass().getSimpleName(), outerPass, innerPass, filtered.size());
                    strategy.clear();
                }
            }
        }
        for (Map.Entry<String, Long> e: timings.entrySet()) {
            System.err.printf("%-30s: %9.2fms%n", e.getKey(), e.getValue() * (1.0/25.0));
        }
    }

    public static volatile int CHECKSUM = 0;

    static void populate(Collection<Point> dst, Random rnd) {
        for (int i = 0; i < INITIAL_SIZE; ++ i) {
            dst.add(new Point(rnd.nextInt(), rnd.nextInt()));
        }
    }

    static boolean wanted(Point p) {
        return p.hashCode() % 20 != 0;
    }

    static abstract class FilterStrategy {
        abstract void clear();
        abstract Collection<Point> filter();
        abstract void populate(Random rnd);
    }

    static final int INITIAL_SIZE = 100000;

    private static class CopyIntoNewListWithIterator extends FilterStrategy {
        public CopyIntoNewListWithIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            ArrayList<Point> dst = new ArrayList<Point>(list.size());
            for (Point p: list) {
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class CopyIntoNewListWithoutIterator extends FilterStrategy {
        public CopyIntoNewListWithoutIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            ArrayList<Point> dst = new ArrayList<Point>(inputSize);
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class FilterLinkedListInPlace extends FilterStrategy {
        public String toString() {
            return getClass().getSimpleName();
        }
        FilterLinkedListInPlace() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (Iterator<Point> it = list.iterator();
                 it.hasNext();
                 ) {
                Point p = it.next();
                if (! wanted(p)) it.remove();
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class RandomRemoveByIndex extends FilterStrategy {
        public RandomRemoveByIndex() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class SequentialRemoveByIndex extends FilterStrategy {
        public SequentialRemoveByIndex() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class ShiftDown extends FilterStrategy {
        public ShiftDown() {
            list = new ArrayList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            int outputSize = 0;
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) {
                    list.set(outputSize++, p);
                }
            }
            list.subList(outputSize, inputSize).clear();
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

}

【讨论】:

    【解决方案2】:

    数组复制是一个相当便宜的操作。它是在非常基本的级别上完成的(它是一个 java 本地静态方法),您还没有处于性能变得非常重要的范围内。

    在您的示例中,您复制了大约 12000 次大小为 150000 的数组(平均而言)。这不需要太多时间。我在这里在我的笔记本电脑上进行了测试,耗时不到 500 毫秒。

    更新我使用以下代码在我的笔记本电脑(Intel P8400)上进行测量

    import java.util.Random;
    
    public class PerformanceArrayCopy {
    
        public static void main(String[] args) {
    
            int[] lengths = new int[] { 10000, 50000, 125000, 250000 };
            int[] loops = new int[] { 1000, 5000, 10000, 20000 };
    
            for (int length : lengths) {
                for (int loop : loops) {
    
                    Object[] list1 = new Object[length];
                    Object[] list2 = new Object[length];
    
                    for (int k = 0; k < 100; k++) {
                        System.arraycopy(list1, 0, list2, 0, list1.length);
                    }
    
                    int[] len = new int[loop];
                    int[] ofs = new int[loop];
    
                    Random rnd = new Random();
                    for (int k = 0; k < loop; k++) {
                        len[k] = rnd.nextInt(length);
                        ofs[k] = rnd.nextInt(length - len[k]);
                    }
    
                    long n = System.nanoTime();
                    for (int k = 0; k < loop; k++) {
                        System.arraycopy(list1, ofs[k], list2, ofs[k], len[k]);
                    }
                    n = System.nanoTime() - n;
                    System.out.print("length: " + length);
                    System.out.print("\tloop: " + loop);
                    System.out.print("\truntime [ms]: " + n / 1000000);
                    System.out.println();
                }
            }
        }
    }
    

    一些结果:

    length: 10000   loop: 10000 runtime [ms]: 47
    length: 50000   loop: 10000 runtime [ms]: 228
    length: 125000  loop: 10000 runtime [ms]: 575
    length: 250000  loop: 10000 runtime [ms]: 1198
    

    【讨论】:

    • +1:如果您可以将测试代码添加到您的答案中(假设它很短)以及您平台的一些基本规格,那将会很有用。
    • @Oli Charlesworth:我愿意,但系统目前不允许我编辑。奇怪...
    • +1 用于编写简单的基准测试,同时仍然避免常见的陷阱。尽管如果他不需要随机访问,则链表上的迭代器应该更快——也就是说,如果他使用迭代器 remove,这似乎是这里真正的问题。
    【解决方案3】:

    我认为性能差异可能归结为 ArrayList 支持随机访问而 LinkedList 不支持的差异。

    如果我想获取(1000)一个 ArrayList,我指定了一个特定的索引来访问它,但是 LinkedList 不支持这个,因为它是通过节点引用组织的。

    如果我调用 LinkedList 的 get(1000),它将迭代整个列表,直到找到索引 1000,如果 LinkedList 中有大量项目,这可能会非常昂贵。

    【讨论】:

      【解决方案4】:

      有趣且出乎意料的结果。这只是一个假设,但是...

      平均而言,删除一个数组元素需要将列表的一半(其后的所有内容)移回一个元素。如果每个项目都是指向对象的 64 位指针(8 字节),那么这意味着复制 125000 个项目 x 每个指针 8 字节 = 1 MB。

      现代 CPU 可以非常快速地将 1 MB RAM 的连续块复制到 RAM。

      与每次访问循环遍历链表(需要比较和分支以及其他不友好的 CPU 活动)相比,RAM 复制速度更快。

      您真的应该尝试独立地对各种操作进行基准测试,看看它们在各种列表实现中的效率如何。如果您愿意,请在此处分享您的结果!

      【讨论】:

      • 也许我弄错了最好的处理方法。因为由于需要编辑列表本身,我做了这样的事情来获得一个新列表: List originalList = new LinkedList(); originalList = getAllItems(); List newList = new LinkedList(originalList);第二个列表的创建速度比所有这些删除操作的总和要慢。
      • @Ken - 我认为你应该提出一个关于如何复制列表的新问题,并包含示例代码。
      【解决方案5】:

      我在这里故意跳过一些实现细节,只是为了解释根本区别。

      要删除 M 元素列表中的第 N 个元素,LinkedList 实现将向上导航到该元素,然后简单地删除它并相应地更新 N-1 和 N+1 元素的指针。第二个操作非常简单,但是要花时间处理这个元素。

      然而,对于 ArrayList,访问时间是瞬时的,因为它由数组支持,这意味着连续的内存空间。可以直接跳转到正确的内存地址去执行,广义上讲如下:

      • 重新分配一个包含 M - 1 个元素的新数组
      • 将 0 到 N - 1 的所有内容放在新数组列表的数组中的索引 0 处
      • 将 N + 1 到 M 的所有内容放在 arraylist 数组中的索引 N 处。

      想一想,您会注意到您甚至可以重用相同的数组,因为 Java 可以使用预先分配大小的 ArrayList,因此如果您删除元素,您不妨跳过步骤 1 和 2,直接执行步骤 3 并更新你的尺码。

      内存访问速度很快,在现代硬件上复制一块内存可能已经足够快,以至于移动到 N 位置太耗时了。

      但是,如果您使用 LinkedList 的方式允许您删除多个相互跟随的元素并跟踪您的位置,您会看到收益。

      但显然,在很长的列表中,执行简单的 remove(i) 会很昂贵。


      为此添加一点盐和香料:

      【讨论】:

      • 第一步中的重新分配不会发生,arraylist 可以保留比它的容量更短的列表(== 长度后备数组)并且 System.arrayCopy 允许重叠副本
      • @ratchet 怪胎:这正是我之后提到的,但如果我的有点模糊,感谢您的补充说明。
      猜你喜欢
      • 2013-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-09
      • 2016-08-24
      • 1970-01-01
      • 1970-01-01
      • 2017-11-15
      相关资源
      最近更新 更多