1. 概要

Javaプログラミング言語は、オブジェクトをグループ化するための配列およびコレクションを提供します。 ほとんどの場合、コレクションは配列に支えられており、コレクションに含まれる要素を処理するための一連のメソッドでモデル化されています。

ソフトウェアを開発している間、これらのデータ構造の両方を使用することは非常に一般的です。 したがって、プログラマーは、これらの要素をある形式から別の形式に変換するためのブリッジングメカニズムを必要とします。 ArraysクラスのasListメソッドとCollectionインターフェイスのtoArrayメソッドがこのブリッジを形成します。

このチュートリアルでは、興味深い議論の詳細な分析を行います。使用するtoArrayメソッドとその理由これらをサポートするためにJMH支援ベンチマークも使用します引数。

2. toArrayうさぎの穴

toArray メソッドを意図せずに呼び出す前に、ボックスの内容を理解しましょう。 コレクションインターフェースは、コレクションを配列に変換する2つの方法を提供します。

Object[] toArray()

<T> T[] toArray(T[] a)

どちらのメソッドも、コレクションのすべての要素を含む配列を返します。 これを実証するために、自然数のリストを作成しましょう。

List<Integer> naturalNumbers = IntStream
    .range(1, 10000)
    .boxed()
    .collect(Collectors.toList());

2.1. Collection.toArray()

toArray()メソッドは、コレクションのサイズに等しい長さの新しいメモリ内配列を割り当てます。 内部的に、コレクションをサポートする基になる配列でArrays.copyOfを呼び出します。 したがって、返された配列にはそれへの参照がなく、安全に使用できます。

Object[] naturalNumbersArray = naturalNumbers.toArray();

ただし、結果を単ににキャストすることはできません。 整数[]。 そうすることで、 ClassCastException

2.2. T [] Collection.toArray(T [] a)

パラメータ化されていないメソッドとは異なり、これは事前に割り当てられた配列を引数として受け入れます。 さらに、メソッドの定義で Generics を使用すると、入力と返される配列に同じ型を使用する必要があります。 これにより、以前に観察された Object[]の反復の問題も解決されます。

このバリアントは、入力配列のサイズに基づいて明確に機能します。

  • 事前に割り当てられた配列の長さがコレクションのサイズよりも短い場合、必要な長さと同じタイプの新しい配列が割り当てられます。
Integer[] naturalNumbersArray = naturalNumbers.toArray(new Integer[0]);
  • 入力配列がコレクションの要素を含むのに十分な大きさである場合、それらの要素が内部に含まれて返されます。
Integer[] naturalNumbersArray = naturalNumbers.toArray(new Integer[naturalNumbers.size]);

それでは、より速く、よりパフォーマンスの高い候補を選択するという元の質問に戻りましょう。

3. パフォーマンストライアル

ゼロサイズ(toArray(new T [0]) と事前サイズ(toArray(new T [size])バリアント)を比較する簡単な実験から始めましょう。 トライアルには、人気のあるArrayListAbstractCollectionに裏打ちされたTreeSetを使用します。 また、さまざまなサイズ(小、中、大)のコレクションを含めて、幅広いサンプルデータを取得します。

3.1. JMHベンチマーク

次に、トライアル用のJMH(Java Microbenchmark Harness)ベンチマークをまとめましょう。 ベンチマークのコレクションのサイズとタイプのパラメーターを構成します。

@Param({ "10", "10000", "10000000" })
private int size;

@Param({ "array-list", "tree-set" })
private String type;

さらに、ゼロサイズおよび事前サイズのtoArrayバリアントのベンチマークメソッドを定義します。

@Benchmark
public String[] zero_sized() {
    return collection.toArray(new String[0]);
}

@Benchmark
public String[] pre_sized() {
    return collection.toArray(new String[collection.size()]);
}

3.2. ベンチマーク結果

上記のベンチマークを8vCPU、32 GB RAM、JMH(v1.28)およびJDK(1.8.0_292)を備えたLinux x86_64仮想マシンで実行すると、以下の結果が得られます。 スコアは、ベンチマークされた各メソッドの平均実行時間を操作あたりのナノ秒単位で示します。

値が小さいほど、パフォーマンスが向上します。

Benchmark                   (size)      (type)  Mode  Cnt          Score          Error  Units

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized        10  array-list  avgt   15         24.939 ±        1.202  ns/op
TestBenchmark.pre_sized         10  array-list  avgt   15         38.196 ±        3.767  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized     10000  array-list  avgt   15      15244.367 ±      238.676  ns/op
TestBenchmark.pre_sized      10000  array-list  avgt   15      21263.225 ±      802.684  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized  10000000  array-list  avgt   15   82710389.163 ±  6616266.065  ns/op
TestBenchmark.pre_sized   10000000  array-list  avgt   15  100426920.878 ± 10381964.911  ns/op

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized        10    tree-set  avgt   15         66.802 ±        5.667  ns/op
TestBenchmark.pre_sized         10    tree-set  avgt   15         66.009 ±        4.504  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized     10000    tree-set  avgt   15      85141.622 ±     2323.420  ns/op
TestBenchmark.pre_sized      10000    tree-set  avgt   15      89090.155 ±     4895.966  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized  10000000    tree-set  avgt   15  211896860.317 ± 21019102.769  ns/op
TestBenchmark.pre_sized   10000000    tree-set  avgt   15  212882486.630 ± 20921740.965  ns/op

上記の結果を注意深く観察した後、このトライアルのすべてのサイズとコレクションタイプについて、ゼロサイズのメソッド呼び出しがすべてを勝ち取ることが非常に明白です。

今のところ、これらの数値は単なるデータです。 詳細を理解するために、深く掘り下げて分析してみましょう。

3.3. 割り当て率

仮に、操作ごとのメモリ割り当てが最適化されているため、ゼロサイズのtoArrayメソッド呼び出しは事前サイズのメソッド呼び出しよりもパフォーマンスが優れていると見なすことができます。 別のベンチマークを実行し、ベンチマークされたメソッド平均割り当て率(操作ごとに割り当てられたバイト単位のメモリ)を定量化して、これを明確にしましょう。

JMHは、 ThreadMXBean#getThreadAllocatedBytesを内部的に使用して@ベンチマークごとの割り当て率を計算するGCプロファイラー -prof gc )を提供します。

Benchmark                                                    (size)      (type)  Mode  Cnt          Score           Error   Units

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized:·gc.alloc.rate.norm                     10  array-list  avgt   15         72.000 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                      10  array-list  avgt   15         56.000 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm                  10000  array-list  avgt   15      40032.007 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                   10000  array-list  avgt   15      40016.010 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm               10000000  array-list  avgt   15   40000075.796 ±         8.882    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                10000000  array-list  avgt   15   40000062.213 ±         4.739    B/op

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized:·gc.alloc.rate.norm                     10    tree-set  avgt   15         56.000 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                      10    tree-set  avgt   15         56.000 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm                  10000    tree-set  avgt   15      40055.818 ±        16.723    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                   10000    tree-set  avgt   15      41069.423 ±      1644.717    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm               10000000    tree-set  avgt   15   40000155.947 ±         9.416    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                10000000    tree-set  avgt   15   40000138.987 ±         7.987    B/op

明らかに、上記の数値は、コレクションタイプや toArray バリアントに関係なく、同じサイズの割り当て率がほぼ同じであることを示しています。 したがって、は、メモリ割り当て率の不規則性のために、事前サイズとゼロサイズのtoArrayバリアントのパフォーマンスが異なるという推測的な仮定を否定します

3.4. toArray(T [] a)内部

問題の原因をさらに突き止めるために、ArrayListの内部を詳しく調べてみましょう。

if (a.length < size)
    return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
    a[size] = null;
return a;

基本的に、事前に割り当てられた配列の長さに応じて、基になる要素をコピーするのはArrays.copyOfまたはnative System.arraycopyメソッド呼び出しのいずれかです。コレクションの配列への変換。

さらに、 copyOf メソッドを見ると、最初にコレクションのサイズに等しい長さのコピー配列が作成され、次にSystem.arraycopy呼び出しが続くことがわかります。

T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength]
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
    Math.min(original.length, newLength));

ゼロサイズのメソッドと事前サイズのメソッドの両方が最終的にネイティブのSystem.arraycopyメソッドを呼び出す場合、ゼロサイズのメソッド呼び出しはどのように高速になりますか?

謎は、 toArray(new T [size])メソッドをはるかに遅くする、外部で事前に割り当てられた配列のゼロ初期化の実行に費やされるCPU時間の直接コストにあります。

4. ゼロ初期化

Java言語仕様では、新しくインスタンス化された配列とオブジェクトは、メモリからの不規則な残り物ではなく、デフォルトのフィールド値を持つ必要があると指示されています。 したがって、ランタイムは事前に割り当てられたストレージをゼロにする必要があります。 ベンチマーク実験は、ゼロサイズの配列メソッド呼び出しがゼロ化を回避できることを証明しましたが、事前サイズの場合はできませんでした。

いくつかのベンチマークを考えてみましょう。

@Benchmark
public Foo[] arraycopy_srcLength() {
    Object[] src = this.src;
    Foo[] dst = new Foo[size];
    System.arraycopy(src, 0, dst, 0, src.length);
    return dst;
}

@Benchmark
public Foo[] arraycopy_dstLength() {
    Object[] src = this.src;
    Foo[] dst = new Foo[size];
    System.arraycopy(src, 0, dst, 0, dst.length);
    return dst;
}

実験的観察は、arraycopy_srcLengthベンチマークでの配列割り当ての直後のSystem.arraycopy が、dst配列の事前ゼロ化を回避できることを示しています。 ただし、 arraycopy_dstLengthの実行では、事前ゼロ化を回避できませんでした。

偶然にも、後者の arraycopy_dstLength の場合は、事前サイズ設定された配列メソッド collection.toArray(new String [collection.size()])に似ています。 。

5. 新しいJDKのベンチマーク

最後に、最近リリースされたJDKで元のベンチマークを実行し、新しく大幅に改善されたG1ガベージコレクターを使用するようにJVMを構成します。

# VM version: JDK 11.0.2, OpenJDK 64-Bit Server VM, 11.0.2+9
-----------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score    Error  Units
-----------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  199.920 ± 11.309  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  237.342 ± 14.166  ns/op
-----------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  819.306 ± 85.916  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  972.771 ± 69.743  ns/op
###################################################################################

# VM version: JDK 14.0.2, OpenJDK 64-Bit Server VM, 14.0.2+12-46
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score    Error   Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  158.344 ±   3.862  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  214.340 ±   5.877  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  877.289 ± 132.673  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  934.550 ± 148.660  ns/op

####################################################################################

# VM version: JDK 15.0.2, OpenJDK 64-Bit Server VM, 15.0.2+7-27
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score     Error  Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  147.925 ±   3.968  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  213.525 ±   6.378  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  820.853 ± 105.491  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  947.433 ± 123.782  ns/op

####################################################################################

# VM version: JDK 16, OpenJDK 64-Bit Server VM, 16+36-2231
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score     Error  Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  146.431 ±   2.639  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  214.117 ±   3.679  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  818.370 ± 104.643  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  964.072 ± 142.008  ns/op

####################################################################################

興味深いことに、 toArray(new T [0])メソッドは、 toArray(new T [size])よりも一貫して高速です。 また、そのパフォーマンスは、JDKの新しいリリースごとに絶えず向上しています。

5.1. Java 11 Collection.toArray(IntFunction )。

Java 11では、 コレクションインターフェースは新しいデフォルト toArray を受け入れるメソッド IntFunction 引数としてのジェネレーター(目的のタイプと指定された長さの新しい配列を生成するもの)。

このメソッドは、ゼロの値でジェネレーター関数を呼び出すことにより、新しいT [0]配列の初期化を保証します。これにより、ゼロサイズの toArray(T [])メソッドは常に実行されます。

6. 結論

この記事では、コレクションインターフェイスのさまざまなtoArrayオーバーロードメソッドを調べました。 また、さまざまなJDKでJMHマイクロベンチマークツールを活用したパフォーマンストライアルを実施しました。

ゼロ化の必要性と影響を理解し、内部で割り当てられたアレイがゼロ化を排除してパフォーマンス競争に勝つ方法を観察しました。 最後に、 toArray(new T [0])variantはtoArray(new T [size])よりも高速であるため、常に優先される必要があると断定できます。コレクションを配列に変換する必要がある場合のオプション。

いつものように、この記事で使用されているコードは、GitHubにあります。