LINQは便利だが、使い方を間違えると大量のメモリを消費してしまう場合がある。本稿では、LINQを使用する場合に消費するメモリ量を抑制する方法を解説する。
対象:.NET 3.5以降
LINQを活用したコードを書いていて、メモリ消費量の多さ(あるいは、それに起因する応答性の悪さ)に驚いたことはないだろうか? 本稿では、最もよくある失敗例と、その対策を説明する。
プログラムが使用しているメモリのサイズをコンソールに表示するメソッドを作っておく(次のコード)。
static void WriteTotalMemory()
{
Console.WriteLine("{0:0.0MBytes}", GC.GetTotalMemory(false) / 1024.0 / 1024.0);
}
Private Sub WriteTotalMemory()
Console.WriteLine("{0:0.0MBytes}", GC.GetTotalMemory(False) / 1024.0 / 1024.0)
End Sub
最もよくある失敗は、LINQで処理している途中でEnumerableクラス(System.Linq名前空間)のToList拡張メソッドやToArray拡張メソッドを使ってしまうことだ。
次のコードは100万個の整数の中から偶数だけの合計を求めるものだ。処理の途中でToList拡張メソッドを使っているため、その都度新しいコレクションが「実体化」され、無駄にメモリを消費してしまう。消費するメモリ量によっては、プログラムの応答にも悪影響を及ぼすだろう。
var nums = Enumerable.Range(1, 1000000).ToArray(); // 100万個の整数を持つ配列(約4MBytes)
GC.Collect();
WriteTotalMemory(); // 出力例→4.1MBytes
var sum = nums.Where(n => n % 2 == 0).ToList().Select(n => (long)n).ToList().Sum(); // よくない例
Console.WriteLine(sum);
WriteTotalMemory(); // 出力例→14.0MBytes
Dim nums = Enumerable.Range(1, 1000000).ToArray() ' 100万個の整数を持つ配列(約4MBytes)
GC.Collect()
WriteTotalMemory() ' 出力例→4.1MBytes
Dim sum = nums.Where(Function(n) n Mod 2 = 0).ToList().Select(Function(n) CLng(n)).ToList().Sum() ' よくない例
Console.WriteLine(sum)
WriteTotalMemory() ' 出力例→14.0MBytes
*1 ここでWhere拡張メソッドとSelect拡張メソッドの引数に記述してあるのは、ラムダ式である。ラムダ式について詳しくは、次のMSDNのドキュメントを参照していただきたい。
ToList拡張メソッドやToArray拡張メソッドなどを、可能な限り途中で使わないようにすればよい。そうすれば無駄にコレクションを「実体化」せずに済み、メモリを節約できるし、大量のメモリを確保するための応答速度低下も避けられる。
上のコードからToList拡張メソッドを取り除くと、次のようになる。
var nums = Enumerable.Range(1, 1000000).ToArray(); // 100万個の整数を持つ配列(約4MBytes)
GC.Collect();
WriteTotalMemory(); // 出力例→4.1MBytes
var sum = nums.Where(n => n % 2 == 0).Select(n => (long)n).Sum(); // よい例
Console.WriteLine(sum);
WriteTotalMemory(); // 出力例→4.1MBytes
Dim nums = Enumerable.Range(1, 1000000).ToArray() ' 100万個の整数を持つ配列(約4MBytes)
GC.Collect()
WriteTotalMemory() ' 出力例→4.1MBytes
Dim sum = nums.Where(Function(n) n Mod 2 = 0).Select(Function(n) CLng(n)).Sum() ' よい例
Console.WriteLine(sum)
WriteTotalMemory() ' 出力例→4.1MBytes
これはLINQの大きな特徴であって、IEnumerable<T>インターフェース(System.Collections.Generic名前空間)のまま扱う限りは、無駄にメモリを消費するコレクションを生成しないようになっているのである*2。
*2 この機能は「クエリの遅延評価(または遅延実行)」と呼ばれる。詳しくはMSDNに掲載されている「LINQ: .NET 統合言語クエリ」(Don Box、Anders Hejlsberg著)の「LINQ プロジェクトをサポートする言語機能」の項中、「クエリの遅延評価」の項を参照していただきたい。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラスライブラリ 処理対象:LINQ
使用ライブラリ:Enumerableクラス(System.Linq名前空間)
関連TIPS:LINQ:数値コレクション内の特定の数値だけを集計するには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.