JMH - List#addAll 比 ArrayList#new 更快吗?

3

我有一个很简单的JMH基准测试,我读过一些资料,说使用集合的构造函数比使用addAll方法更快。

然而,我的基准测试结果似乎证明了相反的结论。

对此有什么解释吗?

@State(Scope.Benchmark)
public static class Strings {
    public String string = "String";
    public List<String> strings = Arrays.asList("String123", "String456", "String789");
}

@Benchmark
@Fork(value = 5, warmups = 3)
public List<String> withStreams(Strings input) {
    return Stream.concat(Stream.of(input.string), input.strings.stream())
            .collect(Collectors.toList());
}

@Benchmark
@Fork(value = 5, warmups = 3)
public List<String> withoutStreamsButWithConstructor(Strings input) {
    List<String> result = new ArrayList<>(input.strings);
    result.add(input.string);
    return result;
}

@Benchmark
@Fork(value = 5, warmups = 3)
public List<String> withoutStreams(Strings input) {
    List<String> result = new ArrayList<>();
    result.add(input.string);
    result.addAll(input.strings);
    return result;
}

注:关于流的示例只是一个实验(实际上我想确认的事情并不是这个)

结果

# Run complete. Total time: 00:16:31

Benchmark                              Mode  Cnt         Score        Error  Units
App.withStreams                       thrpt  100  12649053,043 ± 222716,712  ops/s
App.withoutStreams                    thrpt  100  50572729,531 ± 324271,706  ops/s
App.withoutStreamsButWithConstructor  thrpt  100  30179733,201 ± 380273,095  ops/s

更新

增加了以下基准测试项目

@Benchmark
@Fork(value = 5, warmups = 3)
public List<String> withoutStreamsWithAddAfter(Strings input) {
    List<String> result = new ArrayList<>();
    result.addAll(input.strings);
    result.add(input.string);
    return result;
}

现在我已经得到

# Run complete. Total time: 00:22:00

Benchmark                              Mode  Cnt         Score        Error  Units
App.withStreams                       thrpt  100  13560464,180 ± 201012,539  ops/s
App.withoutStreams                    thrpt  100  47490197,224 ± 864545,886  ops/s
App.withoutStreamsButWithConstructor  thrpt  100  29412182,733 ± 346228,939  ops/s
App.withoutStreamsWithAddAfter        thrpt  100  31030909,677 ±  81494,995  ops/s

因此,withoutStreams 仍然是最高效的。


更新 #2

我已尝试使用以下 List<String>

@State(Scope.Benchmark)
public static class Strings {
    public String string = "String";
    public List<String> strings = Arrays.asList("String123", "String456", "String789", "StringAbc", "StringDef", "StringGhi", "StringJkl", "StringMno", "StringPqr", "StringStu");
}

现在对于构造函数 @Benchmark 的结果确实更好了。

# Run complete. Total time: 00:22:01

Benchmark                              Mode  Cnt         Score        Error  Units
App.withStreams                       thrpt  100   7291967,397 ± 330614,125  ops/s
App.withoutStreams                    thrpt  100  23575768,665 ± 127039,282  ops/s
App.withoutStreamsButWithConstructor  thrpt  100  27046342,511 ± 182227,005  ops/s
App.withoutStreamsWithAddAfter        thrpt  100  17873682,945 ± 170786,259  ops/s

顺序重要吗?先添加 string 还是先添加 strings - 所有测试都应该保持相同的顺序 - 实际上为什么只有一个 string - user15244370
没有顺序,但我可以在添加后重新运行另一个。 - Yassin Hajaj
1个回答

3
我怀疑原因在于ArrayList的实现方式。 withoutStreamsButWithConstructor创建大小为1的数组,当您调用addAll时,将创建一个大小为4的新数组,首个元素被复制过去,其他元素随后添加。 withoutStreams创建一个大小为10的数组(默认容量),不需要调整大小。
如果strings包含10个元素,则两种方法都需要一次数组调整大小,我怀疑结果会更接近。
总的来说,如果性能很重要,使用以int为参数的构造函数正确地调整列表的大小可能会有所帮助。

谢谢@assylias,我会立即尝试在strings中使用10个元素。 - Yassin Hajaj
2
请注意,这并不否定使用构造函数比使用addAll更快的说法,因为基准测试并没有将构造函数与addAll进行比较。它比较的是“默认构造函数、add、addAll”和“复制构造函数、add”的性能,对于固定的输入大小和Java版本。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接