web-dev-qa-db-ja.com

配列をコピーするのにclone()が最適な方法なのはなぜですか?

私にとっては残念ですが、私はそれを知りませんでした:

配列をコピーするには、一般に最速の方法であるため、クローンを使用してください。

josh Blochがこのブログで述べているように: http://www.artima.com/intv/bloch13.html

私は常にSystem.arraycopy(...)を使用しました。どちらのアプローチもネイティブであるため、おそらくライブラリのソースを深く掘り下げることなく、なぜそうなのかを理解できません。

私の質問は簡単です:なぜfastestなのですか? _System.arraycopy_との違いは何ですか? 違いは ここ で説明されていますが、なぜJosh Blochがclone()を最速の方法と見なすのかという質問には答えていません。

28
Andremoniy

clone()System.arraycopy(..)や他のものよりも配列をコピーする最も速い方法である理由について、いくつかの点を述べたいと思います。

1。clone()は、提供されたソース配列をデスティネーション配列にコピーする前に型チェックを行う必要はありません こちら 。単純に新しいメモリ空間を割り当て、オブジェクトを割り当てます。一方、System.arraycopy(..)は型をチェックしてから配列をコピーします。

2。clone()も最適化を中断して、冗長なゼロ化を排除します。ご存じのように、Javaに割り当てられたすべての配列は、_0s_またはそれぞれのデフォルト値で初期化する必要があります。ただし、JITは、作成直後に配列がいっぱいになった場合、この配列のゼロ化を回避できます。これにより、既存の_0s_またはそれぞれのデフォルト値でコピー値を変更するのに比べて、確実に高速になります。 System.arraycopy(..)を使用すると、初期化された配列の消去とコピーにかなりの時間がかかります。そのために、ベンチマークテストのいくつかを実行しました。

_@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, batchSize = 1000)
@Measurement(iterations = 10, time = 1, batchSize = 1000)
public class BenchmarkTests {

    @Param({"1000","100","10","5", "1"})
    private int size;
    private int[] original;

    @Setup
    public void setup() {
        original = new int[size];
        for (int i = 0; i < size; i++) {
            original[i] = i;
        }
    }

    @Benchmark
    public int[] SystemArrayCopy() {
        final int length = size;
        int[] destination = new int[length];
        System.arraycopy(original, 0, destination, 0, length);
        return destination;
    }


    @Benchmark
    public int[] arrayClone() {
        return original.clone();
    }

}
_

出力:

_Benchmark                        (size)   Mode  Cnt       Score      Error  Units
ArrayCopy.SystemArrayCopy            1  thrpt   10   26324.251 ± 1532.265  ops/s
ArrayCopy.SystemArrayCopy            5  thrpt   10   26435.562 ± 2537.114  ops/s
ArrayCopy.SystemArrayCopy           10  thrpt   10   27262.200 ± 2145.334  ops/s
ArrayCopy.SystemArrayCopy          100  thrpt   10   10524.117 ±  474.325  ops/s
ArrayCopy.SystemArrayCopy         1000  thrpt   10     984.213 ±  121.934  ops/s
ArrayCopy.arrayClone                 1  thrpt   10   55832.672 ± 4521.112  ops/s
ArrayCopy.arrayClone                 5  thrpt   10   48174.496 ± 2728.928  ops/s
ArrayCopy.arrayClone                10  thrpt   10   46267.482 ± 4641.747  ops/s
ArrayCopy.arrayClone               100  thrpt   10   19837.480 ±  364.156  ops/s
ArrayCopy.arrayClone              1000  thrpt   10    1841.145 ±  110.322  ops/s
_

出力によると、cloneSystem.arraycopy(..)からほぼ2倍高速であることがわかります。

3。また、clone()のような手動コピー方法を使用すると、VM呼び出し(System.arraycopy()とは異なります)。

20
procrastinator

一つには、clone()System.arraycopy()が行うタイプチェックを行う必要がない。

4
user207421

以前の回答を修正して補完したいと思います。

  1. Object.cloneは、配列に対して未チェックのSystem.arraycopy実装を使用します。
  2. Object.cloneの主なパフォーマンスの改善は、RAWメモリの初期化です。 System.arraycopyの場合、ソースコードで見ることができるように、配列の初期化とコピー操作を組み合わせようとしますが、Object.cloneとは異なり、これに対して異なる追加チェックも行います。この機能を無効にすると(下記参照)、パフォーマンスは非常に近くなります(特に私のハードウェアでは)。
  3. もう1つの興味深い点は、Young対Old Genについてです。ソース配列がOld Gen内で整列している場合、両方のメソッドのパフォーマンスは近いです。
  4. プリミティブ配列をコピーするとき、System.arraycopyは常にgenerate_unchecked_arraycopyを使用します。
  5. ハードウェア/ OS依存の実装に依存するため、ベンチマークと仮定を信頼せず、自分で確認してください。

説明

まず、クローンメソッドとSystem.arraycopyは組み込み関数です。 Object.cloneおよびSystem.arraycopyはgenerate_unchecked_arraycopyを使用します。さらに深く掘り下げると、HotSpotの後、OSなどに依存する具体的な実装が選択されることがわかります。

長く。 Hotspot のコードを見てみましょう。まず、Object.clone(LibraryCallKit :: inline_native_clone)がgenerate_arraycopyを使用し、-XX:-ReduceInitialCardMarksの場合にSystem.arraycopyに使用されることを確認します。それ以外の場合、LibraryCallKit :: copy_to_cloneを実行し、RAWメモリ内の新しい配列を初期化します(デフォルトで有効になっている-XX:+ ReduceBulkZeroingの場合)。対照的に、System.arraycopyはgenerate_arraycopyを直接使用し、ReduceBulkZeroing(および他の多くの場合)をチェックし、前述の追加チェックで配列のゼロ化を排除し、Object.cloneとは異なり、すべての要素が初期化されていることを確認する追加チェックを行います。最後に、最良の場合、両方ともgenerate_unchecked_arraycopyを使用します。

以下に、この効果が実際にどのように作用するかを示すベンチマークをいくつか示します。

  1. 1つ目は単純なベンチマークであり、前の回答との唯一の違いは、配列がソートされていないことです。 arraycopyの方が遅いことがわかります(2倍ではありません)、結果- https://Pastebin.com/ny56Ag1z ;
  2. 次に、オプション-XX:-ReduceBulkZeroingを追加すると、両方の方法のパフォーマンスが非常に近くなっていることがわかります。結果- https://Pastebin.com/ZDAeQWwx ;
  3. また、配列のアライメントのために、Old/Youngの違いがあると仮定します(Java GCの機能であり、GCを呼び出すと、配列のアライメントが変更され、簡単です [〜#〜] jol [〜#〜] )を使用して観察するには、一般にパフォーマンスが同じになり、両方の方法でダウングレードされることに驚いた。結果- https:// Pastebin.com/bTt5SJ8r 。具体的な数値を信じている人にとって、System.arraycopyのスループットはObject.cloneよりも優れています。

最初のベンチマーク:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import Java.util.concurrent.ThreadLocalRandom;
import Java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopy {

    @Param({"10", "1000", "100000"})
    int size;

    int[] source;

    @Setup(Level.Invocation)
    public void setup() {
        source = create(size);
    }

    @Benchmark
    public int[] clone(CloneVsArraycopy cloneVsArraycopy) {
        return cloneVsArraycopy.source.clone();
    }

    @Benchmark
    public int[] arraycopy(CloneVsArraycopy cloneVsArraycopy) {
        int[] dest = new int[cloneVsArraycopy.size];
        System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
        return dest;
    }

    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(CloneVsArraycopy.class.getSimpleName())
                .warmupIterations(20)
                .measurementIterations(20)
                .forks(20)
                .build()).run();
    }

    private static int[] create(int size) {
        int[] a = new int[size];
        for (int i = 0; i < a.length; i++) {
            a[i] = ThreadLocalRandom.current().nextInt();
        }
        return a;
    }

}

PCでこのテストを実行すると、これが得られました- https://Pastebin.com/ny56Ag1z 。違いはそれほど大きくありませんが、まだ存在しています。

2番目のベンチマークでは、1つの設定-XX:-ReduceBulkZeroingのみを追加し、この結果を得ました https://Pastebin.com/ZDAeQWwx =。いいえ、Young Genの場合、その差もはるかに小さいことがわかります。

3番目のベンチマークでは、セットアップ方法のみを変更し、ReduceBulkZeroingオプションを有効に戻しました。

@Setup(Level.Invocation)
public void setup() {
    source = create(size);
    // try to move to old gen/align array
    for (int i = 0; i < 10; ++i) {
        System.gc();
    }
}

違いははるかに小さいです(エラー間隔にある可能性があります)- https://Pastebin.com/bTt5SJ8r

免責事項

それも間違っている可能性があります。自分で確認する必要があります。

さらに

ベンチマークプロセスを見るのは興味深いと思います。

# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.arraycopy
# Parameters: (size = 50000)

# Run progress: 0,00% complete, ETA 00:07:30
# Fork: 1 of 5
# Warmup Iteration   1: 8,870 ops/ms
# Warmup Iteration   2: 10,912 ops/ms
# Warmup Iteration   3: 16,417 ops/ms <- Hooray!
# Warmup Iteration   4: 17,924 ops/ms <- Hooray!
# Warmup Iteration   5: 17,321 ops/ms <- Hooray!
# Warmup Iteration   6: 16,628 ops/ms <- What!
# Warmup Iteration   7: 14,286 ops/ms <- No, stop, why!
# Warmup Iteration   8: 13,928 ops/ms <- Are you kidding me?
# Warmup Iteration   9: 13,337 ops/ms <- pff
# Warmup Iteration  10: 13,499 ops/ms
Iteration   1: 13,873 ops/ms
Iteration   2: 16,177 ops/ms
Iteration   3: 14,265 ops/ms
Iteration   4: 13,338 ops/ms
Iteration   5: 15,496 ops/ms

Object.cloneの場合

# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.clone
# Parameters: (size = 50000)

# Run progress: 0,00% complete, ETA 00:03:45
# Fork: 1 of 5
# Warmup Iteration   1: 8,761 ops/ms
# Warmup Iteration   2: 12,673 ops/ms
# Warmup Iteration   3: 20,008 ops/ms
# Warmup Iteration   4: 20,340 ops/ms
# Warmup Iteration   5: 20,112 ops/ms
# Warmup Iteration   6: 20,061 ops/ms
# Warmup Iteration   7: 19,492 ops/ms
# Warmup Iteration   8: 18,862 ops/ms
# Warmup Iteration   9: 19,562 ops/ms
# Warmup Iteration  10: 18,786 ops/ms

ここでSystem.arraycopyのパフォーマンスのダウングレードを確認できます。 Streamsについても同様の図を見ましたが、コンパイラにバグがありました。コンパイラのバグかもしれません。とにかく、3回のウォームアップ後、パフォーマンスが低下するのは奇妙です。

[〜#〜] update [〜#〜]

型チェックについて

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import Java.util.concurrent.ThreadLocalRandom;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.atomic.AtomicLong;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopyObject {

    @Param({"100"})
    int size;

    AtomicLong[] source;

    @Setup(Level.Invocation)
    public void setup() {
        source = create(size);
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public AtomicLong[] clone(CloneVsArraycopyObject cloneVsArraycopy) {
        return cloneVsArraycopy.source.clone();
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public AtomicLong[] arraycopy(CloneVsArraycopyObject cloneVsArraycopy) {
        AtomicLong[] dest = new AtomicLong[cloneVsArraycopy.size];
        System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
        return dest;
    }

    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(CloneVsArraycopyObject.class.getSimpleName())
                .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining", "-XX:-ReduceBulkZeroing")
                .warmupIterations(10)
                .measurementIterations(5)
                .forks(5)
                .build())
                .run();
    }

    private static AtomicLong[] create(int size) {
        AtomicLong[] a = new AtomicLong[size];
        for (int i = 0; i < a.length; i++) {
            a[i] = new AtomicLong(ThreadLocalRandom.current().nextLong());
        }
        return a;
    }

}

違いは観察されません- https://Pastebin.com/ufxCZVaC 。 System.arraycopyはその場合ホット組み込みなので、説明は簡単だと思います。実際の実装は、型チェックなどをすることなくインライン化されます。

私はあなたが読むのが面白いと思うRadiodefに同意しました ブログ投稿 、このブログの著者は [〜#〜] jmh [〜#〜]の作成者(または作成者の一人)です

3
egorlitvinenko

パフォーマンスの違いは、配列がゼロになるステップをスキップすることから生じます。

public static int[] copyUsingArraycopy(int[] original)
{
    // Memory is allocated and zeroed out
    int[] copy = new int[original.Length];
    // Memory is copied
    System.arraycopy(original, 0, copy, 0, original.length);
}

public static int[] copyUsingClone(int[] original)
{
    // Memory is allocated, but not zeroed out
    // Unitialized memory is then copied into
    return (int[])original.clone();
}

ただし、配列のコピーのパフォーマンスが大きく異なる場合は、一般にダブルバッファリングを使用することをお勧めします。

int[] backBuffer = new int[BUFFER_SIZE];
int[] frontBuffer = new int[BUFFER_SIZE];

...

// Swap buffers
int[] temp = frontBuffer;
frontBuffer = backBuffer;
backBuffer = temp;
System.arraycopy(frontBuffer, 0, backBuffer, 0, BUFFER_SIZE);
0
Paul Smith