120 MB CSVファイルのString.Split()での.NET System.OutOfMemoryException
-
03-07-2019 - |
質問
C#を使用して、約120 MBのプレーンテキストCSVファイルを読み取ります。最初は行ごとに読み取ることで解析を行いましたが、最近、ファイルの内容全体を最初にメモリに読み込む方が数倍高速であると判断しました。 CSVには引用符内にコンマが埋め込まれているため、解析は既にかなり遅くなります。つまり、正規表現分割を使用する必要があります。これは、私が見つけた唯一確実に機能するものです:
string[] fields = Regex.Split(line,
@",(?!(?<=(?:^|,)\s*\x22(?:[^\x22]|\x22\x22|\\\x22)*,)
(?:[^\x22]|\x22\x22|\\\x22)*\x22\s*(?:,|$))");
// from http://regexlib.com/REDetails.aspx?regexp_id=621
すべての内容をメモリに読み込んだ後に解析を行うために、改行文字で文字列を分割して、各行を含む配列を取得します。ただし、120 MBファイルでこれを行うと、 System.OutOfMemoryException
が発生します。コンピューターに4 GBのRAMが搭載されているのに、なぜメモリがすぐに不足するのですか?複雑なCSVをすばやく解析するより良い方法はありますか?
解決
基本的に任意のサイズの割り当てに対してOutOfMemoryExceptionを取得できます。メモリの一部を割り当てるとき、実際に要求されたサイズの連続したメモリを要求しています。それを守れない場合は、OutOfMemoryExceptionが表示されます。
また、64ビットWindowsを実行している場合を除き、4 GB RAMは2 GBのカーネルスペースと2 GBのユーザースペースに分割されているため、.NETアプリケーションはデフォルトで2 GB以上にアクセスできません。
.NETで文字列操作を行う場合、.NET文字列は不変であるため、多くの一時的な文字列を作成するリスクがあります。したがって、メモリ使用量が大幅に増加することがあります。
他のヒント
ファイル全体を文字列に読み込む場合は、おそらく StringReader 。
StringReader reader = new StringReader(fileContents);
string line;
while ((line = reader.ReadLine()) != null) {
// Process line
}
これは、ファイルからのストリーミングとほぼ同じである必要がありますが、コンテンツが既にメモリ内にあるという違いがあります。
テスト後に編集
line.Lengthを使用して長さ変数をインクリメントする処理で構成された140MBファイルで上記を試しました。これには、コンピューターで約1.6秒かかりました。この後、私は次を試しました:
System.IO.StreamReader reader = new StreamReader("D:\\test.txt");
long length = 0;
string line;
while ((line = reader.ReadLine()) != null)
length += line.Length;
結果は約1秒でした。
もちろん、特にネットワークドライブから読み込んでいる場合や、ハードドライブが他の場所を探すのに十分な時間がかかる場合は、走行距離が異なる場合があります。ただし、FileStreamを使用してファイルを読み取り、バッファリングしていない場合も同様です。 StreamReaderは、読み取りを大幅に強化するバッファリングを提供します。
それほど多くの連続したメモリを持つ単一のオブジェクトを割り当てることができない場合もあります。ストリーミングはこれを行う通常の方法ですが、遅くなる可能性があることは正しいです(通常、それほど遅くなるとは思いませんが)。
妥協案として、 StreamReader.ReadBlock()
などの関数を使用して、ファイルの大部分(ただし、全体ではない)を一度に読み取り、各部分を処理してみてください。ターン。
他のポスターが言うように、OutOfMemoryは、要求されたサイズの連続したメモリチャンクを見つけることができないためです。
ただし、行ごとの解析は、一度にすべてを読み取ってから処理するよりも数倍高速であると言います。これは、例えば(擬似コードで)ブロック読み取りを行う単純なアプローチを追求している場合にのみ意味があります:
while(! file.eof() )
{
string line = file.ReadLine();
ProcessLine(line);
}
代わりにストリーミングを使用する必要があります。ストリームは、ファイルを読み込んでいる代替スレッドからのWrite()呼び出しで埋められるため、ProcessLine()の実行によってファイルの読み込みがブロックされることはありません。これは、ファイル全体を一度に読み取ってから処理を実行するパフォーマンスと同等である必要があります。
をお試しください。 CLRプロファイラーを使用して、実際のメモリ使用量を確認します。システムRAM以外のメモリ制限がある可能性があります。たとえば、これがIISアプリケーションである場合、メモリはアプリケーションプールによって制限されます。
このプロファイル情報を使用すると、最初に試行したCSVファイルのストリーミングなど、よりスケーラブルな手法を使用する必要がある場合があります。
ヒープではなく、スタックのメモリが不足しています。
入力をより管理しやすい「チャンク」で処理するように、アプリのリファクタリングを試すことができます。一度に120MBを処理するのではなく、データの量。
ここのほとんどの人に同意します。ストリーミングを使用する必要があります。
これまでに誰かが言ったかどうかはわかりませんが、拡張方法を検討する必要があります。
そして、確かに、.NET / CLRでの最適なCSV分割手法は次のとおりです。 これ
この手法により、入力CSVから+ 10GBのXML出力が生成されました。これには、豊富な入力フィルターなどが含まれており、これまで見たどの製品よりも高速です。
バッファにチャンクを読み込んで作業する必要があります。その後、別のチャンクなどを読み取ります。
これを効率的に行う多くのライブラリがあります。 CsvHelper という名前を保持しています。コンマや行末がフィールドの中央にある場合など、処理する必要のある多くのエッジケースがあります。