C #: File.ReadLines () срещу File.ReadAllLines () - и защо да се грижа?

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

Това предизвика някои други предишни дискусии по тази тема и по-специално за използването на възвръщаемост на печалбата в C # (за което вероятно ще говоря в бъдеща публикация в блога). И така, реших, че би било добро предизвикателство да демонстрирам как C # може да мащабира ефективно, когато става въпрос за обработка на големи парчета данни.

Предизвикателството

И така, проблемният проблем е:

  • Да предположим, че има голям CSV файл, да речем ~ 500 MB за начало
  • Програмата трябва да премине през всеки ред от файла, да го анализира и да направи някои изчисления на базата на карта / намаляване

И въпросът в този момент на дискусията е:

Кой е най-ефективният начин да напишете кода, който може да постигне тази цел? Макар и да спазва:
i) минимизирайте обема на използваната памет и
ii) свеждане до минимум на реда от кода на програмата (разбира се, в разумна степен)

В интерес на аргумента бихме могли да използваме StreamReader, но това ще доведе до написването на повече код, който е необходим и всъщност, C # вече има методите за удобство на File.ReadAllLines () и File.ReadLines (). Така че ние трябва да използваме тези!

Покажете ми кода

За пример, нека разгледаме програма, която:

  1. Взима текстов файл като вход, където всеки ред е цяло число
  2. Изчислява сумата от всички числа във файла

За целта на този пример ще пропуснем доста валидиращи съобщения :-)

В C # това може да се постигне със следния код:

var sumOfLines = File.ReadAllLines (filePath)
    .Избор (линия => int.Parse (линия))
    .Sum ()

Доста проста, нали?

Какво се случва, когато захранваме тази програма с голям файл?

Ако стартираме тази програма за обработка на 100MB файл, това е, което получаваме:

  • 2GB RAM памет изразходва памет, за да завърши този компютър
  • Много GC (всеки жълт елемент е стартиран от GC)
  • 18 секунди, за да завършите изпълнението
BTW, подаването на 500MB файл към този код доведе до срив на програмата с OutOfMemoryException Fun, нали?

Сега нека опитаме File.ReadLines () вместо това

Нека променим кода, за да използваме File.ReadLines () вместо File.ReadAllLines () и вижте как върви:

var sumOfLines = File.ReadLines (filePath)
    .Избор (линия => int.Parse (линия))
    .Sum ()

Когато го изпълняваме, сега получаваме:

  • Консумирана е 12MB RAM, вместо 2GB (!!)
  • Само 1 GC работи
  • 10 секунди за изпълнение, вместо 18

Защо се случва това?

TL; DR ключовата разлика е, че File.ReadAllLines () изгражда низ [], който съдържа всеки ред на файла, изискващ достатъчно памет за зареждане на целия файл; противоположно на File.ReadLines (), който захранва програмата всеки ред наведнъж, като се изисква само паметта да се зареди един ред.

С малко повече подробности:

File.ReadAllLines () чете целия файл наведнъж и връща низ [], където всеки елемент от масива съответства на ред от файла. Това означава, че програмата се нуждае от толкова памет, колкото размера на файла, за да зареди съдържанието от файла. Плюс необходимата памет, за да се разберат ВСИЧКИ низови елементи за int и след това да се изчисли Сумата ()

От другата страна, File.ReadLines () създава изброяване на файла, като го чете по ред (всъщност използвайки StreamReader.ReadLine ()). Това означава, че всеки ред се чете, преобразува и добавя към частичната сума в режим на линия линия.

заключение

Тази тема може да изглежда като детайлна информация за внедряването на ниско ниво, но всъщност е много важна, защото определя как програмата ще мащабира, когато се захранва с голям набор от данни.

За разработчиците на софтуер е важно да могат да предвидят подобен вид ситуации, защото човек никога не знае дали някой ще предостави голям принос, който не е бил предвиден на етапа на разработка.

Също така, LINQ е достатъчно гъвкав, за да се справя безпроблемно с тези два сценария и осигурява отлична ефективност, когато се използва с код, който осигурява "стрийминг" на стойности.

Това означава, че не всичко трябва да е Списък или T [], което означава, че целият набор от данни е зареден в паметта. Използвайки IEnumerable , ние правим нашия код общ за използване с методи, които осигуряват целия набор от данни в паметта или които предоставят стойности в режим „стрийминг“.