Wydajność / użyteczność Java NIO FileChannel a FileOutputstream

169

Próbuję dowiedzieć się, czy jest jakaś różnica w wydajności (lub zaletach), gdy używamy nio w FileChannelporównaniu z normalnym FileInputStream/FileOuputStreamdo odczytu i zapisu plików w systemie plików. Zauważyłem, że na moim komputerze oba działają na tym samym poziomie, również wielokrotnie FileChanneldroga jest wolniejsza. Czy mogę poznać więcej szczegółów porównujących te dwie metody. Oto kod, którego użyłem, plik, z którym testuję, jest w pobliżu 350MB. Czy dobrym rozwiązaniem jest używanie klas opartych na NIO do wejścia / wyjścia plików, jeśli nie patrzę na dostęp losowy lub inne takie zaawansowane funkcje?

package trialjavaprograms;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class JavaNIOTest {
    public static void main(String[] args) throws Exception {
        useNormalIO();
        useFileChannel();
    }

    private static void useNormalIO() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        InputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        byte[] buf = new byte[64 * 1024];
        int len = 0;
        while((len = is.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.flush();
        fos.close();
        is.close();
        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }

    private static void useFileChannel() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        FileInputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        FileChannel f = is.getChannel();
        FileChannel f2 = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        long len = 0;
        while((len = f.read(buf)) != -1) {
            buf.flip();
            f2.write(buf);
            buf.clear();
        }

        f2.close();
        f.close();

        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }
}
Keshav
źródło
5
transferTo/ transferFrombyłoby bardziej konwencjonalne przy kopiowaniu plików. Niezależnie od tego, która technika nie powinna przyspieszyć ani zwolnić dysku twardego, chociaż myślę, że może wystąpić problem, jeśli odczytuje małe fragmenty na raz i powoduje, że głowa spędza nadmierną ilość czasu na szukaniu.
Tom Hawtin - tackline
1
(Nie wspominasz o używanym systemie operacyjnym ani dostawcy JRE i wersji).
Tom Hawtin - tackline
Ups, przepraszam, używam FC10 z Sun JDK6.
Keshav

Odpowiedzi:

202

Moje doświadczenie z większymi rozmiarami plików java.niojest szybsze niż java.io. Solidnie szybciej. Jak w zakresie> 250%. To powiedziawszy, eliminuję oczywiste wąskie gardła, na które, jak sądzę, może ucierpieć Twój mikro-benchmark. Potencjalne obszary do zbadania:

Rozmiar bufora. Algorytm, który w zasadzie masz, to

  • skopiuj z dysku do bufora
  • skopiuj z bufora na dysk

Z własnego doświadczenia wynika, że ​​ten rozmiar bufora jest gotowy do dostrojenia. Zdecydowałem się na 4KB dla jednej części mojej aplikacji, 256KB dla innej. Podejrzewam, że Twój kod cierpi z powodu tak dużego bufora. Uruchom testy porównawcze z buforami 1KB, 2KB, 4KB, 8KB, 16KB, 32KB i 64KB, aby to udowodnić.

Nie wykonuj testów porównawczych Java, które odczytują i zapisują na tym samym dysku.

Jeśli tak, to naprawdę testujesz dysk, a nie Javę. Sugerowałbym również, że jeśli twój procesor nie jest zajęty, prawdopodobnie masz inne wąskie gardło.

Nie używaj bufora, jeśli nie musisz.

Po co kopiować do pamięci, jeśli celem jest inny dysk lub karta sieciowa? W przypadku większych plików powstałe opóźnienie jest nietrywialne.

Jak powiedzieli inni, użyj FileChannel.transferTo()lub FileChannel.transferFrom(). Kluczową zaletą jest to, że JVM wykorzystuje dostęp systemu operacyjnego do DMA ( bezpośredni dostęp do pamięci ), jeśli jest obecny. (Jest to zależne od implementacji, ale nowoczesne wersje Sun i IBM na procesorach ogólnego przeznaczenia są dobre.) Co się dzieje, to dane są przesyłane bezpośrednio do / z dysku, do magistrali, a następnie do celu ... omijając dowolny obwód przez RAM lub procesor.

Aplikacja internetowa, nad którą pracowałem przez całe dnie i noc, jest bardzo wymagająca. Zrobiłem też mikro testy porównawcze i testy porównawcze w świecie rzeczywistym. A wyniki są na moim blogu, spójrzcie:

Korzystaj z danych i środowisk produkcyjnych

Mikro-benchmarki są podatne na zniekształcenia. Jeśli możesz, postaraj się zebrać dane dokładnie tego, co planujesz, z oczekiwanym obciążeniem, na sprzęcie, którego oczekujesz.

Moje testy porównawcze są solidne i niezawodne, ponieważ odbywały się na systemie produkcyjnym, solidnym systemie, systemie pod obciążeniem, zebranym w dziennikach. Nie jest to 2,5-calowy dysk SATA 7200 obr./min mojego notebooka, podczas gdy intensywnie obserwowałem pracę maszyny JVM z dyskiem twardym.

Po czym biegasz? To ma znaczenie.

Stu Thompson
źródło
@Stu Thompson - Dziękujemy za wiadomość. Natknąłem się na twoją odpowiedź, ponieważ prowadzę badania na ten sam temat. Próbuję zrozumieć ulepszenia systemu operacyjnego, które nio przedstawia programistom Java. Kilka z nich to pliki DMA i mapowane w pamięci. Czy spotkałeś więcej takich ulepszeń? PS - Twoje linki do bloga są uszkodzone.
Andy Dufresne
@AndyDufresne mój blog w tej chwili nie działa, zostanie uruchomiony jeszcze w tym tygodniu - w trakcie przenoszenia.
Stu Thompson,
1
Co powiesz na kopiowanie plików z jednego katalogu do drugiego? (Każdy inny dysk)
Deckard
1
Interesujące: mailinator.blogspot.in/2008/02/…
Jeryl Cook
38

Jeśli rzeczą, którą chcesz porównać, jest wydajność kopiowania plików, to dla testu kanału powinieneś zrobić to:

final FileInputStream inputStream = new FileInputStream(src);
final FileOutputStream outputStream = new FileOutputStream(dest);
final FileChannel inChannel = inputStream.getChannel();
final FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
inputStream.close();
outputStream.close();

Nie będzie to wolniejsze niż buforowanie się z jednego kanału do drugiego i potencjalnie będzie znacznie szybsze. Według Javadocs:

Wiele systemów operacyjnych może przesyłać bajty bezpośrednio z pamięci podręcznej systemu plików do kanału docelowego bez faktycznego ich kopiowania.

uckelman
źródło
7

Na podstawie moich testów (Win7 64bit, 6GB RAM, Java6), transfer z NIO jest szybki tylko w przypadku małych plików i staje się bardzo wolny w przypadku większych plików. Przerzucanie bufora danych NIO zawsze przewyższa standardowe IO.

  • Kopiowanie 1000x2MB

    1. NIO (transferFrom) ~ 2300ms
    2. NIO (bezpośredni databuffer 5000b flip) ~ 3500ms
    3. Standardowe IO (bufor 5000b) ~ 6000ms
  • Kopiowanie 100x20mb

    1. NIO (bezpośredni bufor danych 5000b flip) ~ 4000ms
    2. NIO (transferFrom) ~ 5000ms
    3. Standardowe IO (bufor 5000b) ~ 6500ms
  • Kopiowanie 1x1000mb

    1. NIO (bezpośredni databuffer 5000b flip) ~ 4500s
    2. Standardowe IO (bufor 5000b) ~ 7000ms
    3. NIO (transferFrom) ~ 8000 ms

Metoda transferTo () działa na fragmentach pliku; nie był pomyślany jako metoda kopiowania plików wysokiego poziomu: jak skopiować duży plik w systemie Windows XP?

Erkki Nokso-Koivisto
źródło
6

Odpowiadając na część pytania dotyczącą użyteczności:

Jedną raczej subtelną wadą używania FileChannelover FileOutputStreamjest to, że wykonanie którejkolwiek z operacji blokujących (np. read()Lub write()) z wątku, który jest w stanie przerwania , spowoduje nagłe zamknięcie kanału java.nio.channels.ClosedByInterruptException.

To mogłoby być dobrą rzeczą, gdyby cokolwiek FileChannelbyło używane, jest częścią głównej funkcji wątku, a projekt wziął to pod uwagę.

Ale może być również nieznośny, jeśli jest używany przez jakąś funkcję pomocniczą, taką jak funkcja rejestrowania. Na przykład możesz znaleźć wyjście rejestrowania nagle zamknięte, jeśli funkcja rejestrowania zostanie wywołana przez wątek, który również został przerwany.

Szkoda, że ​​jest to tak subtelne, ponieważ nieuwzględnienie tego może prowadzić do błędów wpływających na integralność zapisu. [1] [2]

antak
źródło
3

Przetestowałem wydajność FileInputStream w porównaniu z FileChannel do dekodowania plików zakodowanych w base64. W moich doświadczeniach testowałem dość duży plik i tradycyjne io było zawsze nieco szybsze niż nio.

FileChannel mógł mieć przewagę we wcześniejszych wersjach jvm ze względu na narzut synchronizacji w kilku klasach powiązanych z operacjami io, ale nowoczesne jvm całkiem dobrze radzi sobie z usuwaniem niepotrzebnych blokad.

Jörn Horstmann
źródło
2

Jeśli nie używasz funkcji transferTo lub funkcji nieblokujących, nie zauważysz różnicy między tradycyjnymi IO i NIO (2), ponieważ tradycyjne IO są mapowane na NIO.

Ale jeśli możesz korzystać z funkcji NIO, takich jak transfer z / do, lub chcesz korzystać z buforów, to oczywiście NIO jest najlepszym rozwiązaniem.

eckes
źródło
0

Z mojego doświadczenia wynika, że ​​NIO działa znacznie szybciej z małymi plikami. Ale jeśli chodzi o duże pliki, FileInputStream / FileOutputStream jest znacznie szybszy.

tangens
źródło
4
Pomieszałeś to? Z własnego doświadczenia wiem, że java.niojest szybszy w przypadku większych plików niż java.io, a nie mniejszych.
Stu Thompson
Nie, moje doświadczenie jest odwrotne. java.niojest szybki, o ile plik jest wystarczająco mały, aby można go było zmapować do pamięci. Jeśli robi się większy (200 MB i więcej), java.iojest szybszy.
tangens
Łał. Całkowite przeciwieństwo mnie. Pamiętaj, że nie musisz koniecznie mapować pliku, aby go odczytać - można odczytać z pliku FileChannel.read(). Nie ma tylko jednego podejścia do odczytu plików przy użyciu java.nio.
Stu Thompson
2
@tangens sprawdziłeś to?
sinedsem