Dlaczego System.arraycopy jest natywny w Javie?

84

Byłem zaskoczony, widząc w źródle Java, że ​​System.arraycopy jest metodą natywną.

Oczywiście powodem jest to, że jest szybszy. Ale jakie natywne sztuczki jest w stanie zastosować kod, które czynią go szybszym?

Dlaczego nie po prostu zapętlić oryginalną tablicę i skopiować każdy wskaźnik do nowej tablicy - z pewnością nie jest to takie powolne i uciążliwe?

James B.
źródło

Odpowiedzi:

82

W kodzie natywnym można to zrobić za pomocą pojedynczego memcpy/ memmove, w przeciwieństwie do n odrębnych operacji kopiowania. Różnica w wydajności jest znaczna.

Péter Török
źródło
@Peter, więc w ramach kodu natywnego możesz dostosować się do modelu pamięci Java? (Nigdy nie miałem żadnego powodu, aby popełnić jakąkolwiek rodzimą malarkię)
James B
8
Właściwie tylko niektóre podwizgi arraycopymożna zaimplementować przy użyciu memcpy/ memmove. Inne wymagają sprawdzenia typu w czasie wykonywania dla każdego skopiowanego elementu.
Stephen C
1
@Stephen C, ciekawe - dlaczego tak jest?
Péter Török
3
@ Péter Török - rozważ skopiowanie z Object[]wypełnionego Stringobiektami do pliku String[]. Zobacz ostatni akapit java.sun.com/javase/6/docs/api/java/lang/…
Stephen C
3
Peter, Object [] i byte [] + char [] są najczęściej kopiowanymi, żadne z nich nie wymaga jawnego sprawdzenia typu. Kompilator jest wystarczająco inteligentny, aby NIE sprawdzać, chyba że jest to konieczne, i praktycznie w 99,9% przypadków tak nie jest. Zabawne jest to, że małe kopie (mniej niż wiersz pamięci podręcznej) są dość dominujące, więc "memcpy" dla małych rozmiarów jest naprawdę ważne.
bestsss
16

Nie można go napisać w Javie. Kod natywny może zignorować lub wyeliminować różnicę między tablicami Object i tablicami prymitywów. Java nie może tego zrobić, a przynajmniej nie wydajnie.

I nie można tego napisać pojedynczym memcpy(), ze względu na semantykę wymaganą przez nakładające się tablice.

Markiz Lorne
źródło
5
W porządku, więc memmove. Chociaż nie sądzę, że ma to duże znaczenie w kontekście tego pytania.
Péter Török
Nie memmove () też, zobacz komentarze @Stephen C. na temat innej odpowiedzi.
Markiz Lorne
Widziałem to już, ponieważ to była moja własna odpowiedź ;-) Ale i tak dziękuję.
Péter Török
1
@Geek Tablice, które się nakładają. Jeśli tablice źródłowa i docelowa oraz te same i tylko przesunięcia są różne, zachowanie jest dokładnie określone, a memcpy () nie jest zgodne.
Markiz Lorne
1
Nie można go napisać w Javie? Czy nie można napisać jednej ogólnej metody do obsługi podklas Object, a następnie jednej dla każdego z typów pierwotnych?
Michael Dorst,
10

Jest to oczywiście zależne od implementacji.

HotSpot potraktuje to jako „wewnętrzną” i wstawi kod w miejscu wywołania. To jest kod maszynowy, a nie wolny stary kod C. Oznacza to również, że problemy z podpisem metody w dużej mierze znikną.

Prosta pętla kopiowania jest na tyle prosta, że ​​można do niej zastosować oczywiste optymalizacje. Na przykład rozwijanie pętli. Dokładnie to, co się dzieje, jest ponownie zależne od implementacji.

Tom Hawtin - haczyk
źródło
2
to jest bardzo przyzwoita odpowiedź :), zwł. wspominając o istocie. bez nich prosta iteracja może być szybsza, ponieważ i tak jest zwykle rozwijana przez JIT
bestsss
4

W moich własnych testach System.arraycopy () do kopiowania tablic wielowymiarowych jest 10 do 20 razy szybszy niż przeplatanie dla pętli:

float[][] foo = mLoadMillionsOfPoints(); // result is a float[1200000][9]
float[][] fooCpy = new float[foo.length][foo[0].length];
long lTime = System.currentTimeMillis();
System.arraycopy(foo, 0, fooCpy, 0, foo.length);
System.out.println("native duration: " + (System.currentTimeMillis() - lTime) + " ms");
lTime = System.currentTimeMillis();

for (int i = 0; i < foo.length; i++)
{
    for (int j = 0; j < foo[0].length; j++)
    {
        fooCpy[i][j] = foo[i][j];
    }
}
System.out.println("System.arraycopy() duration: " + (System.currentTimeMillis() - lTime) + " ms");
for (int i = 0; i < foo.length; i++)
{
    for (int j = 0; j < foo[0].length; j++)
    {
        if (fooCpy[i][j] != foo[i][j])
        {
            System.err.println("ERROR at " + i + ", " + j);
        }
    }
}

To drukuje:

System.arraycopy() duration: 1 ms
loop duration: 16 ms
jumar
źródło
9
Chociaż to pytanie jest stare, tylko dla przypomnienia: NIE jest to uczciwy punkt odniesienia (nie mówiąc już o pytaniu, czy taki punkt odniesienia miałby sens). System.arraycopywykonuje płytką kopię (kopiowane są tylko odniesienia do wewnętrznych float[]s), podczas gdy twoje zagnieżdżone for-loops wykonuje głęboką kopię ( floatprzez float). Zmiana na fooCpy[i][j]zostanie odzwierciedlona w fooużyciu System.arraycopy, ale nie będzie używać zagnieżdżonych forpętli.
misberner
4

Jest kilka powodów:

  1. Jest mało prawdopodobne, aby JIT wygenerował tak wydajny kod niskiego poziomu, jak ręcznie napisany kod C. Użycie niskiego poziomu C może umożliwić wiele optymalizacji, które są prawie niemożliwe do wykonania dla ogólnego kompilatora JIT.

    Zobacz ten link po kilka sztuczek i porównań prędkości ręcznie napisanych implementacji C (memcpy, ale zasada jest taka sama): Sprawdź to Optymalizacja Memcpy poprawia szybkość

  2. Wersja C jest prawie niezależna od typu i rozmiaru elementów tablicy. Nie można zrobić tego samego w Javie, ponieważ nie ma możliwości pobrania zawartości tablicy jako surowego bloku pamięci (np. Wskaźnika).

Hrvoje Prgeša
źródło
1
Kod Java może zostać zoptymalizowany. W rzeczywistości generowany jest kod maszynowy, który jest bardziej wydajny niż C.
Tom Hawtin - tackline
Zgadzam się, że czasami kod JITed będzie lepiej zoptymalizowany lokalnie, ponieważ wie, na którym procesorze jest uruchamiany. Jednak ponieważ jest to „w samą porę”, nigdy nie będzie w stanie wykorzystać wszystkich nielokalnych optymalizacji, których wykonanie zajmuje więcej czasu. Ponadto nigdy nie będzie w stanie dopasować ręcznie spreparowanego kodu C (który może również uwzględniać procesor i częściowo zanegować zalety JIT, albo przez kompilację dla określonego procesora, albo przez jakiś rodzaj sprawdzenia w czasie wykonywania).
Hrvoje Prgeša
1
Myślę, że zespół kompilatora Sun JIT zakwestionowałby wiele z tych punktów. Na przykład uważam, że HotSpot przeprowadza globalną optymalizację, aby usunąć niepotrzebne wysyłanie metod, i nie ma powodu, dla którego JIT nie mógłby wygenerować kodu specyficznego dla procesora. Następnie jest punkt, w którym kompilator JIT może przeprowadzić optymalizację gałęzi w oparciu o zachowanie wykonywania bieżącej aplikacji.
Stephen C
@Stephen C - doskonała uwaga na temat optymalizacji gałęzi, chociaż można również wykonać statyczne profilowanie wydajności za pomocą kompilatorów C / C ++, aby osiągnąć podobny efekt. Myślę też, że hotspot ma 2 tryby działania - aplikacje desktopowe nie wykorzystają wszystkich dostępnych optymalizacji, aby osiągnąć rozsądny czas uruchamiania, podczas gdy aplikacje serwerowe będą optymalizowane bardziej agresywnie. Podsumowując, masz pewne zalety, ale niektóre też tracisz.
Hrvoje Prgeša
1
System.arrayCopy nie jest zaimplementowany przy użyciu C, co w pewnym sensie unieważnia tę odpowiedź
Nitsan Wakart