.NET System.OutOfMemoryException для String.Split() CSV-файла размером 120 МБ

StackOverflow https://stackoverflow.com/questions/809156

  •  03-07-2019
  •  | 
  •  

Вопрос

Я использую C# для чтения текстового CSV-файла размером ~ 120 МБ.Первоначально я выполнял анализ, читая его построчно, но недавно обнаружил, что сначала чтение всего содержимого файла в память происходит в несколько раз быстрее.Анализ уже довольно медленный, потому что 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 МБ, я получаю System.OutOfMemoryException.Почему так быстро заканчивается память, если на моем компьютере 4 ГБ оперативной памяти?Есть ли лучший способ быстро проанализировать сложный CSV-файл?

Это было полезно?

Решение

Вы можете получить исключение OutOfMemoryException практически для любого размера выделения.Когда вы выделяете часть памяти, вы на самом деле запрашиваете непрерывную часть памяти запрошенного размера.Если это невозможно выполнить, вы увидите исключение OutOfMemoryException.

Вы также должны знать, что, если вы не используете 64-разрядную версию Windows, ваши 4 ГБ ОЗУ разделены на 2 ГБ пространства ядра и 2 ГБ пользовательского пространства, поэтому ваше .NET-приложение не может получить доступ к более чем 2 ГБ по умолчанию.

При выполнении строковых операций в .NET вы рискуете создать множество временных строк из-за того, что строки .NET неизменяемы.Поэтому вы можете заметить, что использование памяти значительно увеличится.

Другие советы

Не запускайте собственный парсер, если в этом нет необходимости.Мне повезло с этим:

Быстрый читатель CSV

По крайней мере, вы можете заглянуть под капот и посмотреть, как это делает кто-то другой.

Если у вас есть весь файл, прочитанный в строку, вам, вероятно, следует использовать StringReader.

StringReader reader = new StringReader(fileContents);
string line;
while ((line = reader.ReadLine()) != null) {
    // Process line
}

Это должно быть примерно так же, как потоковая передача из файла, с той разницей, что содержимое уже находится в памяти.

Редактировать после тестирования

Попробовал вышеописанное с файлом размером 140 МБ, где обработка заключалась в увеличении переменной длины с помощью line.Length.На моем компьютере это заняло около 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 чтобы определить фактическое использование памяти.Возможно, существуют ограничения на память, отличную от оперативной памяти вашей системы.Например, если это приложение IIS, ваша память ограничена пулами приложений.

С помощью этой информации профиля вы можете обнаружить, что вам необходимо использовать более масштабируемый метод, например потоковую передачу файла CSV, который вы изначально пытались использовать.

У вас заканчивается память в стеке, а не в куче.

Вы можете попробовать рефакторинг вашего приложения таким образом, чтобы вы обрабатывали входные данные более управляемыми «кусками» данных, а не обрабатывали 120 МБ за раз.

Я согласен почти со всеми здесь: вам нужно использовать потоковую передачу.

Я не знаю, сказал ли кто-нибудь до сих пор, но вам следует посмотреть на метод расширения.

И я точно знаю, что лучший метод разделения CSV в .NET/CLR — этоВот этот

Этот метод сгенерировал мне +10 ГБ выходных XML-файлов из входного CSV, включая обширные входные фильтры и все такое, быстрее, чем что-либо еще, что я видел.

Вам следует прочитать фрагмент в буфер и поработать над этим.Затем прочитайте еще один фрагмент и так далее.

Существует множество библиотек, которые эффективно сделают это за вас.Я поддерживаю один под названием CsvHelper.Существует множество крайних случаев, которые вам необходимо обработать, например, когда запятая или окончание строки находится в середине поля.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top