string.split()“メモリ不足例外”タブ区切りファイルを読むとき
-
05-07-2019 - |
質問
タブ区切りファイルを読み取るために、C#コードでstring.split()を使用しています。 「OutOfMemory例外」に直面しています以下のコードサンプルで説明します。
ここで、サイズが16 MBのファイルで問題が発生する理由を知りたいのですが?
これは正しいアプローチかどうか?
using (StreamReader reader = new StreamReader(_path))
{
//...........Load the first line of the file................
string headerLine = reader.ReadLine();
MeterDataIPValueList objMeterDataList = new MeterDataIPValueList();
string[] seperator = new string[1]; //used to sepreate lines of file
seperator[0] = "\r\n";
//.............Load Records of file into string array and remove all empty lines of file.................
string[] line = reader.ReadToEnd().Split(seperator, StringSplitOptions.RemoveEmptyEntries);
int noOfLines = line.Count();
if (noOfLines == 0)
{
mFileValidationErrors.Append(ConstMsgStrings.headerOnly + Environment.NewLine);
}
//...............If file contains records also with header line..............
else
{
string[] headers = headerLine.Split('\t');
int noOfColumns = headers.Count();
//.........Create table structure.............
objValidateRecordsTable.Columns.Add("SerialNo");
objValidateRecordsTable.Columns.Add("SurveyDate");
objValidateRecordsTable.Columns.Add("Interval");
objValidateRecordsTable.Columns.Add("Status");
objValidateRecordsTable.Columns.Add("Consumption");
//........Fill objValidateRecordsTable table by string array contents ............
int recordNumber; // used for log
#region ..............Fill objValidateRecordsTable.....................
seperator[0] = "\t";
for (int lineNo = 0; lineNo < noOfLines; lineNo++)
{
recordNumber = lineNo + 1;
**string[] recordFields = line[lineNo].Split(seperator, StringSplitOptions.RemoveEmptyEntries);** // Showing me error when we split columns
if (recordFields.Count() == noOfColumns)
{
//Do processing
}
解決
Splitは実装が不十分であり、巨大な文字列に適用するとパフォーマンスに重大な問題が発生します。分割機能によるメモリ要件の詳細については、この記事を参照してください。 :
全文字長が25745930で、それぞれ16文字のコンマ区切りの1355049文字列を含む文字列で分割を行うとどうなりますか?
文字列オブジェクトへのポインターの配列:4(アドレスポインター)* 1355049 = 5420196(配列サイズ)+ 16(ブックキーピング用)= 5420212の連続した仮想アドレススペース。
1355049文字列の非連続仮想アドレススペース(それぞれ54バイト)。これらの130万個の文字列がすべてヒープ全体に散らばるわけではありませんが、LOHには割り当てられません。 GCは、Gen0ヒープの束にそれらを割り当てます。
Split.Functionは、サイズが25745930のSystem.Int32 []の内部配列を作成し、LOHを(102983736バイト)〜98MB消費します。これは非常に高価なLです。
他のヒント
最初にファイル全体を配列に読み取らないしないを試してください&quot; reader.ReadToEnd()&quot;ファイルを1行ずつ直接読み取ります。.
using (StreamReader sr = new StreamReader(this._path))
{
string line = "";
while(( line= sr.ReadLine()) != null)
{
string[] cells = line.Split(new string[] { "\t" }, StringSplitOptions.None);
if (cells.Length > 0)
{
}
}
}
可能な場合は1行ずつ読むことをお勧めしますが、新しい行で分割することは必須ではありません。
したがって、独自のメモリ効率の良いスプリットをいつでも作成できます。これで問題は解決しました。
private static IEnumerable<string> CustomSplit(string newtext, char splitChar)
{
var result = new List<string>();
var sb = new StringBuilder();
foreach (var c in newtext)
{
if (c == splitChar)
{
if (sb.Length > 0)
{
result.Add(sb.ToString());
sb.Clear();
}
continue;
}
sb.Append(c);
}
if (sb.Length > 0)
{
result.Add(sb.ToString());
}
return result;
}
私は自分のものを使用します。 10個のユニットテストでテストされています。
public static class StringExtensions
{
// the string.Split() method from .NET tend to run out of memory on 80 Mb strings.
// this has been reported several places online.
// This version is fast and memory efficient and return no empty lines.
public static List<string> LowMemSplit(this string s, string seperator)
{
List<string> list = new List<string>();
int lastPos = 0;
int pos = s.IndexOf(seperator);
while (pos > -1)
{
while(pos == lastPos)
{
lastPos += seperator.Length;
pos = s.IndexOf(seperator, lastPos);
if (pos == -1)
return list;
}
string tmp = s.Substring(lastPos, pos - lastPos);
if(tmp.Trim().Length > 0)
list.Add(tmp);
lastPos = pos + seperator.Length;
pos = s.IndexOf(seperator, lastPos);
}
if (lastPos < s.Length)
{
string tmp = s.Substring(lastPos, s.Length - lastPos);
if (tmp.Trim().Length > 0)
list.Add(tmp);
}
return list;
}
}
コンテンツ全体を分割する代わりに、ファイルを行ごとに読み取ってください。