【问题标题】:Why is filtering a stream faster than an iterative code that simply returns a sublist?为什么过滤流比只返回子列表的迭代代码更快?
【发布时间】:2017-05-26 15:05:29
【问题描述】:

有人可以向我解释为什么以下代码在流版本中比通过返回主列表的子列表比仅过滤器更快吗?

public static final int N = 50000000;

static List<Integer> sourceList = new ArrayList<>();

static {
  for (int i = 0; i < N; i++) {
    sourceList.add(i);
  }
}

@Benchmark
public List<Pair<Integer, Integer>> vanilla() {
   List<Pair<Integer, Integer>> collect1 = 
   sourceList.stream()
     .map(integer -> Pair.of(integer, integer))
     .collect(Collectors.toList());
   return collect1.subList(1000, 100000);
}

@Benchmark
public List<Pair<Integer, Integer>> stream() {
  return sourceList.stream()
    .map(value -> Pair.of(value, value))
    .filter(value -> value.getLeft() > 1000 && value.getLeft() < 100000)
    .collect(Collectors.toList());
}

Benchmark     Mode  Cnt    Score   Error  Units
Test.stream   avgt   20    9.867 ± 0.218  ns/op
Test.vanilla  avgt   20  183.304 ± 8.550  ns/op

我使用 JMH 运行测试,但我不理解结果。我认为通过添加将 Integer 值包装在 Pair 中的映射函数将强制流创建所有新对象以将它们传递给 filter 方法,然后提取 Pair 的左侧部分进行比较。这对我来说听起来比没有过滤的其他方法更密集,结果只是原始列表的子列表,因此没有遍历整个列表。

我错过了什么吗?

【问题讨论】:

    标签: java-8 java-stream


    【解决方案1】:

    很可能是因为第一个必须填充一个包含 50000000 个元素的整个列表,这涉及分配更多内存,每次达到容量时都会制作多个列表使用的内部数组的副本。

    第二个只需要创建一个包含 99000 个元素的列表,因此分配的内存更少,内部数组的副本也更少。

    更快的解决方案是在映射之前进行过滤,从而避免无用的装箱和对的创建。当然,限制为 100,000 也会更快。

    【讨论】:

    • 好的,有道理。我知道先过滤会更快,但我尝试简化此示例的生产代码,该代码也先进行一些映射。生产代码确实喜欢这里,它必须按照它们来的顺序为每个对象分配一个 int 值,然后只保留给定下限和上限的对象。问题是我需要以某种方式保留一个计数器并将其传递给过滤器,因为对象不是这里的 int。有没有办法重构这个示例代码来保持语义,同时反转映射和过滤函数?
    • 在您发布的示例中首先过滤将是微不足道的,对吧?对于您的实际生产代码,我不能没有看到它。
    【解决方案2】:

    性能问题不是subList的问题,实际上它只是简单地包装了主要的List

    ArrayList 将重置容量并将元素重复复制到一个新数组中,以便它可以添加更多元素。 vanilla 使用更大的内存大小来添加 50000000 元素。所以它比stream 慢,因为stream 仅添加[1000..100000] 元素。

    以下部分是ArrayList 类,需要扩展容量,更多元素添加到ArrayList 导致更多ensureCapacityInternal 方法调用:

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    
        ensureExplicitCapacity(minCapacity);
    }
    

    也许以下版本vanilla 会比stream 更快,但仍然使用更大的内存大小:

    @Benchmark
    public List<Pair<Integer, Integer>> vanilla() {
       List<Pair<Integer, Integer>> main = 
       sourceList.stream()
                 .map(integer -> Pair.of(integer, integer))
                 .collect(Collectors.toCollection(()->new ArrayList<>(N)));
       return main.subList(1000, 100000);
    }
    

    【讨论】:

    • stream() 方法创建与另一个一样多的 Pair 对象。但它不会将它们全部添加到列表中。
    • @JBNizet 很抱歉,只是打字错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-03-26
    • 1970-01-01
    • 2012-05-16
    • 2012-01-31
    • 2019-07-30
    • 1970-01-01
    • 2018-11-17
    相关资源
    最近更新 更多