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?

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.


@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.


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.


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

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).


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
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.