C#:File.ReadLines()vs File.ReadAllLines()—なぜ気にする必要があるのですか?

数週間前、私と一緒に仕事をしている2つのチームは、大きなテキストファイルを処理する効率的な方法について話し合いました。

これにより、このトピックについて、特にC#でのyield returnの使用(これについては今後のブログ投稿で説明します)について、私が過去に行った他のいくつかの議論のきっかけとなりました。そのため、大量のデータを処理する場合に、C#がどのように効果的にスケーリングできるかを示すことは良い挑戦になると思いました。

チャレンジ

したがって、議論中の問題は次のとおりです。

  • 大きなCSVファイルがあると仮定します。例えば、最初に500MBまで
  • プログラムはファイルのすべての行を調べて解析し、map / reduceベースの計算を行う必要があります

そして、議論のこの時点での質問は次のとおりです。

この目標を達成できるコードを記述する最も効率的な方法は何ですか?以下も順守しながら:
i)使用されるメモリの量を最小化し、
ii)プログラムのコード行を最小限に抑えます(もちろん合理的な範囲で)

引数のために、StreamReaderを使用することもできますが、それにより必要なコードをより多く書くことができ、実際、C#にはFile.ReadAllLines()およびFile.ReadLines()の便利なメソッドが既にあります。だからそれらを使うべきです!

コードを見せて

例のために、次のようなプログラムを考えてみましょう。

  1. 各行が整数であるテキストファイルを入力として受け取ります
  2. ファイル内のすべての数値の合計を計算します

この例のために、かなりの検証メッセージをスキップします:-)

C#では、これは次のコードで実現できます。

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

かなり簡単ですね。

このプログラムに大きなファイルをフィードするとどうなりますか?

このプログラムを実行して100MBのファイルを処理すると、次のようになります。

  • このコンピューティングを完了するために2GBのRAMがメモリを消費しました
  • 多くのGC(黄色の各項目はGC実行です)
  • 実行が完了するまで18秒
ところで、このコードに500MBのファイルを渡すと、プログラムはOutOfMemoryException Funでクラッシュしますよね?

代わりにFile.ReadLines()を試してみましょう

File.ReadAllLines()の代わりにFile.ReadLines()を使用するようにコードを変更して、その動作を確認しましょう。

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

実行すると、次のようになります。

  • 2GBではなく、12MBのRAMが消費されます(!!)
  • GC実行は1回のみ
  • 完了するのに18秒ではなく10秒

なぜこうなった?

TL; DRの主な違いは、File.ReadAllLines()がファイルのすべての行を含むstring []を構築し、ファイル全体をロードするのに十分なメモリを必要とすることです。 File.ReadLines()とは反対に、一度に1行ずつプログラムをフィードし、1行の読み込みにメモリのみを必要とします。

もう少し詳しく:

File.ReadAllLines()は、ファイル全体を一度に読み取り、配列の各項目がファイルの行に対応するstring []を返します。これは、ファイルからコンテンツをロードするために、プログラムがファイルのサイズと同じだけのメモリを必要とすることを意味します。さらに、すべての文字列要素をintに解析してからSum()を計算するために必要なメモリ

一方、File.ReadLines()は、ファイルに列挙子を作成し、1行ずつ読み取ります(実際にはStreamReader.ReadLine()を使用)。これは、各行が読み込まれ、変換され、line-be-lineモードで部分合計に追加されることを意味します。

結論

このトピックは低レベルの実装の詳細のように思えるかもしれませんが、ビッグデータセットが与えられたときにプログラムがどのようにスケーリングされるかを決定するため、実際には非常に重要です。

ソフトウェア開発者がこの種の状況を予測できることは重要です。なぜなら、開発段階で予測されなかった大きな入力を誰かが提供するかどうかはわからないからです。

また、LINQはこれら2つのシナリオをシームレスに処理するのに十分な柔軟性を備えており、値の「ストリーミング」を提供するコードと併用すると優れた効率を提供します。

つまり、すべてがList またはT []である必要はなく、データセット全体がメモリにロードされることを意味します。 IEnumerable を使用することで、メモリ内のデータセット全体を提供するメソッドまたは「ストリーミング」モードで値を提供するメソッドで使用するコードを汎用化します。