C#でのストリームを含む大きなテキストファイルの読み取り


96

私は、アプリケーションのスクリプトエディターに読み込まれる大きなファイルを処理する方法を考え出すというすばらしいタスクを持っています(これは、クイックマクロ用の内部製品のVBAのようなものです)。ほとんどのファイルは約300〜400 KBで、読み込みに優れています。しかし、それらが100 MBを超えると、プロセスは困難になります(予想どおり)。

何が起こるかというと、ファイルが読み込まれ、RichTextBoxに押し込まれてナビゲートされます。この部分についてはあまり気にしないでください。

最初のコードを書いた開発者は、単にStreamReaderを使用して

[Reader].ReadToEnd()

完了するまでにかなり時間がかかる場合があります。

私の仕事は、このコードのコードを分割し、それをチャンクでバッファーに読み取り、それをキャンセルするオプションを備えた進行状況バーを表示することです。

いくつかの仮定:

  • ほとんどのファイルは30〜40 MBになります
  • ファイルの内容はテキスト(バイナリではない)であり、一部はUnix形式、一部はDOSです。
  • コンテンツが取得されると、使用されているターミネーターがわかります。
  • いったん読み込まれると、richtextboxでのレンダリングにかかる​​時間を気にする必要はありません。これは、テキストの初期ロードにすぎません。

さて、質問のために:

  • StreamReaderを使用してから、Lengthプロパティ(ProgressMax)を確認し、設定されたバッファーサイズに対してReadを発行して、バックグラウンドワーカー内のwhileループWHILSTを反復処理して、メインUIスレッドをブロックしないようにすることはできますか?次に、stringbuilderが完了したらメインスレッドに戻します。
  • 内容はStringBuilderに送られます。長さが利用できる場合、ストリームのサイズでStringBuilderを初期化できますか?

これらは(あなたの専門家の意見では)良いアイデアですか?Streamsからのコンテンツの読み取りでは、最後の数バイトなどが常に失われるため、過去にいくつかの問題がありましたが、その場合は別の質問をします。


29
30〜40MBのスクリプトファイル 聖なるサバ!私はそれをコードレビューしなければならないのが嫌いです...
dthorpe 2010

私はこの質問がかなり古いことを知っていますが、先日それを見つけ、MemoryMappedFileの推奨事項をテストしました。これは最速の方法です。比較では、readlineメソッドを使用して7,616,939行の345MBファイルを読み取ると、私のマシンでは12時間以上かかりますが、同じロードを実行し、MemoryMappedFileを介して読み取るには3秒かかりました。
csonon

ほんの数行のコードです。25 GB以上の大きなファイルの読み取りにも使用しているこのライブラリを参照してください。github.com/Agenty/FileReader
Vikash Rathee 2017年

回答:


175

次のように、BufferedStreamを使用して読み取り速度を向上させることができます。

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

2013年3月更新

私は最近、1 GBのテキストファイル(ここに含まれるファイルよりもはるかに大きい)の読み取りと処理(テキストの検索)のためのコードを記述し、プロデューサー/コンシューマーパターンを使用することで、大幅なパフォーマンスの向上を達成しました。プロデューサータスクはを使用してテキスト行を読み取りBufferedStream、検索を実行する別のコンシューマータスクにそれらを渡しました。

これを、このパターンをすばやくコーディングするのに非常に適したTPL Dataflowを学ぶ機会として使用しました。

BufferedStreamの方が速い理由

バッファは、データをキャッシュするために使用されるメモリ内のバイトのブロックであり、それによってオペレーティングシステムへの呼び出し回数を減らします。バッファにより、読み取りおよび書き込みパフォーマンスが向上します。バッファは、読み取りまたは書き込みのどちらにも使用できますが、同時に両方に使用することはできません。BufferedStreamのReadメソッドとWriteメソッドは、自動的にバッファを維持します。

2014年12月更新:マイレージが異なる場合があります

コメントに基づいて、FileStreamは内部でBufferedStreamを使用する必要があります。この回答が最初に提供された時点で、BufferedStreamを追加することでパフォーマンスの大幅な向上を測定しました。当時、私は32ビットプラットフォームの.NET 3.xをターゲットとしていました。今日、64ビットプラットフォームの.NET 4.5を対象としていますが、改善は見られません。

関連した

ASP.Net MVCアクションから生成された大きなCSVファイルを応答ストリームにストリーミングするのが非常に遅いケースに遭遇しました。この例では、BufferedStreamを追加すると、パフォーマンスが100倍向上しました。詳細については、バッファリングされていない出力が非常に遅い


12
おい、BufferedStreamはすべての違いを生む。+1 :)
Marcus

2
IOサブシステムからデータを要求するにはコストがかかります。ディスクが回転している場合、次のデータチャンクを読み取るためにプラッターが回転するのを待つか、ディスクヘッドが移動するのを待つ必要があります。SSDには速度を落とすための機械的な部品はありませんが、SSDにアクセスするには、IOオペレーションあたりのコストがかかります。バッファリングされたストリームは、StreamReaderが要求するもの以上のものを読み取るため、OSへの呼び出しの数が減り、最終的には個別のIO要求の数が減ります。
エリックJ.

4
本当に?これは私のテストシナリオに違いはありません。Brad Abrams氏によると、FileStreamよりもBufferedStreamを使用するメリットはありません。
Nick Cox

2
@NickCox:結果は、基になるIOサブシステムによって異なる場合があります。回転ディスクと、キャッシュにデータがない(およびWindowsによってキャッシュされていないデータもある)ディスクコントローラーでは、速度が大幅に向上します。ブラッドのコラムは2004年に書かれました。私は実際に劇的な改善を最近測定しました。
エリックJ.

3
これは役に立たない:stackoverflow.com/questions/492283/…FileStreamはすでに内部的にバッファを使用しています。
アーウィンメイヤー

21

あなたが読めば、このウェブサイト上のパフォーマンスとベンチマークの統計情報を、あなたは、最速の方法をしていることがわかります読んで(理由はリーディング、ライティング、および処理すべて異なっている)、テキストファイルは次のコードの抜粋です。

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

約9種類のメソッドすべてがベンチマークされましたが、他のリーダーが述べたようにバッファーリーダー実行する場合でも、ほとんどの場合、その方が先に出てきます。


2
これは、19GBのpostgresファイルを切り離して複数のファイルのSQL構文に変換するのに適しています。私のパラメーターを正しく実行したことがないpostgresの人に感謝します。/ため息
デイモンドレイク

ここでのパフォーマンスの違いは、150 MBを超えるような非常に大きなファイルに見返りがあるようです(またStringBuilder、メモリへのロードにはを使用する必要があり、charを追加するたびに新しい文字列を作成しないため、ロードが速くなります)
Joshua G

15

大きなファイルの読み込み中に進行状況バーを表示するように求められたと言います。それは、ユーザーがファイルのロードの正確な割合を本当に知りたいからなのか、それとも何かが起こっているという視覚的なフィードバックを求めているからなのか?

後者が真の場合、ソリューションははるかに簡単になります。reader.ReadToEnd()バックグラウンドスレッドで実行し、適切なプログレスバーの代わりにマーキータイプのプログレスバーを表示します。

私の経験ではこれがよくあるので、この点を上げます。データ処理プログラムを作成している場合、ユーザーは完全な%の数値に間違いなく関心を示しますが、UIの更新は単純ですが遅いため、コンピューターがクラッシュしていないことを知りたいだけです。:-)


2
しかし、ユーザーはReadToEnd呼び出しをキャンセルできますか?
Tim Scarborough

@ティム、よく見つかる。その場合、StreamReaderループに戻ります。ただし、進行状況インジケーターを計算するために先読みする必要がないため、より簡単です。
Christian Hayter

8

バイナリファイルの場合、私が見つけたファイルを読み取る最も速い方法はこれです。

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

私のテストでは何百倍も高速です。


2
これについての確固たる証拠はありますか?なぜOPはこれを他の答えよりも使用する必要があるのですか もう少し深く掘り下げて、もう少し詳しく説明してください
Dylan Corriveau 2014

7

バックグラウンドワーカーを使用して、限られた数の行のみを読み取ります。詳細は、ユーザーがスクロールしたときにのみ表示されます。

そして、ReadToEnd()を使用しないようにしてください。それはあなたが「なぜ彼らはそれを作ったのですか?」と考える関数の1つです。それはですスクリプトキディ小さな事で罰金行くヘルパーが、あなたが見るように、それは大きなファイルのために吸います...

StringBuilderを使用するように指示している人は、MSDNをより頻繁に読む必要があります。

パフォーマンスに関する考慮事項
ConcatメソッドとAppendFormatメソッドはどちらも、新しいデータを既存のStringオブジェクトまたはStringBuilderオブジェクトに連結します。Stringオブジェクトの連結操作では、既存の文字列と新しいデータから常に新しいオブジェクトが作成されます。StringBuilderオブジェクトは、新しいデータの連結に対応するためにバッファを維持します。空きがある場合は、バッファの最後に新しいデータが追加されます。それ以外の場合は、新しい大きなバッファーが割り当てられ、元のバッファーのデータが新しいバッファーにコピーされ、新しいデータが新しいバッファーに追加されます。StringまたはStringBuilderオブジェクトの連結操作のパフォーマンスは、メモリ割り当てが発生する頻度に依存します。
String連結操作は常にメモリを割り当てますが、StringBuilder連結操作は、StringBuilderオブジェクトバッファが小さすぎて新しいデータを収容できない場合にのみメモリを割り当てます。したがって、固定数のStringオブジェクトが連結される場合、連結操作にはStringクラスが適しています。その場合、個々の連結操作は、コンパイラーによって1つの操作に結合されることさえあります。StringBuilderオブジェクトは、任意の数の文字列が連結される場合の連結操作に適しています。たとえば、ループがランダムな数のユーザー入力の文字列を連結する場合などです。

つまり、メモリの巨大な割り当て、つまりスワップファイルシステムの大規模な使用となり、ハードディスクドライブのセクションをRAMメモリのように動作するようにシミュレートしますが、ハードディスクドライブは非常に低速です。

StringBuilderオプションは、システムをモノユーザーとして使用するユーザーには問題ありませんが、2人以上のユーザーが同時に大きなファイルを読み取る場合、問題が発生します。


遠く離れた皆さん、とても速いです!残念ながら、マクロの動作方法により、ストリーム全体をロードする必要があります。先ほど述べたように、リッチテキスト部分については心配しないでください。改善したい初期ロード。
ニコールリー

あなたが部品に働くことができるように、第2のXラインを読んで、マクロを適用するマクロを適用し、そして、最初のXラインを読むように...あなたはこのマクロDOは、我々はより高い精度であなたを助けることができるかを説明場合
トゥーフォ

5

これはあなたが始めるのに十分なはずです。

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}

4
「var buffer = new char [1024]」をループの外に移動します。毎回新しいバッファーを作成する必要はありません。「while(count> 0)」の前に置くだけです。
Tommy Carlier、2010年

4

次のコードスニペットをご覧ください。あなたは言及しましたMost files will be 30-40 MB。これはIntel Quad Coreで1.4秒で180 MBを読み取ると主張しています:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

原著


3
これらの種類のテストは、信頼できないことで有名です。テストを繰り返すと、ファイルシステムキャッシュからデータが読み取られます。これは、ディスクからデータを読み取る実際のテストよりも少なくとも1桁高速です。180 MBのファイルの場合、3秒もかかりません。マシンを再起動し、実数のテストを1回実行します。
ハンスパッサント2010年

7
行stringBuilder.Appendは潜在的に危険です。それをstringBuilder.Append(fileContents、0、charsRead);に置き換える必要があります。ストリームが早く終了した場合でも、完全な1024文字を追加しないようにします。
ヨハネスルドルフ

@JohannesRudolph、あなたのコメントは私にバグを解決しました。どのようにして1024番を思いついたのですか?
HeyJude

3

ここでメモリマップファイルの処理を使用した方がよいかもしれません。メモリマップファイルのサポートは.NET 4で行われる予定です(他の誰かがそれについて話していると聞きました)。したがって、pを使用するこのラッパー/ invokesは同じ仕事をします。

編集:それがどのように機能するかについては、MSDNのここを参照してください。これは、リリースとして公開される次の.NET 4でそれがどのように行われるかを示すブログエントリです。先ほど紹介したリンクは、これを実現するためのピンボークのラッパーです。ファイル全体をメモリにマップし、ファイルをスクロールすると、スライドウィンドウのように表示できます。


2

すべての素晴らしい答え!しかし、答えを探している人にとっては、これらはやや不完全なようです。

標準の文字列は、構成に応じてサイズX、2Gb〜4Gbのみが可能であるため、これらの回答はOPの質問を実際には満たしていません。1つの方法は、文字列のリストを操作することです。

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

一部のユーザーは、処理時にトークン化して行を分割したい場合があります。文字列リストには、非常に大量のテキストを含めることができます。


1

イテレータはこのタイプの作業に最適です。

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

以下を使用して呼び出すことができます。

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

ファイルがロードされると、イテレーターは0から100までの進行状況番号を返します。これを使用して、進行状況バーを更新できます。ループが終了すると、StringBuilderにはテキストファイルの内容が含まれます。

また、テキストが必要なため、BinaryReaderを使用して文字を読み取ることができるため、マルチバイト文字(UTF-8UTF-16など)を読み取るときにバッファーが正しく整列することが保証されます。

これはすべて、バックグラウンドタスク、スレッド、または複雑なカスタムステートマシンを使用せずに行われます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.