0xDEADBEEF

[RSS]
««« »»»

Java IO & NIO - jak nejrychleji načíst soubor z disku

24. 12. 2018

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 FileInputStreamBufferedInputStream 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/ByteBufferu velikosti 64kB, je to rychlejší než v případě 1kB bufferu. BufferedInputStream obsahuje vlastní buffer (ve výchozím stavu 8kB) 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ěř 2GB/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-10x.


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

píše k47 (@kaja47, k47)