107

私たちのアプリケーションのスクリプト エディターに読み込まれる大きなファイルを処理する方法を考え出すという素敵な仕事があります (クイック マクロ用の内部製品のVBAのようなものです)。ほとんどのファイルは約 300 ~ 400 KB で、読み込みは問題ありません。しかし、100 MB を超えると、(ご想像のとおり) 処理が困難になります。

何が起こるかというと、ファイルが読み取られて RichTextBox に押し込まれ、ナビゲートされます。この部分についてはあまり心配する必要はありません。

最初のコードを書いた開発者は、単純に StreamReader を使用して実行しています。

[Reader].ReadToEnd()

完了するまでにかなりの時間がかかる可能性があります。

私の仕事は、このコードを分割し、チャンクでバッファに読み込み、キャンセルするオプションを含むプログレスバーを表示することです。

いくつかの仮定:

  • ほとんどのファイルは 30 ~ 40 MB になります
  • ファイルの内容はテキスト (バイナリではない) で、一部は Unix 形式、一部は DOS 形式です。
  • コンテンツが取得されたら、どのターミネータが使用されているかを調べます。
  • リッチテキストボックスでレンダリングするのにかかる時間が読み込まれると、誰も心配しません。これは、テキストの初期ロードにすぎません。

質問は次のとおりです。

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

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

4

12 に答える 12

193

次のように、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 データフローを学ぶ機会として利用しました。これは、このパターンをすばやくコーディングするのに非常に適しています。

BufferedStream が速い理由

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

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

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

関連している

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

于 2012-03-10T01:22:32.293 に答える
28

この Web サイト でパフォーマンスとベンチマークの統計情報を読むと、テキスト ファイルを読み取る最速の方法(読み取り、書き込み、処理はすべて異なるため) が次のコード スニペットであることがわかります。

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

約 9 つの異なる方法すべてがベンチマークされましたが、他の読者が言及したように、バッファリングされたリーダーを実行しても、ほとんどの場合、その 1 つが先行しているようです。

于 2014-09-19T14:21:27.713 に答える
16

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

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

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

于 2010-01-29T13:03:51.473 に答える
8

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

また、ReadToEnd() を使用しないようにしてください。「なんで作ったの?」と思う機能の一つです。小さなものには問題なく機能するスクリプトキディのヘルパーですが、ご覧のとおり、大きなファイルには適していません...

StringBuilder を使用するように言っている人は、MSDN をもっと頻繁に読む必要があります。

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

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

StringBuilder オプションは、システムを単一ユーザーとして使用する場合には問題ないように見えますが、2 人以上のユーザーが同時に大きなファイルを読み取っている場合、問題が発生します。

于 2010-01-29T12:42:11.850 に答える
6

始めるにはこれで十分です。

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();
    }
}
于 2010-01-29T12:56:33.113 に答える
5

次のコード スニペットを見てください。あなたは言及しMost files will be 30-40 MBました。これは、Intel クアッド コアで 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);
        }
    }
}

原著

于 2010-01-29T12:52:50.243 に答える
3

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

編集:仕組みについてはMSDNのこちらを参照してください。次の .NET 4 がリリースされたときにどのように行われるかを示すブログエントリを次に示します。以前に提供したリンクは、これを実現するための pinvoke のラッパーです。ファイル全体をメモリにマップし、ファイルをスクロールするときにスライド ウィンドウのように表示できます。

于 2010-01-29T12:52:03.043 に答える
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など) を読み取るときにバッファーが正しく整列することが保証されます。

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

于 2010-07-09T18:35:03.297 に答える