08 stycznia 2024 blog java jmh io

Jak najwydajniej odczytać plik?

Java Microbenchmark Harness

JMH jest przyjemnym frameworkiem do testowania wydajności. Po dodaniu zależności do pom.xml wystarczy dodanie kilku adnotacji, żeby przetestować jakiś fragment kodu.

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class FileReaderBenchmark {

    @Setup
    public void setup() {
    }

    @TearDown
    public void tearDown() {
    }

    @Benchmark
    public void ,ethod() {
    }
}

Odczyt pliku

W Javie istnieje kilka sposobów odczytu plików, najbardziej popularny to chyba new BufferedInputStream(new FileInputStream(new File(fileName))).

Też szukając informacji natknąłem się na Tuning Java I/O Performance gdzie między innymi wykorzystano new RandomAccessFile(fileName, "r").

Ostatnią metodą, ale moim zdaniem najciekawszą jest new RandomAccessFile(fileName, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileSize). Dużo artykułów opisujących memory mapped file odczytuje bajty pojedynczo, ale o wiele wydajniej jest, jeśli czytamy dane w większych porcjach.

Przetestujmy wydajność tych 3 metod przy pomocy JMH.

@Benchmark
public long bufferedInputStream(Blackhole blackhole) throws Exception {
    byte[] buffer = new byte[1024 * 16];
    int k;
    try (UsingBufferedInputStream reader = new UsingBufferedInputStream(filename)) {
        while ((k = reader.read(buffer, 0, buffer.length)) > 0) {
            blackhole.consume(k);
            blackhole.consume(buffer);
        }
    }
    return 0;
}

@Benchmark
public long readRandom(Blackhole blackhole) throws Exception {
    try (UsingReadRandom reader = new UsingReadRandom(filename)) {
        long pos = 0;
        int c;
        byte buf[] = new byte[1];
        while ((c = reader.read(pos)) != -1) {
            pos++;
            buf[0] = (byte) c;
            blackhole.consume(c);
            blackhole.consume(buf);
        }
    }
    return 0;
}

@Benchmark
public long bufferedMemory(Blackhole blackhole) throws Exception {
    byte[] buffer = new byte[1024 * 16];
    int k;
    try (UsingBufferedMemoryMappedFile reader = new UsingBufferedMemoryMappedFile(filename)) {
        while ((k = reader.read(buffer, 0, buffer.length)) > 0) {
            blackhole.consume(k);
            blackhole.consume(buffer);
        }
    }
    return 0;
}

Cały kod benchmarku.

Co z tego wyszło

Benchmark                                Mode  Cnt  Score   Error  Units
FileReaderBenchmark.bufferedInputStream  avgt    5  0.142 ± 0.010   s/op
FileReaderBenchmark.bufferedMemory       avgt    5  0.093 ± 0.005   s/op
FileReaderBenchmark.readRandom           avgt    5  1.987 ± 0.637   s/op

Na moim systemie odczyt 1GB pliku przy użyciu memory mapped file średnio trwał 93 milisekundy. Połowę wolniejszy był BufferedInputStream.

Dla porównania dd if=/dev/urandom of=1G bs=1M count=1000; hyperfine "dd if=1G bs=16K of=/dev/null" daje wynik:

Benchmark 1: dd if=1G bs=16K of=/dev/null
  Time (mean ± σ):     105.1 ms ±   8.9 ms    [User: 4.4 ms, System: 101.2 ms]
  Range (min … max):    98.6 ms … 134.6 ms    21 runs

Nie tak źle Java, nie tak źle.

Linki