C #: File.ReadLines () vs File.ReadAllLines () - a proč by mě to mělo zajímat?

Před několika týdny jsem se já a dva týmy, se kterými pracuji, narazili na diskusi o účinných způsobech zpracování velkých textových souborů.

To vyvolalo některé další předchozí diskuse, které jsem měl v minulosti o tomto tématu, a zejména o využití výnosové návratnosti v C # (o kterém pravděpodobně budu mluvit v budoucím blogovém příspěvku). Takže jsem si myslel, že by bylo dobrou výzvou ukázat, jak může C # efektivně škálovat, pokud jde o zpracování velkých kousků dat.

Výzva

Diskutovaným problémem je tedy:

  • Předpokládejme, že existuje velký soubor CSV, řekněme ~ 500 MB pro začátek
  • Program musí projít každý řádek souboru, analyzovat jej a provést některé výpočty založené na mapách / redukcích

A otázkou v tomto bodě diskuse je:

Jaký je nejúčinnější způsob, jak napsat kód, který je schopen tohoto cíle dosáhnout? Při dodržení:
i) minimalizovat množství použité paměti a
ii) minimalizovat řádky kódu programu (samozřejmě v rozumném rozsahu)

Kvůli argumentu bychom mohli použít StreamReader, ale to by vedlo k napsání více kódu, který je potřeba, a ve skutečnosti, C # již obsahuje metody pohodlí File.ReadAllLines () a File.ReadLines (). Měli bychom je používat!

Ukaž kód

Pro příklad si představme program, který:

  1. Vezme textový soubor jako vstup, kde každý řádek je celé číslo
  2. Vypočítá součet všech čísel v souboru

Kvůli tomuto příkladu vynecháme pěkně ověřovací zprávy :-)

V C # toho lze dosáhnout pomocí následujícího kódu:

var sumOfLines = File.ReadAllLines (filePath)
    .Výběr (line => int.Parse (line))
    .Součet()

Docela jednoduché, že?

Co se stane, když krmíme tento program velkým souborem?

Pokud spustíme tento program pro zpracování souboru 100 MB, získáme toto:

  • K dokončení tohoto výpočtu spotřebovala 2 GB paměti RAM
  • Spousta GC (každá žlutá položka je GC run)
  • 18 sekund k dokončení provádění
BTW, krmení souboru 500 MB do tohoto kódu způsobilo zhroucení programu s funkcí OutOfMemoryException Fun, že?

Nyní zkusme raději File.ReadLines ()

Změníme kód tak, aby používal File.ReadLines () místo File.ReadAllLines () a uvidíme, jak to jde:

var sumOfLines = File.ReadLines (filePath)
    .Výběr (line => int.Parse (line))
    .Součet()

Při jeho spuštění nyní získáme:

  • Spotřeba 12 MB RAM, namísto 2 GB (!!)
  • Pouze 1 GC běh
  • 10 sekund, místo 18

Proč se toto děje?

TL; DR je klíčový rozdíl v tom, že File.ReadAllLines () vytváří řetězec [], který obsahuje každý řádek souboru, což vyžaduje dostatek paměti pro načtení celého souboru; na rozdíl od File.ReadLines (), který napájí program každý řádek najednou a vyžaduje pouze načtení jednoho řádku z paměti.

Podrobněji:

File.ReadAllLines () přečte celý soubor najednou a vrátí řetězec [], kde každá položka pole odpovídá řádku souboru. To znamená, že program potřebuje k načtení obsahu ze souboru tolik paměti jako velikost souboru. Plus nezbytnou paměť k analýze VŠECH řetězcových prvků na int a poté vypočítat součet ()

Na druhé straně File.ReadLines () vytvoří v souboru enumerátor, který jej čte řádek po řádku (ve skutečnosti používá StreamReader.ReadLine ()). To znamená, že každý řádek je čten, převeden a přidán k částečnému součtu v režimu řádek-řádek.

Závěr

Toto téma se může jevit jako detail implementace na nízké úrovni, ale ve skutečnosti je velmi důležitý, protože určuje, jak se bude program škálovat, když je napájen velkým množstvím dat.

Je důležité, aby vývojáři softwaru byli schopni předvídat tento druh situací, protože nikdy neví, jestli někdo poskytne velký vstup, který nebyl ve fázi vývoje předvídán.

LINQ je také dostatečně flexibilní, aby zvládl tyto dva scénáře hladce a poskytoval vynikající účinnost při použití s ​​kódem, který poskytuje „streamování“ hodnot.

To znamená, že ne všechno musí být seznam nebo T [], což znamená, že celá sada dat je načtena do paměti. Pomocí IEnumerable děláme náš kód obecným pro použití s ​​metodami, které poskytují celou sadu dat v paměti nebo které poskytují hodnoty v režimu „streamování“.