1. 序章

このチュートリアルでは、従来のJDKコレクションとEclipseコレクションのパフォーマンスを比較します。 さまざまなシナリオを作成し、結果を調査します。

2. 構成

まず、この記事では、デフォルトの構成を使用してテストを実行することに注意してください。 ベンチマークにはフラグやその他のパラメータは設定されません。

次のハードウェアとライブラリを使用します。

プロジェクトを作成する最も簡単な方法は、コマンドラインを使用することです。

mvn archetype:generate \
  -DinteractiveMode=false \
  -DarchetypeGroupId=org.openjdk.jmh \
  -DarchetypeArtifactId=jmh-java-benchmark-archetype \
  -DgroupId=com.baeldung \
  -DartifactId=benchmark \
  -Dversion=1.0

その後、お気に入りのIDEを使用してプロジェクトを開き、 pom.xml を編集して、Eclipseコレクションの依存関係を追加できます。

<dependency>
    <groupId>org.eclipse.collections</groupId>
    <artifactId>eclipse-collections</artifactId>
    <version>10.0.0</version>
</dependency>
<dependency>
    <groupId>org.eclipse.collections</groupId>
    <artifactId>eclipse-collections-api</artifactId>
    <version>10.0.0</version>
</dependency>

3. 最初のベンチマーク

最初のベンチマークは単純です。 以前に作成したリストof整数の合計を計算します。

シリアルおよびパラレルで実行しながら、6つの異なる組み合わせをテストします。

private List<Integer> jdkIntList;
private MutableList<Integer> ecMutableList;
private ExecutorService executor;
private IntList ecIntList;

@Setup
public void setup() {
    PrimitiveIterator.OfInt iterator = new Random(1L).ints(-10000, 10000).iterator();
    ecMutableList = FastList.newWithNValues(1_000_000, iterator::nextInt);
    jdkIntList = new ArrayList<>(1_000_000);
    jdkIntList.addAll(ecMutableList);
    ecIntList = ecMutableList.collectInt(i -> i, new IntArrayList(1_000_000));
    executor = Executors.newWorkStealingPool();
}

@Benchmark
public long jdkList() {
    return jdkIntList.stream().mapToLong(i -> i).sum();
}

@Benchmark
public long ecMutableList() {
    return ecMutableList.sumOfInt(i -> i);
}

@Benchmark
public long jdkListParallel() {
    return jdkIntList.parallelStream().mapToLong(i -> i).sum();
}

@Benchmark
public long ecMutableListParallel() {
    return ecMutableList.asParallel(executor, 100_000).sumOfInt(i -> i);
}

@Benchmark
public long ecPrimitive() { 
    return this.ecIntList.sum(); 
}

@Benchmark
public long ecPrimitiveParallel() {
    return this.ecIntList.primitiveParallelStream().sum(); 
}

最初のベンチマークを実行するには、以下を実行する必要があります。

mvn clean install
java -jar target/benchmarks.jar IntegerListSum -rf json

これにより、 IntegerListSum クラスでベンチマークがトリガーされ、結果がJSONファイルに保存されます。

テストではスループットまたは1秒あたりの操作数を測定するため、高いほど良いです。

Benchmark                              Mode  Cnt     Score       Error  Units
IntegerListSum.ecMutableList          thrpt   10   573.016 ±    35.865  ops/s
IntegerListSum.ecMutableListParallel  thrpt   10  1251.353 ±   705.196  ops/s
IntegerListSum.ecPrimitive            thrpt   10  4067.901 ±   258.574  ops/s
IntegerListSum.ecPrimitiveParallel    thrpt   10  8827.092 ± 11143.823  ops/s
IntegerListSum.jdkList                thrpt   10   568.696 ±     7.951  ops/s
IntegerListSum.jdkListParallel        thrpt   10   918.512 ±    27.487  ops/s

私たちのテストによると、Eclipseコレクションのプリミティブの並列リストはすべての中で最高のスループットを示しました。 また、これは最も効率的であり、並列で実行されているJavaJDKのほぼ10倍のパフォーマンスでした。

もちろん、その一部は、プリミティブリストを使用する場合、ボクシングとアンボクシングに関連するコストがないという事実によって説明できます。

JMHVisualizerを使用して結果を分析できます。 以下のチャートは、より良い視覚化を示しています。

4. フィルタリング

次に、リストを変更して、5の倍数であるすべての要素を取得します。 以前のベンチマークの大部分とフィルター関数を再利用します。

private List<Integer> jdkIntList;
private MutableList<Integer> ecMutableList;
private IntList ecIntList;
private ExecutorService executor;

@Setup
public void setup() {
    PrimitiveIterator.OfInt iterator = new Random(1L).ints(-10000, 10000).iterator();
    ecMutableList = FastList.newWithNValues(1_000_000, iterator::nextInt);
    jdkIntList = new ArrayList<>(1_000_000);
    jdkIntList.addAll(ecMutableList);
    ecIntList = ecMutableList.collectInt(i -> i, new IntArrayList(1_000_000));
    executor = Executors.newWorkStealingPool();
}

@Benchmark
public List<Integer> jdkList() {
    return jdkIntList.stream().filter(i -> i % 5 == 0).collect(Collectors.toList());
}

@Benchmark
public MutableList<Integer> ecMutableList() {
    return ecMutableList.select(i -> i % 5 == 0);
}


@Benchmark
public List<Integer> jdkListParallel() {
    return jdkIntList.parallelStream().filter(i -> i % 5 == 0).collect(Collectors.toList());
}

@Benchmark
public MutableList<Integer> ecMutableListParallel() {
    return ecMutableList.asParallel(executor, 100_000).select(i -> i % 5 == 0).toList();
}

@Benchmark
public IntList ecPrimitive() {
    return this.ecIntList.select(i -> i % 5 == 0);
}

@Benchmark
public IntList ecPrimitiveParallel() {
    return this.ecIntList.primitiveParallelStream()
      .filter(i -> i % 5 == 0)
      .collect(IntLists.mutable::empty, MutableIntList::add, MutableIntList::addAll);
}

前と同じようにテストを実行します。

mvn clean install
java -jar target/benchmarks.jar IntegerListFilter -rf json

そして結果:

Benchmark                                 Mode  Cnt     Score    Error  Units
IntegerListFilter.ecMutableList          thrpt   10   145.733 ±  7.000  ops/s
IntegerListFilter.ecMutableListParallel  thrpt   10   603.191 ± 24.799  ops/s
IntegerListFilter.ecPrimitive            thrpt   10   232.873 ±  8.032  ops/s
IntegerListFilter.ecPrimitiveParallel    thrpt   10  1029.481 ± 50.570  ops/s
IntegerListFilter.jdkList                thrpt   10   155.284 ±  4.562  ops/s
IntegerListFilter.jdkListParallel        thrpt   10   445.737 ± 23.685  ops/s

ご覧のとおり、EclipseCollectionsPrimitiveが再び勝者となりました。 JDK並列リストの2倍以上のスループットを実現します。

フィルタリングの場合、並列処理の効果がより明確になることに注意してください。 加算はCPUにとって安価な操作であり、シリアルとパラレルの間に同じ違いは見られません。

また、Eclipse Collectionsプリミティブリストが以前に取得したパフォーマンスの向上は、各要素で行われた作業がボクシングとアンボクシングのコストを上回り始めるにつれて蒸発し始めます。

最後に、プリミティブに対する操作はオブジェクトよりも高速であることがわかります。

5. 結論

この記事では、JavaコレクションとEclipseコレクションを比較するためのベンチマークをいくつか作成しました。 JMHを活用して、環境バイアスを最小限に抑えようとしました。

いつものように、ソースコードはGitHubから入手できます。