junit 数组中的Java列表过大Arrays.asList与AbstractList的性能问题

bttbmeg0  于 2023-02-16  发布在  Java
关注(0)|答案(2)|浏览(116)
    • 情况:**

针对一个(庞大的)元素数组需要高效地转换为一个扩展了一个值的List的问题,设计了一个简化的AbstractList实现,并将其用于一个专用的方法中,取得了很好的效果。

    • 问题:**

简单地应用Arrays.asList(String element, String[] elements)是不会起作用的(这样会非常快),尝试任何其他看似简单的方法都会失败,因为会出现严重的性能问题。

    • 请考虑以下示例,其中没有一个执行良好:**
protected static <T> List<T> slowAsList(final T last, final T... elements) {

    //1
    return IntStream.rangeClosed(0, length)
      .mapToObj(i -> i < length ? elements[i] : last)
      .collect(Collectors.toList());

    //2
    return Stream.concat(Stream.of(first), Arrays.stream(elements))
      .parallel()
      .collect(Collectors.toList());
    //3
    return new ArrayList() {{ for (Object o : elements) add(o); add(last); }};

    //4
    final ArrayList arrayList = new ArrayList();
    arrayList.add(last);
    for (Object o : elements) arrayList.add(o);
    return arrayList;

    //5
    final int arrayLen = elements.length + 1;
    final Object[] array = Arrays.copyOf(elements, arrayLen);
    for (int i = 1; i < arrayLen; i++) {
      array[i] = elements[i - 1];
    }
    array[arrayLen - 1] = last;
    return Arrays.asList(array);
}

该方法将在JUnit测试中停止,并且不会在可行的时间内完成:

@Timeout(15)
@Test
public void fastAsListTest() {
    final Object[] testStrings1MwithNull = IntStream.rangeClosed(0, 1000000)
      .mapToObj(i -> i == 1000000 ? null : "TEST" + i)
      .toArray();

    slowAsList(testStrings1MwithNull);
}

当用AbstractList实现的变体替换方法中的实现时,测试将在几毫秒内完成,而不是:

protected static <T> List<T> fastAsList(final T last, final T... elements) {
    if (elements == null) {
      return null;
    }
    return new AbstractList<T>() {
      @Override
      public int size() {
        return elements.length;
      }
      @Override
      public T get(int index) {
        return index < 0 ? null : elements[index];
      }
    };
}

编辑:这是原始代码,添加了first元素

protected static List<Object> asList(final Object first, final Object[] elements) {
    
    return new AbstractList<Object>() {
      @Override
      public int size() {
        return elements.length + 1;
        // return elements != null ? elements.length + 1 : 0;
      }
    
      @Override
      public Object get(int index) {
        return (index == 0) ? first : elements[index - 1];
        // return (index == 0) ? first : elements!= null ? elements[index - 1] : null;
      }
    };

注解掉的代码显示了添加的null检查,用于检查导致com.sun.jdi.InvocationException的空值以及元素null
导致此问题的原因可能是什么,是否内存不足?
我已经将Xms/Xmx提升到2G/2G进行测试,在Windows中的总内存消耗约为83%(总共16G)。

w1jd8yoj

w1jd8yoj1#

首先:
你的性能测试很差。你的JVM没有预热,代码需要JIT等等。甚至有可能对被测方法的调用被编译掉--你不使用结果。学习如何使用JMH来更深入地了解每种替代方法的性能。
话虽如此:
所有的替代方法都非常慢,因为它们将源列表的元素复制到目标列表中,一次一个元素。你甚至没有设置输出列表的初始大小-它从一个小的默认值开始,当你添加元素时需要增长。同样,复制更多。
你还没有考虑过的一个选择是:

  • 分配所需大小的目标数组
  • 使用System.arraycopy将源数组复制到目标数组中所需的位置。System.arraycopy是复制数组的最快方法(等同于memcpy)
static <T> List<T> slowAsList(final Class<T> clazz, final T firstElem, final T... sourceElements) {
    final int arrayLen = sourceElements.length + 1;
    final T[] targetArray = (T[])Array.newInstance(clazz, arrayLen);
    System.arraycopy(sourceElements,
            0,
            targetArray, 1, sourceElements.length);
    targetArray[0] = firstElem;
    return Arrays.asList(targetArray);
}

@Test
public void slowAsListTest() {
    final String[] testStrings1MwithNull = IntStream.rangeClosed(0, 1000000)
            .mapToObj(i -> i == 1000000 ? null : "TEST" + i)
            .toArray(String[]::new);

    var ret = slowAsList(String.class, "1", testStrings1MwithNull);
    System.out.println(ret.size());
}

在我的机器上,这比您的 Package 器实现要快(同样,没有预热JVM)
最重要的是:如果您的自定义 Package 器是一个性能优化,并且您不会 Package 一个 Package 器的 Package 器......,那么这样做似乎是合理的。

pkmbmrz7

pkmbmrz72#

我的猜测是:AbstractList的返回对象是一个简单的Reference示例,返回对象的字段是一个巨大的数组。
换句话说,没有真正的拷贝。

相关问题