【问题标题】:Java Collection performance [duplicate]Java集合性能[重复]
【发布时间】:2019-12-17 04:09:51
【问题描述】:

我刚刚尝试了一个关于 codility 的测试示例。任务是:“......给定一个包含 N 个整数的数组 A,返回 A 中未出现的最小正整数(大于 0)。”。

加号:

  • N 是 [1..100,000] 范围内的整数;

  • 数组 A 的每个元素都是 [−1,000,000..1,000,000] 范围内的整数。

我的第一次尝试是典型的 Java 8 解决方案:

public int solution(int[] A) {

     Set<Integer> l = Arrays
            .stream(A)
            .boxed()
            .filter(i -> i > 0)
            .collect(Collectors.toSet());

    return IntStream
            .iterate(1, a -> a + 1)
            .filter(i -> !l.contains(i))
            .findFirst()
            .getAsInt();
}

一切正确,但对中等大小的测试数组的测试遇到了超时。

第二次尝试(普通的旧java):

public int solution(int[] A) {

    boolean[] B = new boolean[1000001];

    for (int a : A) {
        if (a > 0) {
            B[a] = true;
        }
    }

    for (int i = 1; i <= 1000000; i++) {
        if (!B[i]) {
            return i;
        }
    }

    return 1;
}

这个版本快得令人难以置信,尤其是对于短数组 A。

问题:

  • 我第一次尝试时是否遗漏了什么?
  • 是否有调整选项?
  • 对代码能力的测试是否为时过早,实际上预计差异会消失,几乎完全消失?
  • 有人有更好的 Java 8 解决方案吗?

测试结果第一版:

▶ 示例1 第一个示例测试 ✔OK 1. 0.108秒OK

▶ 例子2 第二个示例测试✔OK 1. 0.104秒OK

▶ 例子3 第三个示例测试✔OK 1. 0.104秒OK

▶extreme_single 单个元素 ✔OK 1. 0.100 秒确定 2. 0.104 秒 3. 0.104 秒 OK 4. 0.100秒OK

▶ 简单 简单测试✔OK 1. 0.100 秒确定 2. 0.104 秒 3. 0.100秒OK

▶extreme_min_max_value 最小值和最大值 ✔OK 1. 0.100 秒确定 2. 0.104秒OK

▶ positive_only 0...100 然后 102...200 的随机序列 ✔OK 1. 0.100 秒确定 2. 0.104秒OK

▶negative_only 随机序列 -100 ... -1 ✔OK 1. 0.100秒OK

▶ 中等 混沌序列长度=10005(带减号)✘TIMEOUT ERROR 运行时间:0.136 秒,时限:0.100 秒。 1. 0.136 s TIMEOUT ERROR,运行时间:0.136 秒,时限:0.100 秒。 2. 0.128 s TIMEOUT ERROR,运行时间:0.128 秒,时限:0.100 秒。 3. 0.144 s TIMEOUT ERROR,运行时间:0.144 秒,时限:0.128 秒。

▶ 大_1 混乱 + 序列 1, 2, ..., 40000(没有减号)✔OK 1. 0.588秒OK

▶大_2 改组后的序列 1, 2, ..., 100000(没有减号) ✔OK 1. 0.748 秒 2. 0.660秒OK

▶ 大_3 混乱+许多-1、1、2、3(带减号)✔OK 1. 0.444秒OK

测试结果第二版:

▶ 示例1 第一个示例测试 ✔OK 1. 0.004秒OK

▶ 例子2 第二个示例测试✔OK 1. 0.004秒OK

▶ 例子3 第三个示例测试✔OK 1. 0.004秒OK

▶extreme_single 单个元素 ✔OK 1. 0.004 秒 2. 0.008 秒 3. 0.004 秒 4. 0.008秒OK

▶ 简单 简单测试✔OK 1. 0.004 秒 2. 0.004 秒 3. 0.008秒OK

▶extreme_min_max_value 最小值和最大值 ✔OK 1. 0.008 秒 2. 0.004秒OK

▶ positive_only 0...100 然后 102...200 的随机序列 ✔OK 1. 0.008 秒 2. 0.004秒OK

▶negative_only 随机序列 -100 ... -1 ✔OK 1. 0.008秒OK

▶ 中等 混沌序列长度=10005(带减号)✔OK 1. 0.024 秒 2. 0.024 秒 3. 0.032秒OK

▶ 大_1 混乱 + 序列 1, 2, ..., 40000(没有减号)✔OK 1. 0.220秒OK

▶大_2 改组后的序列 1, 2, ..., 100000(没有减号) ✔OK 1. 0.244 秒 2. 0.244秒OK

▶ 大_3 混乱+许多-1、1、2、3(带减号)✔OK 1. 0.172秒OK

底线: 尤其是使用小型数组的测试仅使用普通 java 就快了两个数量级。对于大型数组,它“仅”是 3 倍。

编辑:

根据 cmets,我只是试图深入研究问题并尝试:

public int solution(int[] A) {

boolean[] B = new boolean[1000001];

for (int a : A) {
    if (a>0){
        B[a] = true;
    }
}

 return IntStream
        .iterate(1, a -> a + 1)
        .filter(i -> !B[i])
        .findFirst()
        .getAsInt();

}

结果:

▶ 示例1 第一个示例测试 ✔OK 1. 0.096秒OK

▶ 例子2 第二个示例测试✔OK 1. 0.096秒OK

▶ 例子3 第三个示例测试✔OK 1. 0.096 秒 折叠所有正确性测试

▶extreme_single 单个元素 ✔OK 1. 0.096 秒 2. 0.096 秒 3. 0.096 秒 OK 4. 0.096秒OK

▶ 简单 简单测试✔OK 1. 0.100 秒确定 2. 0.096 秒 3. 0.100秒OK

▶extreme_min_max_value 最小值和最大值 ✔OK 1. 0.096 秒 2. 0.100秒OK

▶ positive_only 0...100 然后 102...200 的随机序列 ✔OK 1. 0.096 秒 2. 0.096秒OK

▶negative_only 随机序列 -100 ... -1 ✔OK 1. 0.096 秒 折叠所有性能测试

▶ 中等 混沌序列长度=10005(带减号)✘TIMEOUT ERROR 运行时间:0.116 秒,时限:0.112 秒。 1. 0.116 s TIMEOUT ERROR,运行时间:0.116 秒,时限:0.112 秒。 2. 0.116 s TIMEOUT ERROR,运行时间:0.116 秒,时限:0.100 秒。 3. 0.124秒OK

▶ 大_1 混乱 + 序列 1, 2, ..., 40000(没有减号)✔OK 1. 0.340秒OK

▶大_2 改组后的序列 1, 2, ..., 100000(没有减号) ✔OK 1. 0.408 秒 2. 0.372秒OK

▶ 大_3 混乱+许多-1、1、2、3(带减号)✔OK 1. 0.272秒OK

结论:

  • 对于小型测试阵列,它几乎和第一个版本一样慢,因此这里的 IntStream 似乎是瓶颈。
  • 对于大型测试阵列,速度似乎是中等的。因此,我不确定它应该告诉我什么。

编辑 2:

我实际上只是找到了一篇部分描述该问题的文章:https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html。其中,作者声称流和在数组上运行的 for 循环之间的差异是由于流是相当新的事实。但是这篇文章的日期是 4 年前。

【问题讨论】:

  • 你为什么认为流应该胜过普通的旧循环?
  • 您的流解决方案中有很多装箱/拆箱。
  • @AndrewTobilko 我没有声称,我希望流更快。但是,我预计可能会有一些百分比的差异,因此对于大多数现实世界的应用程序来说并不有趣。但是两个数量级,甚至是三倍,都超出了我的预期。
  • @LutzHorn 我无权访问支持的 java 进程。它是由 codility 运行的。
  • @Nexevis 是的,但我将其与基元数组进行比较。

标签: java java-8 performance-testing primitive-types


【解决方案1】:

你没有错过任何东西。装箱整数和检查 HashSet 比迭代原始数组要慢。我对您的第二个解决方案所做的唯一更改是将boolean[] 替换为BitSet,这类似于boolean[],但更节省空间,因为它每个元素仅使用一位。

【讨论】:

  • 我相应地对我的问题进行了编辑。看起来,在 HashSet 中的搜索并没有对小测试数组产生很大的影响。
  • 是的,我记得 BitSet(很久以前)。没错,这应该会有所帮助。但这与我在问题中的观点无关。
  • @PeMa 不清楚,你的问题是什么意思。您正在创建两个代码 sn-ps 做完全不同的事情,这与 Stream API 无关(设置数组的布尔条目 is 与填充 HashSet&lt;Integer&gt; 完全不同,此外,循环在1000000 处停止,而流是无限的,而不是等效的IntStream.rangeClosed(1, 1000000))。您没有记录您的测量方法。
  • @MikeFHay 使用BitSet 执行此任务不仅仅是“更节省空间”,因为通过.nextClearBit(1) 搜索最小的数字将一次测试64 位。
【解决方案2】:

这两段代码之间有很多不同之处。我怀疑最大的区别来自使用Set&lt;Integer&gt;boolean[],所以我写了一个小测试:

        Set<Integer> set = new HashSet<>();
        for (int n : numbs) {
            set.add(n);
        }

        boolean[] arr = new boolean[numbs.length];
        for (int n : numbs) {
            arr[n] = true;
        }

1000000 个数字在 [0, 1000000) 范围内的差异是 20 倍。

【讨论】:

  • 嗯,我实际上怀疑同样有同样的。但正如在编辑中看到的那样,对于小型测试数组,这并不是真正产生巨大差异的原因。
  • 这个解决方案在帖子末尾的问题是它没有测试结果.getAsInt() - 1是否在A中(可能是)。
  • @scottb;我的解决方案是错误的。我删除了它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多