1. 序章

このチュートリアルでは、2つのJavaメソッド System.arraycopy() Arrays.copyOf()のパフォーマンスを確認します。 まず、それらの実装を分析します。 次に、いくつかのベンチマークを実行して、それらの平均実行時間を比較します。

2. System.arraycopy()のパフォーマンス

System.arraycopy()は、指定された位置から開始して、ソース配列から宛先配列の指定された位置に配列の内容をコピーします。 さらに、コピーする前に、JVMはソースタイプと宛先タイプの両方が同じであることを確認します。

System.arraycopy()のパフォーマンスを見積もるときは、ネイティブメソッドであることに注意する必要があります。ネイティブメソッドは、プラットフォームに依存するコード(通常はC)で実装され、JNI呼び出しを介してアクセスされます。

ネイティブメソッドはすでに特定のアーキテクチャ用にコンパイルされているため、実行時の複雑さを正確に見積もることはできません。 さらに、それらの複雑さはプラットフォーム間で異なる可能性があります。 最悪のシナリオはO(N)であると確信できます。 ただし、プロセッサはメモリの連続ブロックを一度に1ブロックずつコピーできるため(Cでは memcpy())、実際の結果はより良くなる可能性があります。

System.arraycopy()の署名のみを表示できます。

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

3. Arrays.copyOf()のパフォーマンス

Arrays.copyOf()は、 System.arraycopy()の実装に加えて追加機能を提供します。 System.arraycopy()は単にソース配列から宛先に値をコピーしますが、 Arrays.copyOf()は新しい配列も作成します。 必要に応じて、コンテンツを切り捨てるか、埋め込みます。

2つ目の違いは、新しい配列はソース配列とは異なるタイプにすることができるということです。 その場合、 JVMはリフレクションを使用するため、パフォーマンスのオーバーヘッドが追加されます

Object 配列で呼び出されると、 copyOf()はリフレクティブ Array.newInstance()メソッドを呼び出します。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    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));
    return copy;
}

ただし、プリミティブをパラメーターとして使用して呼び出された場合、宛先配列を作成するためにリフレクションは必要ありません。

public static int[] copyOf(int[] original, int newLength) {
    int[] copy = new int[newLength];
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

現在、Arrays.copyOf()の実装がSystem.arraycopy()を呼び出していることがはっきりとわかります。 結果として、実行時の実行は同様になります。 疑わしい点を確認するために、プリミティブとオブジェクトの両方をパラメーターとして使用して、上記のメソッドのベンチマークを行います。

4. コードベンチマーク

実際のテストで、どちらのコピー方法が速いかを確認してみましょう。 そのために、 JMH (Javaマイクロベンチマークハーネス)を使用します。 System.arraycopy() Arrays.copyOf()の両方を使用して、ある配列から別の配列に値をコピーする簡単なテストを作成します。

2つのテストクラスを作成します。 1つのテストクラスではプリミティブをテストし、2番目のテストクラスではオブジェクトをテストします。 ベンチマーク構成は、どちらの場合も同じになります。

4.1. ベンチマーク構成

まず、ベンチマークパラメータを定義しましょう。

@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 10)
@Fork(1)
@Measurement(iterations = 100)

ここでは、10回のウォームアップ反復と100回の測定反復で、ベンチマークを1回だけ実行することを指定します。  さらに、平均実行時間を計算し、結果をナノ秒単位で収集したいと思います。 正確な結果を得るには、少なくとも5回のウォームアップ反復を実行することが重要です。

4.2. パラメータの設定

配列の作成ではなく、メソッドの実行に費やされた時間のみを測定するようにする必要があります。 そのために、ベンチマークのセットアップフェーズでソースアレイを初期化します。 大きな数値と小さな数値の両方でベンチマークを実行することをお勧めします。

セットアップメソッドでは、ランダムなパラメーターを使用して配列を初期化するだけです。 まず、プリミティブのベンチマーク設定を定義します。

public class PrimitivesCopyBenchmark {

    @Param({ "10", "1000000" })
    public int SIZE;

    int[] src;

    @Setup
    public void setup() {
        Random r = new Random();
        src = new int[SIZE];

        for (int i = 0; i < SIZE; i++) {
            src[i] = r.nextInt();
        }
    }
}

オブジェクトベンチマークについても同じ設定が続きます。

public class ObjectsCopyBenchmark {

    @Param({ "10", "1000000" })
    public int SIZE;
    Integer[] src;

    @Setup
    public void setup() {
        Random r = new Random();
        src = new Integer[SIZE];

        for (int i = 0; i < SIZE; i++) {
            src[i] = r.nextInt();
        }
    }
}

4.3. テスト

コピー操作を実行する2つのベンチマークを定義します。 まず、 System.arraycopy()を呼び出します。

@Benchmark
public Integer[] systemArrayCopyBenchmark() {
    Integer[] target = new Integer[SIZE];
    System.arraycopy(src, 0, target, 0, SIZE);
    return target;
}

両方のテストを同等にするために、ベンチマークにターゲットアレイの作成を含めました。

次に、 Arrays.copyOf()のパフォーマンスを測定します。

@Benchmark
public Integer[] arraysCopyOfBenchmark() {
    return Arrays.copyOf(src, SIZE);
}

4.4. 結果

テストを実行した後、結果を見てみましょう。

Benchmark                                          (SIZE)  Mode  Cnt        Score       Error  Units
ObjectsCopyBenchmark.arraysCopyOfBenchmark             10  avgt  100        8.535 ±     0.006  ns/op
ObjectsCopyBenchmark.arraysCopyOfBenchmark        1000000  avgt  100  2831316.981 ± 15956.082  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark          10  avgt  100        9.278 ±     0.005  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark     1000000  avgt  100  2826917.513 ± 15585.400  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark          10  avgt  100        9.172 ±     0.008  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark     1000000  avgt  100   476395.127 ±   310.189  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark       10  avgt  100        8.952 ±     0.004  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark  1000000  avgt  100   475088.291 ±   726.416  ns/op

ご覧のとおり、 System.arraycopy() Arrays.copyOf()のパフォーマンスは、プリミティブとIntegerオブジェクトの両方の測定誤差の範囲で異なります。 。 Arrays.copyOf()が内部で System.arraycopy()を使用していることを考えると、当然のことです。 2つのプリミティブint配列を使用したため、リフレクティブ呼び出しは行われませんでした。

JMHは実行時間の概算を提供し、結果はマシンとJVMの間で異なる可能性があることを覚えておく必要があります。

5. 固有の候補者

HotSpot JVM 16では、 Arrays.copyOf() System.arraycopy()の両方が@IntrinsicCandidateとしてマークされていることに注意してください。 このアノテーションは、アノテーションが付けられたメソッドをHotSpotVMによってより高速な低レベルコードに置き換えることができることを意味します。

JITコンパイラーは、(一部またはすべてのアーキテクチャーで)組み込みメソッドをマシン依存の大幅に最適化された命令に置き換えることができます。 ネイティブメソッドはコンパイラにとってブラックボックスであり、呼び出しのオーバーヘッドが大きいため、両方のメソッドのパフォーマンスが向上する可能性があります。 繰り返しますが、このようなパフォーマンスの向上は保証されていません。

6. 結論

この例では、 System.arraycopy()および Arrays.copyOf()のパフォーマンスを調べました。 まず、両方の方法のソースコードを分析しました。 次に、ベンチマークの例を設定して、平均実行時間を測定します。

その結果、 Arrays.copyOf() System.arraycopy()を使用するため、両方のメソッドのパフォーマンスは非常に似ているという理論を確認しました。

いつものように、この記事で使用されている例は、GitHubから入手できます。