C #: File.ReadLines () супраць File.ReadAllLines () - і чаму я павінен клапаціцца?

Пару тыдняў таму я і дзве каманды, з якімі я працую, натыкнуліся на абмеркаванне эфектыўных спосабаў апрацоўкі вялікіх тэкставых файлаў.

Гэта выклікала некаторыя іншыя папярэднія дыскусіі ў мінулым пра гэтую тэму і, у прыватнасці, аб выкарыстанні прыбытковасці ў C # (пра якую я, магчыма, буду казаць у будучай публікацыі блога). Такім чынам, я думаў, што гэта будзе добрай задачай прадэманстраваць, як C # можа эфектыўна маштабаваць, калі справа даходзіць да апрацоўкі вялікіх кавалкаў дадзеных.

Задача

Такім чынам, праблема, якая абмяркоўваецца:

  • Выкажам здагадку, ёсць вялікі CSV файл, скажам, ~ 500 Мб для пачатку
  • Праграма павінна прайсці кожную радок файла, разабраць яго і зрабіць некаторыя карты / паменшыць разлікі на аснове

І пытанне ў гэты момант у дыскусіі:

Які самы эфектыўны спосаб напісаць код, здольны дасягнуць гэтай мэты? Пры гэтым выконваюцца:
i) мінімізаваць колькасць выкарыстанай памяці і
ii) мінімізаваць радкі кода праграмы (зразумела, вядома)

Дзеля аргументу мы маглі б выкарыстоўваць StreamReader, але гэта прывядзе да напісання дадатковага кода, які патрэбны, і на самай справе, C # ужо мае зручныя метады File.ReadAllLines () і File.ReadLines (). Такім чынам, мы павінны выкарыстоўваць гэтыя!

Пакажыце мне код

Дзеля прыкладу разгледзім праграму, якая:

  1. Прымае тэкставы файл у якасці ўваходу, дзе кожны радок з'яўляецца цэлым
  2. Вылічвае суму ўсіх лікаў у файле

Дзеля гэтага прыкладу мы будзем прапускаць прыгожыя паведамленні праверкі :-)

У C # гэта можа быць дасягнута наступным кодам:

var sumOfLines = File.ReadAllLines (filePath)
    .Select (line => int.Parse (line))
    .Sum ()

Даволі проста, праўда?

Што адбываецца, калі мы падаем гэтую праграму з вялікім файлам?

Калі мы запусцім гэтую праграму для апрацоўкі файла памерам 100 МБ, гэта тое, што мы атрымліваем:

  • Для завяршэння гэтых вылічэнняў была выкарыстана 2 Гб аператыўнай памяці
  • Шмат GC (кожны жоўты элемент - гэта выкананне GC)
  • 18 секунд, каб скончыць расстрэл
Дарэчы, пры падачы гэтага кода файла 500MB прычынілася збой праграмы з праграмай OutOfMemoryException Fun, праўда?

Зараз паспрабуем File.ReadLines ()

Давайце зменім код, каб выкарыстоўваць File.ReadLines (), а не File.ReadAllLines (), і паглядзім, як гэта адбываецца:

var sumOfLines = File.ReadLines (filePath)
    .Select (line => int.Parse (line))
    .Sum ()

Запускаючы яго, мы атрымліваем:

  • Спажываецца 12 Мб аператыўнай памяці, а не 2 ГБ (!!)
  • Толькі 1 ГК працуе
  • 10 секунд, каб скончыць, а не 18

Чаму гэта адбываецца?

Ключавая розніца TL; DR заключаецца ў тым, што File.ReadAllLines () будуе радок [], якая змяшчае кожную радок файла, для запаўнення ўсяго файла патрабуецца дастаткова памяці; у адрозненне ад File.ReadLines (), які падае праграму кожнай радкі па чарзе, патрабуючы толькі адзін раз, каб загрузіць адзін радок.

Крыху больш падрабязна:

File.ReadAllLines () чытае ўвесь файл адразу і вяртае радок [], дзе кожнаму элементу масіва адпавядае радок файла. Гэта азначае, што праграме трэба столькі ж памяці, колькі памер файла, каб загрузіць змесціва з файла. Плюс неабходная памяць, каб разабраць усе элементы радкі да int, а потым вылічыць суму ()

З іншага боку, File.ReadLines () стварае нумары для файла, чытаючы яго па радку (на самай справе, выкарыстоўваючы StreamReader.ReadLine ()). Гэта азначае, што кожны рэжым счытваецца, пераўтвараецца і дадаецца да частковай сумы ў рэжыме радка.

Выснова

Гэтая тэма можа выглядаць як дэталь рэалізацыі на нізкім узроўні, але яна на самай справе вельмі важная, бо яна вызначае, як праграма будзе маштабаваць пры падачы з вялікім наборам дадзеных.

Распрацоўшчыкам праграмнага забеспячэння важна мець магчымасць прадказваць падобныя сітуацыі, бо ніхто ніколі не ведае, ці будзе хтосьці прадаставіць вялікі ўклад, які не быў прадугледжаны на этапе распрацоўкі.

Акрамя таго, LINQ з'яўляецца дастаткова гнуткім, каб лёгка абыходзіцца з гэтымі двума сцэнарыямі і забяспечваў выдатную эфектыўнасць пры выкарыстанні з кодам, які забяспечвае "струменевае" значэнне.

Гэта азначае, што не ўсё павінна быць спіс або T [], што азначае, што ўвесь набор дадзеных загружаны ў памяць. Выкарыстоўваючы IEnumerable , мы робім наш код агульным для выкарыстання з метадамі, якія забяспечваюць увесь набор дадзеных у памяці або якія забяспечваюць значэнні ў рэжыме "струменевага".