Czy lepiej jest używać System.arraycopy (…) niż pętli for do kopiowania tablic?


90

Chcę utworzyć nową tablicę obiektów składającą dwie mniejsze tablice.

Nie mogą być zerowe, ale rozmiar może wynosić 0.

Nie mogę wybrać między tymi dwoma sposobami: czy są one równoważne, czy też jeden bardziej wydajny (na przykład system.arraycopy () kopiuje całe fragmenty)?

MyObject[] things = new MyObject[publicThings.length+privateThings.length];
System.arraycopy(publicThings, 0, things, 0, publicThings.length);
System.arraycopy(privateThings, 0, things,  publicThings.length, privateThings.length);

lub

MyObject[] things = new MyObject[publicThings.length+privateThings.length];
for (int i = 0; i < things.length; i++) {
    if (i<publicThings.length){
        things[i] = publicThings[i]
    } else {
        things[i] = privateThings[i-publicThings.length]        
    }
}

Czy jedyną różnicą jest wygląd kodu?

EDYCJA: dziękuję za powiązane pytanie, ale wydaje się, że mają nierozwiązaną dyskusję:

Czy jest to naprawdę szybsze, jeśli it is not for native types: bajt [], obiekt [], znak []? we wszystkich innych przypadkach wykonywana jest kontrola typu, co byłoby moim przypadkiem, a więc byłoby równoważne ... nie?

W innym powiązanym pytaniu mówią, że the size matters a lotdla rozmiaru> 24 wygrywa system.arraycopy (), dla mniejszych niż 10, ręczna pętla jest lepsza ...

Teraz jestem naprawdę zdezorientowany.


16
arraycopy()jest rodzimym połączeniem, które z pewnością jest szybsze.
Sotirios Delimanolis,

4
Czy próbowałeś porównać dwie różne implementacje?
Alex,


15
Powinieneś wybrać to, co uważasz za najbardziej czytelne i najłatwiejsze do utrzymania w przyszłości. Dopiero gdy ustalisz, że jest to źródło wąskiego gardła, powinieneś zmienić swoje podejście.
arshajii,

1
Nie wynajduj koła na nowo!
camickr

Odpowiedzi:


87
public void testHardCopyBytes()
{
    byte[] bytes = new byte[0x5000000]; /*~83mb buffer*/
    byte[] out = new byte[bytes.length];
    for(int i = 0; i < out.length; i++)
    {
        out[i] = bytes[i];
    }
}

public void testArrayCopyBytes()
{
    byte[] bytes = new byte[0x5000000]; /*~83mb buffer*/
    byte[] out = new byte[bytes.length];
    System.arraycopy(bytes, 0, out, 0, out.length);
}

Wiem, że testy JUnit nie są najlepsze do testów porównawczych, ale
wykonanie testu testHardCopyBytes zajęło 0,157 s,
a
testArrayCopyBytes - 0,086 s.

Myślę, że zależy to od maszyny wirtualnej, ale wygląda na to, że kopiuje bloki pamięci zamiast kopiować pojedyncze elementy tablicy. To zdecydowanie zwiększyłoby wydajność.

EDYCJA:
Wygląda na to, że wydajność System.arraycopy jest wszędzie. Gdy zamiast bajtów używane są ciągi znaków, a tablice są małe (rozmiar 10), otrzymuję następujące wyniki:

    String HC:  60306 ns
    String AC:  4812 ns
    byte HC:    4490 ns
    byte AC:    9945 ns

Oto, jak to wygląda, gdy tablice mają rozmiar 0x1000000. Wygląda na to, że System.arraycopy zdecydowanie wygrywa z większymi tablicami.

    Strs HC:  51730575 ns
    Strs AC:  24033154 ns
    Bytes HC: 28521827 ns
    Bytes AC: 5264961 ns

Jakie to dziwne!

Dzięki, Daren, za zwrócenie uwagi, że referencje kopiują się inaczej. To sprawiło, że był to znacznie bardziej interesujący problem!


2
Dziękuję za twój wysiłek, ale przegapiłeś pozornie kluczowe punkty: typy nienatywne (utwórz losową klasę z dowolnym kątem, aby tablica zawierała odniesienia) i rozmiar ... wydaje się, że przy mniejszym rozmiarze tablicy, ręczna pętla for jest szybsza. Chcesz to poprawić?
Daren

2
Och wow, masz rację! To jest interestracyjne. Umieszczenie ciągów znaków w tych tablicach zamiast bajtów robi ogromną różnicę: <<< testHardCopyStrs: 0,161s >>> <<< testArrayCopyStrs: 0,170s >>>
Trent Small

Jakie otrzymujesz wyniki? interesująca byłaby również próba z wielkością tablicy = 10 ... dzięki! (Żałuję, że nie mam tutaj mojego IDE, koduję bez kompilatora).
Daren

Teraz zawarłem je w niektóre wywołania System.nanoTime () i ustawiłem rozmiar = 10, aby zobaczyć, ile nanosekund trwa każde. Wygląda na to, że w przypadku małych prymitywnych tablic pętle są lepsze; w przypadku odniesień arrayCopy jest lepsza .: <<< testHardCopyBytes: 4491 ns >>> <<< testHardCopyStrs: 56778 ns >>> <<< testArrayCopyBytes: 10265 ns >>> <<< testArrayCopyStrs: 4490 ns >>>
Trent Mały

Bardzo ciekawe wyniki! Dziękuję bardzo! czy możesz edytować swoją odpowiedź, aby to uwzględnić, a ja z chęcią ją zaakceptuję, aby wszyscy mogli ją zobaczyć jako pierwsza ... masz już swój głos. :)
Daren,

36

Arrays.copyOf(T[], int)jest łatwiejszy do odczytania. Używa wewnętrznego połączenia, System.arraycopy()które jest rodzimym połączeniem.

Nie da się tego szybciej!


wydaje się, że możesz polegać na kilku rzeczach, ale dzięki za zwrócenie uwagi na tę funkcję, której nie znałem i jest rzeczywiście łatwiejsza do odczytania. :)
Daren

tak! zależy to od kilku rzeczy, jak powiedział @Svetoslav Tsolov. chciałem tylko wskazać Arrays.copyOf
Philipp Sander

2
copyOfnie zawsze można zastąpić arraycopy, ale jest to odpowiednie w tym przypadku użycia.
Blaisorblade

1
NB Jeśli patrzysz na wydajność, to nie będzie tak szybkie jak System.arraycopy (), ponieważ wymaga alokacji pamięci. Jeśli jest to pętla, powtarzające się alokacje doprowadzą do czyszczenia pamięci, co będzie ogromnym spadkiem wydajności.
Will Calderwood

1
@PhilippSander Aby sprawdzić, czy nie jestem głupi, dodałem kod do kopii tablicy 1MB w mojej pętli gry, która prawie nigdy nie uruchamia GC. Z Array.copyOf () mój DVM dzwonił do GC 5 razy na sekundę i gra stała się bardzo lagowa. Myślę, że można bezpiecznie powiedzieć, że występuje alokacja pamięci.
Will Calderwood

17

Zależy to od maszyny wirtualnej, ale System.arraycopy powinien zapewniać najbliższą wydajność natywną.

Pracowałem przez 2 lata jako programista Java dla systemów wbudowanych (gdzie wydajność jest ogromnym priorytetem) i wszędzie tam, gdzie można użyć System.arraycopy, najczęściej go używałem / widziałem w istniejącym kodzie. W przypadku problemów z wydajnością jest zawsze preferowana zamiast pętli. Jeśli wydajność nie jest dużym problemem, wybrałbym pętlę. O wiele łatwiejsze do odczytania.


Wydaje się, że rzeczy takie jak rozmiar i typ tablicy (podstawowe lub dziedziczone) mają wpływ na wydajność.
Daren

2
Tak, to nie jest „natywna wydajność” jako taka, dlatego powiedziałem, że używam go „głównie” tam, gdzie mogę (zauważysz, że wygrywa głównie z kopiowaniem w pętli). Myślę, że przyczyna jest taka: kiedy jest to mała tablica prymitywnego typu, „koszt połączenia” jest większy niż wzrost wydajności. Korzystanie z JNI może obniżyć wydajność z tego samego powodu - sam kod natywny jest szybki, ale wywołanie go z procesu Java - nie tak bardzo.

Drobna poprawka, Arrays.copy to nie JNI, to jest nieodłączne. Funkcje wewnętrzne są znacznie szybsze niż JNI. To, jak i kiedy kompilator JIT przekształca go w wewnętrzną, zależy od używanej maszyny JVM / kompilatora.
Nitsan Wakart

1
Arrays.copynie istnieje, Arrays.copyOfjest funkcją biblioteczną.
Blaisorblade

14

Zamiast polegać na spekulacjach i prawdopodobnie nieaktualnych informacjach, przeprowadziłem testy porównawcze, używając . W rzeczywistości Caliper zawiera kilka przykładów, w tym odpowiedź, CopyArrayBenchmarkktóra dokładnie mierzy to pytanie! Wszystko, co musisz zrobić, to biec

mvn exec:java -Dexec.mainClass=com.google.caliper.runner.CaliperMain -Dexec.args=examples.CopyArrayBenchmark

Moje wyniki są oparte na 64-bitowej maszynie wirtualnej Oracle Java HotSpot (TM) Server VM, 1.8.0_31-b13, działającej na MacBooku Pro z połowy 2010 roku (macOS 10.11.6 z Intel Arrandale i7, 8 GiB RAM). Nie wierzę, że publikowanie surowych danych dotyczących czasu jest przydatne. Raczej podsumuję wnioski za pomocą pomocniczych wizualizacji.

W podsumowaniu:

  • Pisanie ręcznej forpętli w celu skopiowania każdego elementu do nowo utworzonej tablicy nigdy nie jest korzystne, zarówno w przypadku krótkich, jak i długich tablic.
  • Arrays.copyOf(array, array.length)i array.clone()oba są konsekwentnie szybkie. Te dwie techniki są prawie identyczne pod względem wydajności; który wybierzesz to kwestia gustu.
  • System.arraycopy(src, 0, dest, 0, src.length)jest prawie tak szybki jak i , ale nie do końca konsekwentnie. (Zobacz przypadek dla 50000 s.) Z tego powodu i szczegółowości wywołania polecam, jeśli potrzebujesz dokładnej kontroli nad tym, które elementy są kopiowane i gdzie.Arrays.copyOf(array, array.length)array.clone()intSystem.arraycopy()

Oto wykresy czasowe:

Czasy kopiowania tablic o długości 5 Czasy kopiowania tablic o długości 500 Czasy kopiowania tablic o długości 50000


3
Czy jest coś dziwnego w kopiowaniu int? Wydaje się dziwne, że arraykopia byłaby powolna na ints w dużych skalach.
whaleberg

6

Wykonywanie metod natywnych, takich jak, Arrays.copyOf(T[], int)wiąże się z pewnym narzutem, ale nie oznacza to, że nie jest szybkie, ponieważ wykonujesz je za pomocą JNI.

Najłatwiej jest napisać benchmark i przetestować.

Możesz sprawdzić, czy Arrays.copyOf(T[], int)jest szybszy niż normalna forpętla.

Kod testu porównawczego stąd : -

public void test(int copySize, int copyCount, int testRep) {
    System.out.println("Copy size = " + copySize);
    System.out.println("Copy count = " + copyCount);
    System.out.println();
    for (int i = testRep; i > 0; --i) {
        copy(copySize, copyCount);
        loop(copySize, copyCount);
    }
    System.out.println();
}

public void copy(int copySize, int copyCount) {
    int[] src = newSrc(copySize + 1);
    int[] dst = new int[copySize + 1];
    long begin = System.nanoTime();
    for (int count = copyCount; count > 0; --count) {
        System.arraycopy(src, 1, dst, 0, copySize);
        dst[copySize] = src[copySize] + 1;
        System.arraycopy(dst, 0, src, 0, copySize);
        src[copySize] = dst[copySize];
    }
    long end = System.nanoTime();
    System.out.println("Arraycopy: " + (end - begin) / 1e9 + " s");
}

public void loop(int copySize, int copyCount) {
    int[] src = newSrc(copySize + 1);
    int[] dst = new int[copySize + 1];
    long begin = System.nanoTime();
    for (int count = copyCount; count > 0; --count) {
        for (int i = copySize - 1; i >= 0; --i) {
            dst[i] = src[i + 1];
        }
        dst[copySize] = src[copySize] + 1;
        for (int i = copySize - 1; i >= 0; --i) {
            src[i] = dst[i];
        }
        src[copySize] = dst[copySize];
    }
    long end = System.nanoTime();
    System.out.println("Man. loop: " + (end - begin) / 1e9 + " s");
}

public int[] newSrc(int arraySize) {
    int[] src = new int[arraySize];
    for (int i = arraySize - 1; i >= 0; --i) {
        src[i] = i;
    }
    return src;
}

System.arraycopy()używa JNI (Java Native Interface) do kopiowania tablicy (lub jej części), więc jest niesamowicie szybki, co możesz potwierdzić tutaj


Ten kod używa int [], możesz go wypróbować z ciągiem [] (zainicjowanym różnymi wartościami: "1", "2" itd., Ponieważ są one niezmienne
Daren

1
JNI jest bardzo powolny . System.arraycopynie używa go.
Chai T. Rex

Nie, System.arraycopynie używa JNI, który służy tylko do wywoływania bibliotek stron trzecich. Zamiast tego jest to wywołanie natywne, co oznacza, że ​​istnieje dla niego natywna implementacja w maszynie wirtualnej.
sfenik

6

Nie jest możliwe, żeby Arrays.copyOfto było szybsze niż, System.arraycopyponieważ jest to implementacja copyOf:

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;
}

4

System.arraycopy()jest rodzimym wywołaniem, które wykonuje operację kopiowania bezpośrednio w pamięci. Pojedyncza kopia w pamięci byłaby zawsze szybsza niż twoja pętla for


3
Czytałem, że dla typów innych niż natywne (jakakolwiek stworzona klasa, jak moja) może nie być tak wydajna ... a dla małych rozmiarów (w moim przypadku) instrukcja pętli może być lepsza ... czy chcesz skomentować?
Daren

Rzeczywiście, System.arraycopy () ma pewien narzut, więc dla małych tablic (n = ~ 10) pętla jest w rzeczywistości szybsza
RecursiveExceptionException
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.