web-dev-qa-db-ja.com

BufferedReader read()がreadLine()よりもずっと遅いのはなぜですか?

一度に1文字ずつファイルを読み取る必要があり、BufferedReaderread()メソッドを使用しています。 *

read()readLine()よりも約10倍遅いことがわかりました。これは予想されますか?それとも私は何か間違ったことをしていますか?

Java 7.のベンチマークです。入力テストファイルには、約500万行と2億5400万文字(〜242 MB)**があります。**:

read()メソッドは、すべての文字を読み取るのに約7000ミリ秒かかります。

_@Test
public void testRead() throws IOException, UnindexableFastaFileException{

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa")));

    long t0= System.currentTimeMillis();
    int c;
    while( (c = fa.read()) != -1 ){
        //
    }
    long t1= System.currentTimeMillis();
    System.err.println(t1-t0); // ~ 7000 ms

}
_

readLine()メソッドの所要時間はわずか700ミリ秒です。

_@Test
public void testReadLine() throws IOException{

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa")));

    String line;
    long t0= System.currentTimeMillis();
    while( (line = fa.readLine()) != null ){
        //
    }
    long t1= System.currentTimeMillis();
    System.err.println(t1-t0); // ~ 700 ms
}
_

*実用的な目的:改行文字(_\n_または_\r\n_)を含む各行の長さを知る必要がありますそれらを除去した後の行の長さ。行が_>_文字で始まるかどうかも知る必要があります。与えられたファイルに対して、これはプログラムの開始時に一度だけ行われます。 EOL文字はBufferedReader.readLine()によって返されないので、read()メソッドに頼っています。これを行うより良い方法があれば、言ってください。

** gzip圧縮されたファイルはこちら http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz 。不思議に思われる方のために、私はfastaファイルのインデックスを作成するクラスを書いています。

39
dariober

パフォーマンスを分析する際に重要なことは、開始する前に有効なベンチマークを取得することです。それでは、ウォームアップ後の予想パフォーマンスを示す簡単なJMHベンチマークから始めましょう。

考慮しなければならないことの1つは、最新のオペレーティングシステムは定期的にアクセスされるファイルデータをキャッシュするため、テスト間でキャッシュをクリアする方法が必要だということです。 Windowsには小さなユーティリティがあります これだけを行います -Linuxでは、どこかに疑似ファイルを書き込むことでそれを行うことができます。

コードは次のようになります。

_import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;

import Java.io.BufferedReader;
import Java.io.FileReader;
import Java.io.IOException;

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public class IoPerformanceBenchmark {
    private static final String FILE_PATH = "test.fa";

    @Benchmark
    public int readTest() throws IOException, InterruptedException {
        clearFileCaches();
        int result = 0;
        try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
            int value;
            while ((value = reader.read()) != -1) {
                result += value;
            }
        }
        return result;
    }

    @Benchmark
    public int readLineTest() throws IOException, InterruptedException {
        clearFileCaches();
        int result = 0;
        try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
            String line;
            while ((line = reader.readLine()) != null) {
                result += line.chars().sum();
            }
        }
        return result;
    }

    private void clearFileCaches() throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder("EmptyStandbyList.exe", "standbylist");
        pb.inheritIO();
        pb.start().waitFor();
    }
}
_

そしてそれを実行すると

_chcp 65001 # set codepage to utf-8
mvn clean install; Java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
_

次の結果が得られます(キャッシュをクリアするのに約2秒必要です。これをHDDで実行しているため、あなたよりもかなり遅いです)。

_Benchmark                            Mode  Cnt  Score   Error  Units
IoPerformanceBenchmark.readLineTest  avgt   20  3.749 ± 0.039   s/op
IoPerformanceBenchmark.readTest      avgt   20  3.745 ± 0.023   s/op
_

驚き!予想どおり、JVMが安定モードに落ち着いた後は、パフォーマンスの違いはまったくありません。ただし、readCharTestメソッドには1つの外れ値があります。

_# Warmup Iteration   1: 6.186 s/op
# Warmup Iteration   2: 3.744 s/op
_

まさにあなたが見ている問題です。私が考えることができる最も可能性の高い理由は、OSRがここで良い仕事をしていないか、JITが最初の反復で違いを生むには遅すぎるだけであるということです。

ユースケースによっては、これは大きな問題または無視できる場合があります(数千のファイルを読んでいる場合は重要ではありません。1つだけ読んでいる場合はこれが問題です)。

このような問題を解決するのは簡単ですが、一般的な解決策はありませんが、これを処理する方法はあります。正しい軌道に乗っているかどうかを確認する簡単なテストは、_-Xcomp_オプションを使用してコードを実行することです。これにより、HotSpotは最初の呼び出しですべてのメソッドをコンパイルします。そして実際にそうすると、最初の呼び出しでの大きな遅延が消えます:

_# Warmup Iteration   1: 3.965 s/op
# Warmup Iteration   2: 3.753 s/op
_

可能な解決策

実際の問題がよくわかったので(私の推測では、これらのロックはすべて合体されておらず、効率的なバイアスロック実装を使用していません)、解決策はかなり単純で単純です:関数呼び出しの数を減らします上記のすべてがなくてもこのソリューションにたどり着くことができましたが、問題をよく把握できていて、多くのコードを変更する必要のないソリューションがあったかもしれません)。

次のコードは他の2つのいずれよりも一貫して高速に実行されます-配列サイズで遊ぶことができますが、驚くほど重要ではありません(おそらく他のメソッドread(char[])がロックを取得する必要がないため、呼び出しは最初から低い)。

_private static final int BUFFER_SIZE = 256;
private char[] arr = new char[BUFFER_SIZE];

@Benchmark
public int readArrayTest() throws IOException, InterruptedException {
    clearFileCaches();
    int result = 0;
    try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
        int charsRead;
        while ((charsRead = reader.read(arr)) != -1) {
            for (int i = 0; i < charsRead; i++) {
                result += arr[i];
            }
        }
    }
    return result;
} 
_

これはおそらく十分なパフォーマンスの点で十分ですが、 file mapping を使用してパフォーマンスをさらに向上させたい場合は、このような場合にあまり大きな改善は期待しませんが、テキストが常にASCIIであることを知っている場合は、さらに最適化を行うとパフォーマンスが向上します。

35
Voo

これがpractical私自身の質問への答えです:BufferedReader.read()を使用せず、代わりにFileChannelを使用してください。 (明らかに、タイトルに入れた理由には答えていません)。以下に、手早くて汚いベンチマークを示します。他の人が役に立つと思います。

_@Test
public void testFileChannel() throws IOException{

    FileChannel fileChannel = FileChannel.open(Paths.get("chr1.fa"));
    long n= 0;
    int noOfBytesRead = 0;

    long t0= System.nanoTime();

    while(noOfBytesRead != -1){
        ByteBuffer buffer = ByteBuffer.allocate(10000);
        noOfBytesRead = fileChannel.read(buffer);
        buffer.flip();
        while ( buffer.hasRemaining() ) {
            char x= (char)buffer.get();
            n++;
        }
    }
    long t1= System.nanoTime();
    System.err.println((float)(t1-t0) / 1e6); // ~ 250 ms
    System.err.println("nchars: " + n); // 254235640 chars read
}
_

ファイルごとに1文字ずつ読み込むのに約250ミリ秒かかるため、この戦略はBufferedReader.readLine()はもちろん、read()(〜700ミリ秒)よりもかなり高速です。 _x == '\n'_と_x == '>'_をチェックするループにifステートメントを追加しても、ほとんど違いはありません。また、StringBuilderを配置して行を再構築しても、タイミングにあまり影響しません。ですから、これは私にとっては十分に良いことです(少なくとも今のところ)。

FileChannelについて言及してくれた@ Marco13に感謝します。

2
dariober

@Vooに感謝します。以下で言及したことは、FileReader#read() v/s BufferedReader#readLine()の観点から正しいが、BufferedReader#read() v/s BufferedReader#readLine()の観点からは正しくないので、私は答えを取り消しました。

BufferedReaderread()メソッドを使用することは良い考えではありません。害を及ぼすことはありませんが、クラスの目的は確かに無駄になります。

BufferedReaderのライフサイクル全体の目的は、コンテンツをバッファリングすることでI/Oを減らすことです。 here をJavaチュートリアルで読むことができます。BufferedReaderread()メソッドは実際にはReaderから継承されますが、readLine()BufferedReader独自のメソッドです。

read()メソッドを使用する場合は、FileReaderを使用することをお勧めします。これは、その目的のためのものです。ここでJavaチュートリアルを読んで read )できます。

だから、あなたの質問への答えは非常に簡単だと思います(ベンチマークとその説明に行くことなく)-

  • read()は基盤となるOSによって処理され、ディスクアクセス、ネットワークアクティビティ、または比較的高価な他の操作をトリガーします。
  • readLine()を使用すると、これらのオーバーヘッドをすべて節約できるため、readLine()read()よりも常に高速になります。小さなデータでは実質的に高速ではありません。
1
hagrawal

考えれば、この違いを見て驚くことではありません。 1つのテストはテキストファイル内の行の反復で、もう1つのテストは文字の反復です。

各行に1文字が含まれていない限り、readLine()read()メソッドよりもはるかに高速であることが期待されます(上記のコメントで指摘されているように、BufferedReaderは入力。ただし、物理ファイルの読み取りだけがパフォーマンス取得操作ではない場合があります)

2の違いを実際にテストする場合は、両方のテストで各キャラクターを反復処理するセットアップをお勧めします。例えば。何かのようなもの:

void readTest(BufferedReader r)
{
    int c;
    StringBuilder b = new StringBuilder();
    while((c = r.read()) != -1)
        b.append((char)c);
}

void readLineTest(BufferedReader r)
{
    String line;
    StringBuilder b = new StringBuilder();
    while((line = b.readLine())!= null)
        for(int i = 0; i< line.length; i++)
            b.append(line.charAt(i));
}

上記の他に、「Javaパフォーマンス診断ツール」を使用してコードのベンチマークを行ってください。また、 マイクロベンチマークの方法Javaコード を参照してください。

0
n247s

Java JITは空のループボディを最適化するため、ループは実際には次のようになります。

while((c = fa.read()) != -1);

そして

while((line = fa.readLine()) != null);

ベンチマーク here およびループの最適化 here を読むことをお勧めします。


時間がかかる理由について:

  • 理由1(これは、ループの本文にコードが含まれている場合にのみ適用されます):最初の例では、行ごとに1つの操作を実行しています第二に、あなたはキャラクターごとに1つやっています。これにより、より多くの行/文字が追加されます。

    while((c = fa.read()) != -1){
        //One operation per character.
    }
    
    while((line = fa.readLine()) != null){
        //One operation per line.
    }
    
  • 理由2:クラスBufferedReaderでは、メソッドreadLine()は舞台裏でread()を使用しません-独自のコードを使用します。メソッドreadLine()は、read()メソッドを使用して行を読み取るよりも、行を読み取るための文字ごとの操作が少ない-これが、readLine()がファイル全体の読み取りを高速化する理由です。

  • 理由3:各文字を読み込むには、各行を読み込むよりも多くの反復が必要です(各文字が新しい行にない場合)。 read()は、readLine()よりも多く呼び出されます。

0
Luke Melaia

ドキュメントによると:

すべてのread()メソッド呼び出しは、高価なシステム呼び出しを行います。

すべてのreadLine()メソッド呼び出しは依然として高価なシステム呼び出しを行いますが、一度により多くのバイトを使用するため、呼び出しは少なくなります。

同様の状況は、更新する各レコードに対してデータベースupdateコマンドを作成する場合と、すべてのレコードに対して1つの呼び出しを行うバッチ更新を実行する場合に発生します。

0
simon_