ByteBuffer.allocate () a ByteBuffer.allocateDirect ()

144

Aby allocate()lub allocateDirect(), oto jest pytanie.

Od kilku lat utknąłem w przekonaniu, że skoro DirectByteBuffers to bezpośrednie mapowanie pamięci na poziomie systemu operacyjnego, będzie działać szybciej z wywołaniami get / put niż HeapByteBuffers. Aż do tej pory nigdy nie byłem zainteresowany poznaniem dokładnych szczegółów tej sytuacji. Chcę wiedzieć, który z dwóch typów ByteBufferjest szybszy i na jakich warunkach.

ROMANIA_engineer
źródło
Aby udzielić konkretnej odpowiedzi, musisz dokładnie powiedzieć, co z nimi robisz. Jeśli jeden byłby zawsze szybszy od drugiego, dlaczego miałyby istnieć dwa warianty. Być może możesz wyjaśnić, dlaczego jesteś teraz „naprawdę zainteresowany poznaniem dokładnych szczegółów”. Przy okazji: czy przeczytałeś kod, szczególnie dla DirectByteBuffer?
Peter Lawrey
Będą one używane do odczytywania i zapisywania do SocketChannels skonfigurowanych na nieblokowanie. Więc biorąc pod uwagę to, co powiedział @bmargulies, DirectByteBuffers będzie działać szybciej na kanałach.
@Gnarly Przynajmniej aktualna wersja mojej odpowiedzi mówi, że kanały powinny odnieść korzyści.
bmargulies

Odpowiedzi:

150

Ron Hitches w swojej doskonałej książce Java NIO wydaje się oferować to, co moim zdaniem może być dobrą odpowiedzią na twoje pytanie:

Systemy operacyjne wykonują operacje we / wy w obszarach pamięci. Z punktu widzenia systemu operacyjnego te obszary pamięci są ciągłymi sekwencjami bajtów. Nie jest więc zaskoczeniem, że tylko bufory bajtów mogą uczestniczyć w operacjach we / wy. Przypomnijmy również, że system operacyjny będzie miał bezpośredni dostęp do przestrzeni adresowej procesu, w tym przypadku procesu JVM, w celu przesłania danych. Oznacza to, że obszary pamięci, które są celami operacji we / wy, muszą być ciągłymi sekwencjami bajtów. W JVM tablica bajtów może nie być przechowywana w pamięci ciągłej lub moduł Garbage Collector może ją przenieść w dowolnym momencie. Tablice to obiekty w Javie, a sposób przechowywania danych w tym obiekcie może się różnić w zależności od implementacji maszyny JVM.

Z tego powodu wprowadzono pojęcie bufora bezpośredniego. Bufory bezpośrednie są przeznaczone do interakcji z kanałami i natywnymi procedurami we / wy. Dokładają wszelkich starań, aby przechowywać elementy bajtowe w obszarze pamięci, którego kanał może używać do bezpośredniego lub surowego dostępu, używając kodu natywnego, aby nakazać systemowi operacyjnemu opróżnienie lub bezpośrednie wypełnienie obszaru pamięci.

Bezpośrednie bufory bajtowe są zwykle najlepszym wyborem dla operacji we / wy. Z założenia obsługują najbardziej wydajny mechanizm we / wy dostępny dla maszyny JVM. Niebezpośrednie bufory bajtów mogą być przekazywane do kanałów, ale może to spowodować spadek wydajności. Zwykle nie jest możliwe, aby niebezpośredni bufor był celem natywnej operacji we / wy. Jeśli przekażesz niebezpośredni obiekt ByteBuffer do kanału w celu zapisu, kanał może niejawnie wykonać następujące czynności przy każdym wywołaniu:

  1. Utwórz tymczasowy bezpośredni obiekt ByteBuffer.
  2. Skopiuj zawartość niebezpośredniego bufora do bufora tymczasowego.
  3. Wykonaj niskopoziomową operację we / wy przy użyciu tymczasowego bufora.
  4. Tymczasowy obiekt bufora wykracza poza zakres i ostatecznie jest usuwany z pamięci.

Może to potencjalnie skutkować kopiowaniem buforów i rezygnacją z obiektów na każdym I / O, czyli dokładnie tego rodzaju rzeczy, których chcielibyśmy uniknąć. Jednak w zależności od implementacji sytuacja może nie wyglądać tak źle. Środowisko wykonawcze prawdopodobnie buforuje i ponownie wykorzystuje bufory bezpośrednie lub wykonuje inne sprytne sztuczki, aby zwiększyć przepustowość. Jeśli po prostu tworzysz bufor do jednorazowego użytku, różnica nie jest znacząca. Z drugiej strony, jeśli będziesz używać bufora wielokrotnie w scenariuszu o wysokiej wydajności, lepiej jest przydzielić bufory bezpośrednie i ponownie ich używać.

Bufory bezpośrednie są optymalne dla operacji we / wy, ale ich tworzenie może być droższe niż niebezpośrednie bufory bajtowe. Pamięć używana przez bufory bezpośrednie jest przydzielana przez wywołanie natywnego kodu specyficznego dla systemu operacyjnego, z pominięciem standardowej sterty maszyny JVM. Konfigurowanie i niszczenie bezpośrednich buforów może być znacznie droższe niż bufory rezydentne, w zależności od systemu operacyjnego hosta i implementacji maszyny JVM. Obszary pamięci buforów bezpośrednich nie podlegają czyszczeniu pamięci, ponieważ znajdują się poza standardową stertą maszyny JVM.

Kompromisy wydajnościowe wynikające z używania buforów bezpośrednich i niebezpośrednich mogą się znacznie różnić w zależności od maszyny JVM, systemu operacyjnego i projektu kodu. Alokując pamięć poza stertą, można poddać aplikację dodatkowym siłom, o których JVM nie jest świadoma. Wprowadzając do gry dodatkowe ruchome części, upewnij się, że osiągasz pożądany efekt. Polecam starą maksymę dotyczącą oprogramowania: najpierw spraw, by działało, a potem szybko. Nie przejmuj się zbytnio optymalizacją z góry; skoncentruj się najpierw na poprawności. Implementacja JVM może być w stanie wykonać buforowanie bufora lub inne optymalizacje, które zapewnią wymaganą wydajność bez zbędnego wysiłku z Twojej strony.

Edwin Dalorzo
źródło
9
Nie podoba mi się ten cytat, ponieważ zawiera zbyt wiele domysłów. Ponadto JVM z pewnością nie musi przydzielać bezpośredniego ByteBuffer podczas wykonywania operacji we / wy dla niebezpośredniego ByteBuffera: wystarczy wykonać malloc sekwencję bajtów na stercie, wykonać IO, skopiować z bajtów do ByteBuffer i zwolnić bajty. Te obszary można nawet zapisać w pamięci podręcznej. Ale przydzielanie do tego obiektu Java jest całkowicie niepotrzebne. Prawdziwe odpowiedzi zostaną uzyskane tylko z pomiaru. Podczas ostatniego pomiaru nie było znaczącej różnicy. Musiałbym powtórzyć testy, aby wymyślić wszystkie szczegółowe szczegóły.
Robert Klemme,
4
Wątpliwe jest, czy książka opisująca NIO (i rodzime operacje) może zawierać w sobie pewne pewniki. W końcu różne maszyny JVM i systemy operacyjne zarządzają różnymi rzeczami w różny sposób, więc nie można winić autora za to, że nie jest w stanie zagwarantować określonego zachowania.
Martin Tuskevicius
@RobertKlemme, +1, wszyscy nienawidzimy domysłów, jednak zmierzenie wydajności dla wszystkich głównych systemów operacyjnych może być niemożliwe, ponieważ jest ich zbyt wiele. Podjęto taką próbę w innym poście , ale możemy zobaczyć wiele problemów z jego testem porównawczym, zaczynając od „wyników wahają się znacznie w zależności od systemu operacyjnego”. A co jeśli jest czarna owca, która robi okropne rzeczy, takie jak kopiowanie bufora na każdym I / O? Wtedy z powodu tej owcy możemy zostać zmuszeni do uniemożliwienia pisania kodu, którego w innym przypadku używalibyśmy, tylko po to, aby uniknąć tych najgorszych scenariuszy.
Pacerier
@RobertKlemme Zgadzam się. Jest tu o wiele za dużo domysłów. Na przykład jest znikome prawdopodobieństwo, że JVM rzadko przydziela tablice bajtów.
Markiz Lorne
@Edwin Dalorzo: Po co nam taki bufor bajtów w prawdziwym świecie? Czy zostały wymyślone jako hack do współdzielenia pamięci między procesami? Załóżmy na przykład, że JVM działa na procesie i byłby to inny proces działający w sieci lub warstwie łącza danych - która jest odpowiedzialna za przesyłanie danych - czy te bufory bajtów są przydzielane do współdzielenia pamięci między tymi procesami? Proszę mnie poprawić, jeśli się mylę ...
Tom Taylor,
25

Nie ma powodu, aby oczekiwać bezpośrednie bufory być szybszy dostęp do wewnątrz JVM. Ich przewaga pojawia się, gdy przekażesz je do kodu natywnego - na przykład kodu znajdującego się za wszelkiego rodzaju kanałami.

bmargulies
źródło
W rzeczy samej. Na przykład, gdy trzeba wykonać operacje we / wy w Scala / Java i wywołać wbudowane biblioteki Python / natywne z dużymi danymi w pamięci do przetwarzania algorytmicznego lub przesłać dane bezpośrednio do GPU w Tensorflow.
SemanticBeeng
21

ponieważ DirectByteBuffers to bezpośrednie mapowanie pamięci na poziomie systemu operacyjnego

Nie są. Są po prostu zwykłą pamięcią procesu aplikacji, ale nie podlegają relokacji podczas Java GC, co znacznie upraszcza rzeczy wewnątrz warstwy JNI. To, co opisujesz, dotyczy MappedByteBuffer.

że będzie działać szybciej z wywołaniami get / put

Wniosek nie wynika z przesłanki; przesłanka jest fałszywa; a wniosek również jest fałszywy. Są szybsze, gdy znajdziesz się w warstwie JNI, a jeśli czytasz i piszesz z tej samej DirectByteBuffer, są znacznie szybsze, ponieważ dane nigdy nie muszą w ogóle przekraczać granicy JNI.

Markiz Lorne
źródło
7
To dobry i ważny punkt: na ścieżce IO trzeba w pewnym momencie przekroczyć granicę Java - JNI . Bezpośrednie i niebezpośrednie bufory bajtowe przesuwają tylko granicę: w przypadku bezpośredniego bufora wszystkie operacje put z języka Java muszą się przekroczyć, natomiast w przypadku niebezpośredniego bufora wszystkie operacje we / wy muszą przekroczyć. To, co jest szybsze, zależy od aplikacji.
Robert Klemme
@RobertKlemme Twoje podsumowanie jest nieprawidłowe. W przypadku wszystkich buforów wszelkie dane przychodzące i wychodzące z Javy muszą przekroczyć granicę JNI. Celem bezpośrednich buforów jest to, że jeśli tylko kopiujesz dane z jednego kanału do drugiego, np. Przesyłając plik, nie musisz wcale przenosić go do Javy, co jest znacznie szybsze.
Markiz Lorne
gdzie dokładnie moje podsumowanie jest nieprawidłowe? A od jakiego „podsumowania” na początek? Mówiłem wyraźnie o „operacjach put z ziemi Java”. Jeśli kopiujesz dane tylko między kanałami (tj. Nigdy nie musisz zajmować się danymi w środowisku Java), to oczywiście inna historia.
Robert Klemme
@RobertKlemme Twoje stwierdzenie, że „z bezpośrednim buforem [tylko] wszystkie operacje put z języka Java muszą się krzyżować” jest niepoprawne. Zarówno dostaje, jak i stawia, muszą przejść.
Markiz Lorne
EJP, najwyraźniej nadal brakuje ci zamierzonego rozróżnienia, które @RobertKlemme robił, decydując się na użycie słów „operacje wstawiania” w jednej frazie i używając słów „operacje we / wy” w skontrastowanej frazie zdania. W tym ostatnim zdaniu jego zamiarem było odniesienie się do operacji między buforem a urządzeniem dostarczonym przez system operacyjny.
naki
18

Najlepiej wykonać własne pomiary. Szybka odpowiedź wydaje się być taka, że ​​wysyłanie z allocateDirect()bufora zajmuje od 25% do 75% mniej czasu niż allocate()wariant (testowany jako kopiowanie pliku do / dev / null), w zależności od rozmiaru, ale sama alokacja może być znacznie wolniejsza (nawet o współczynnik 100x).

Źródła:

Raph Levien
źródło
Dzięki. Przyjąłbym twoją odpowiedź, ale szukam bardziej szczegółowych informacji dotyczących różnic w wydajności.