Java性能:removeAll()上的搜索和删除速度

Java性能:removeAll()上的搜索和删除速度,java,performance,collections,benchmarking,Java,Performance,Collections,Benchmarking,我比较了removeAll(Collection c)调用在Collection中声明的速度,觉得很有趣。现在我知道微基准测试很难做对,我不会看几毫秒的差异,但我相信我的结果是有效的,因为我反复运行它们,它们是非常可复制的 让我们假设有两个集合不是太小,比如100000个连续整数元素,而且它们大部分重叠,例如5000个在左侧,但不在右侧。现在我简单地说: left.removeAll(right); 当然,这一切都取决于左集合和右集合的类型。如果正确的集合是一个哈希映射,那么它的速度会非常快,

我比较了
removeAll(Collection c)
调用在
Collection
中声明的速度,觉得很有趣。现在我知道微基准测试很难做对,我不会看几毫秒的差异,但我相信我的结果是有效的,因为我反复运行它们,它们是非常可复制的

让我们假设有两个集合不是太小,比如100000个连续整数元素,而且它们大部分重叠,例如5000个在左侧,但不在右侧。现在我简单地说:

left.removeAll(right);
当然,这一切都取决于左集合和右集合的类型。如果正确的集合是一个哈希映射,那么它的速度会非常快,因为这就是查找的地方。但仔细观察,我发现了两个我无法解释的结果。我用排序的
ArrayList
和另一个被洗牌的
Collections.shuffle()
尝试了所有的测试


第一个奇怪的结果是:

00293  025%   shuffled ArrayList, HashSet
00090  008%     sorted ArrayList, HashSet
现在,从排序的
ArrayList
中删除元素比从无序列表中删除要快,或者从
HashSet
中查找连续值比查找随机值要快


另一个:

02311  011%     sorted ArrayList, shuffled ArrayList
01401  006%     sorted ArrayList,   sorted ArrayList
现在,这表明排序后的
ArrayList
(对左侧列表的每个元素使用
contains()
调用)中的查找速度比无序列表中的快。现在,如果我们能利用它被排序的事实并使用二进制搜索,这将是非常容易的,但我不这样做


这两个结果对我来说都很神秘。我无法通过查看代码或使用我的数据结构知识来解释它们。它是否与处理器缓存访问模式有关?JIT编译器在优化东西吗?但如果是,那是什么?我进行了热身并连续运行了几次测试,但我的基准测试可能有一个根本性的问题

现在我知道微基准测试很难做对,我不会看几毫秒的差异,但我相信我的结果是有效的,因为我反复运行它们,它们是非常可复制的

这不能说服我。有缺陷基准的行为可以100%重复


我怀疑。。。事实上基准测试中的一个或多个缺陷>>正在查看(OpenJDK7-b147)的源代码。它似乎委托给名为
batchRemove()
的私有方法,如下所示:

663     private boolean batchRemove(Collection<?> c, boolean complement) {
664         final Object[] elementData = this.elementData;
665         int r = 0, w = 0;
666         boolean modified = false;
667         try {
668             for (; r < size; r++)
669                 if (c.contains(elementData[r]) == complement)
670                     elementData[w++] = elementData[r];
671         } finally {
672             // Preserve behavioral compatibility with AbstractCollection,
673             // even if c.contains() throws.
674             if (r != size) {
675                 System.arraycopy(elementData, r,
676                                  elementData, w,
677                                  size - r);
678                 w += size - r;
679             }
680             if (w != size) {
681                 for (int i = w; i < size; i++)
682                     elementData[i] = null;
683                 modCount += size - w;
684                 size = w;
685                 modified = true;
686             }
687         }
688         return modified;
689     }
(method)  (numElements)  Mode  Cnt        Score       Error  Units
  sorted           1000  avgt   50       52,055 ±     0,507  us/op
shuffled           1000  avgt   50       55,720 ±     0,466  us/op
  sorted          10000  avgt   50     5341,917 ±    28,630  us/op
shuffled          10000  avgt   50     7108,845 ±    45,869  us/op
  sorted         100000  avgt   50   621714,569 ± 19040,964  us/op
shuffled         100000  avgt   50  1110301,876 ± 22935,976  us/op
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.Function;

public class RemoveAllBenchmarkExt
{
    public static void main(String[] args)
    {
        for (int n=10000; n<=100000; n+=10000)
        {
            runTest(n, sortedList()  , sortedList());
            runTest(n, sortedList()  , shuffledList(0.00));
            runTest(n, sortedList()  , shuffledList(0.25));
            runTest(n, sortedList()  , shuffledList(0.50));
            runTest(n, sortedList()  , shuffledList(0.75));
            runTest(n, sortedList()  , shuffledList(1.00));
            runTest(n, sortedList()  , reversedList());
            System.out.println();
        }
    }

    private static Function<Integer, Collection<Integer>> sortedList()
    {
        return new Function<Integer, Collection<Integer>>()
        {
            @Override
            public Collection<Integer> apply(Integer t)
            {
                List<Integer> list = new ArrayList<Integer>(t);
                for (int i=0; i<t; i++)
                {
                    list.add(i);
                }
                return list;
            }

            @Override
            public String toString()
            {
                return "sorted";
            }
        };
    }

    private static Function<Integer, Collection<Integer>> shuffledList(
        final double degree)
    {
        return new Function<Integer, Collection<Integer>>()
        {
            @Override
            public Collection<Integer> apply(Integer t)
            {
                List<Integer> list = new ArrayList<Integer>(t);
                for (int i=0; i<t; i++)
                {
                    list.add(i);
                }
                shuffle(list, degree);
                return list;
            }

            @Override
            public String toString()
            {
                return String.format("shuffled(%4.2f)", degree);
            }
        };
    }


    private static void shuffle(List<Integer> list, double degree)
    {
        Random random = new Random(0);
        int n = (int)(degree * list.size());
        for (int i=n; i>1; i--)
        {
            swap(list, i-1, random.nextInt(i));
        }
    }
    private static void swap(List<Integer> list, int i, int j)
    {
        list.set(i, list.set(j, list.get(i)));
    }

    private static Function<Integer, Collection<Integer>> reversedList()
    {
        return new Function<Integer, Collection<Integer>>()
        {
            @Override
            public Collection<Integer> apply(Integer t)
            {
                List<Integer> list = new ArrayList<Integer>(t);
                for (int i=0; i<t; i++)
                {
                    list.add(i);
                }
                Collections.reverse(list);
                return list;
            }

            @Override
            public String toString()
            {
                return "reversed";
            }
        };
    }


    private static void runTest(int n,
        Function<Integer, ? extends Collection<Integer>> leftFunction,
        Function<Integer, ? extends Collection<Integer>> rightFunction)
    {

        Collection<Integer> left = leftFunction.apply(n);
        Collection<Integer> right = rightFunction.apply((int)(n*0.95));

        long before = System.nanoTime();
        left.removeAll(right);
        long after = System.nanoTime();
        double durationMs = (after - before) / 1e6;

        System.out.printf(
            "%8d elements, %15s, duration %10.3f ms, size %d\n",
            n, rightFunction, durationMs, left.size());
    }
}
100000 elements,          sorted, duration   6016,354 ms, size 5000
100000 elements,  shuffled(0,00), duration   5849,537 ms, size 5000
100000 elements,  shuffled(0,25), duration   7319,948 ms, size 5000
100000 elements,  shuffled(0,50), duration   9344,408 ms, size 5000
100000 elements,  shuffled(0,75), duration  10657,021 ms, size 5000
100000 elements,  shuffled(1,00), duration  11295,808 ms, size 5000
100000 elements,        reversed, duration   5830,695 ms, size 5000
663私有布尔批处理删除(集合c,布尔补码){
664最终对象[]elementData=this.elementData;
665 int r=0,w=0;
666布尔修改=假;
667试试看{
668用于(;r
它实际上在数组中循环,并有一系列
c.contains()
调用。基本上,对于排序的数组,没有理由让迭代速度更快

我支持StephenC对基准测试的质疑,并相信在深入研究缓存访问模式等之前仔细检查基准测试代码会更有成效


此外,如果基准代码不是罪魁祸首,那么了解java版本和OS/arch等也很有意思。

性能差异的原因是内存访问模式:访问内存中连续的元素比随机内存访问要快(由于内存预取、cpu缓存等原因)

最初填充集合时,会在内存中按顺序创建所有元素,因此在遍历集合(foreach、removeAll等)时,会访问连续的内存区域,这是缓存友好的。当您洗牌集合时-元素在内存中保持相同的顺序,但指向这些元素的指针不再具有相同的顺序,因此当您遍历集合时,您将访问例如第10个、第1个、然后是第5个元素,这非常不利于缓存并破坏性能

您可以更详细地查看此问题,其中可以看到此效果:

由于询问者没有提供任何示例代码,并且对评论和回答中提到的基准有疑问,我创建了一个小测试,以查看当参数是无序列表(而不是排序列表)时,
removeAll
方法是否较慢。我确认了提问者的观察结果:测试的输出大致是

100000 elements,   sortedList and   sortedList,  5023,090 ms, size 5000
100000 elements, shuffledList and   sortedList,  5062,293 ms, size 5000
100000 elements,   sortedList and shuffledList, 10657,438 ms, size 5000
100000 elements, shuffledList and shuffledList, 10700,145 ms, size 5000
我将在这里省略这个特定测试的代码,因为它也受到了质疑(顺便说一句,这是完全合理的!很多BS发布在web上…)

所以我做了进一步的测试,我将在这里提供代码

这也可能不是一个明确的答案。但我试图调整测试,以便它们至少提供一些强有力的证据,证明性能降低的原因确实是什么(+1,如果它能说服您,请接受)。也就是说,减速的原因在于
100000 elements,          sorted, duration   6016,354 ms, size 5000
100000 elements,  shuffled(0,00), duration   5849,537 ms, size 5000
100000 elements,  shuffled(0,25), duration   7319,948 ms, size 5000
100000 elements,  shuffled(0,50), duration   9344,408 ms, size 5000
100000 elements,  shuffled(0,75), duration  10657,021 ms, size 5000
100000 elements,  shuffled(1,00), duration  11295,808 ms, size 5000
100000 elements,        reversed, duration   5830,695 ms, size 5000