C #: File.ReadLines () vs File.ReadAllLines () - ja miks ma peaksin sellest hoolima?

Paar nädalat tagasi sattusime koos kahe meeskonnaga, kellega töötan, arutelule tõhusate viiside üle suurte tekstifailide töötlemiseks.

See kutsus esile mõned muud varasemad arutelud, mis mul selle teema kohta varem olid, eriti aga saagise tulu kasutamise kohta C # -s (millest ma räägin tõenäoliselt mõnes tulevases blogipostituses). Niisiis, ma arvasin, et hea väljakutse on näidata, kuidas C # saab tõhusalt skaleerida, kui tegemist on suurte andmemahtude töötlemisega.

Väljakutse

Arutatav probleem on järgmine:

  • Oletame, et seal on suur CSV-fail, stardiks umbes 500 MB
  • Programm peab läbima kõik faili read, parsima selle ja tegema mõned kaardistamise / vähendamise põhised arvutused

Ja arutelu praegusel hetkel on küsimus:

Kuidas on selle eesmärgi saavutamiseks kõige tõhusam kood kirjutada? Järgides samas:
i) minimeerige kasutatud mälu maht ja
ii) minimeerige programmi koodiridad (muidugi mõistlikul määral)

Argumendi huvides võiksime kasutada StreamReaderit, kuid see viiks vajaliku koodi kirjutamiseni ja tegelikult on C # -l juba mugavusmeetodid File.ReadAllLines () ja File.ReadLines (). Nii et me peaksime neid kasutama!

Näita mulle koodi

Vaatleme näite huvides programmi, mis:

  1. Sisendina tekstifaili, kus iga rida on täisarv
  2. Arvutab kõigi failis olevate numbrite summa

Selle näite huvides jätame vahele päris valideerimise sõnumid :-)

C # -s saab seda teha järgmise koodiga:

var sumOfLines = File.ReadAllLines (filePath)
    .Vali (rida => int.Parse (joon))
    .Sum ()

Päris lihtne, eks?

Mis juhtub, kui toidame seda programmi suure failiga?

Kui käivitame selle programmi 100MB faili töötlemiseks, saame selle:

  • Selle andmetöötluse lõpetamiseks kulus 2 GB RAM-i mälu
  • Palju GC (iga kollane element on GC käitus)
  • 18 sekundit täitmise lõpetamiseks
BTW, 500MB faili lisamine sellele koodile põhjustas programmi krahhi OutOfMemoryException Fun abil, eks?

Proovime nüüd selle asemel File.ReadLines ()

Muudame koodi, et kasutada File.ReadAllLines () asemel File.ReadLines () ja vaatame, kuidas see läheb:

var sumOfLines = File.ReadLines (filePath)
    .Vali (rida => int.Parse (joon))
    .Sum ()

Selle käitamisel saame nüüd:

  • 12 MB RAM-i tarbitud, mitte 2 GB (!!)
  • Ainult 1 GC jooks
  • 18 sekundi asemel 10 sekundit

Miks see juhtub?

TL; DR põhiline erinevus seisneb selles, et File.ReadAllLines () ehitab stringi [], mis sisaldab faili iga rida, nõudes kogu faili laadimiseks piisavalt mälu; vastupidiselt File.ReadLines () -le, mis toidab programmi igal real korraga, nõudes ühe rea laadimiseks ainult mälu.

Pisut detailsemalt:

File.ReadAllLines () loeb korraga kogu faili ja tagastab stringi [], kus massiivi iga element vastab faili reale. See tähendab, et programmi sisu laadimiseks vajab programm sama palju mälu kui faili maht. Lisaks vajalik mälu KÕIK stringielementide sõelumiseks int ja seejärel summa () arvutamiseks

Teisest küljest loob File.ReadLines () failile loendaja, lugedes seda rida-realt (kasutades tegelikult StreamReader.ReadLine ()). See tähendab, et iga rida loetakse, teisendatakse ja lisatakse line-be-line režiimis osalisele summale.

Järeldus

See teema võib tunduda madala taseme rakendamisdetail, kuid see on tegelikult väga oluline, kuna see määrab kindlaks, kuidas programm suureneb, kui seda toidetakse suure andmekogumiga.

Tarkvaraarendajatele on oluline, et nad oskaksid selliseid olukordi ette näha, sest kunagi ei või teada, kas keegi annab suure panuse, mida arendusetapis ette ei nähtud.

Samuti on LINQ piisavalt paindlik, et neid kahte stsenaariumi sujuvalt käsitseda ja pakub suurepärast tõhusust, kui seda kasutatakse koodiga, mis pakub väärtuste voogesitust.

See tähendab, et kõik ei pea olema loend ega T [], mis tähendab, et kogu andmekogum on mällu laaditud. Kasutades IEnumerable , muudame oma koodi üldiseks kasutamiseks meetoditega, mis pakuvad kogu andmekogu mällu või pakuvad voogesituse režiimis väärtusi.