Java IO & NIO - jak nejrychleji načíst soubor z disku
Bez většího důvodu jsem se začal zajímat jak v Javě co nejrychleji načíst data
z disku. Java nabízí několik způsobů, jednak starý java.io
způsob přes
FileInputStream
a BufferedInputStream
a pak novou java.nio
cestu přes
FileChannel
. Je v nich nějaký rozdíl, popřípadě jak velký?
Proto jsem napsal JMH benchmark (zdroják níže), který načte zdrojové texty části k47čky sražené do jednoho souboru (9.7 MB textu) a počítá kolik je v něm '\n' bajtů. Není nijak důležité, co se s daty dělá, jen s nimi chci dělat co nejmenší množství práce, která může být aspoň okrajově užitečná.
Na OpenJDK 10.0.2 jsou výsledky následující:
Benchmark (bufferSize) Mode Cnt Score Error Units readFile.countNewlines_io_InputStream 1024 thrpt 4 108,321 ± 2,235 ops/s readFile.countNewlines_io_InputStream 8192 thrpt 4 206,031 ± 1,553 ops/s readFile.countNewlines_io_InputStream 65536 thrpt 4 219,326 ± 4,065 ops/s readFile.countNewlines_io_BufferedInputStream 1024 thrpt 4 177,596 ± 36,830 ops/s readFile.countNewlines_io_BufferedInputStream 8192 thrpt 4 206,120 ± 8,747 ops/s readFile.countNewlines_io_BufferedInputStream 65536 thrpt 4 226,468 ± 1,681 ops/s readFile.countNewlines_nio 1024 thrpt 4 119,426 ± 1,142 ops/s readFile.countNewlines_nio 8192 thrpt 4 213,778 ± 1,433 ops/s readFile.countNewlines_nio 65536 thrpt 4 168,841 ± 3,932 ops/s readFile.countNewlines_nio_newInputStream 1024 thrpt 4 108,176 ± 4,005 ops/s readFile.countNewlines_nio_newInputStream 8192 thrpt 4 176,283 ± 0,728 ops/s readFile.countNewlines_nio_newInputStream 65536 thrpt 4 220,651 ± 1,712 ops/s readFile.countNewlines_nio_newInputStream_buffered 1024 thrpt 4 173,761 ± 36,901 ops/s readFile.countNewlines_nio_newInputStream_buffered 8192 thrpt 4 199,666 ± 13,302 ops/s readFile.countNewlines_nio_newInputStream_buffered 65536 thrpt 4 220,418 ± 0,790 ops/s
Je z nich vidět, že mezi jednotlivými metodami není více méně žádný rozdíl.
Záleží jen na granularitě operací. Když čtu data do pole/ByteBuffer
u velikosti 64 kB, je to rychlejší než v případě 1kB bufferu. BufferedInputStream
obsahuje vlastní buffer (ve výchozím stavu 8 kB) a to zlepší výkon, když
čteme data do malých 1kB polí.
Nějaká anomálie se vyskytuje v případě nio
a 64kB bufferu, ale té bych
nepřikládal velkou váhu. Možná jde o důsledek použití přímého buffer
alokovaného v nativní paměti (OS by do něj měl přímo zkopírovat data a nemusí je nejdřív kopírovat do C pole a z něj pak do Java pole, takže by
to mohlo být o něco efektivnější), kdo ví. Když čtu data s 8kB bufferem, v tomto případě nezáleží, jakým způsobem se to děje. Pokud se tedy soubor nachází
v cache OS a nejsem limitován propustností disku (testy čtou téměř 2 GB/s).
To je jedna věc. Druhou je, co se s těmi bajty bude dít potom. Protože když do rovnice započítám utf-8 dekódování, propustnost spadne 5-10×.
Benchmark:
import org.openjdk.jmh.annotations._ @State(Scope.Thread) class readFile { import java.nio.file._ import java.nio._ import java.io._ val f = "k47merged.txt" @Param(Array("1024", "8192", "65536")) var bufferSize: Int = _ @Benchmark def countNewlines_nio = { var count = 0 val bb = ByteBuffer.allocateDirect(bufferSize) val ch = new RandomAccessFile(f, "r").getChannel while (ch.read(bb) != -1) { bb.flip() while (bb.hasRemaining) { if (bb.get == '\n') count += 1 } bb.clear() } ch.close() count } @Benchmark def countNewlines_nio_newInputStream = countNewlines(Files.newInputStream(Paths.get(f)), bufferSize) @Benchmark def countNewlines_nio_newInputStream_buffered = countNewlines(new BufferedInputStream(Files.newInputStream(Paths.get(f))), bufferSize) @Benchmark def countNewlines_io_InputStream = countNewlines(new FileInputStream(f), bufferSize) @Benchmark def countNewlines_io_BufferedInputStream = countNewlines(new BufferedInputStream(new FileInputStream(f)), bufferSize) def countNewlines(is: InputStream, buffer: Int) = { var count = 0 val bb = new Array[Byte](buffer) var n = is.read(bb) while (n != -1) { var i = 0; while (i < n) { if (bb(i) == '\n') count += 1 i += 1 } n = is.read(bb) } is.close() count } }
+1: Dodatek pro úplnost: mmap
v případě takto velkého souboru není rychlejší.