連載
» 2014年09月24日 14時18分 UPDATE

.NET TIPS:LINQの処理中に使うメモリを節約するには?[C#、VB]

LINQは便利だが、使い方を間違えると大量のメモリを消費してしまう場合がある。本稿では、LINQを使用する場合に消費するメモリ量を抑制する方法を解説する。

[山本康彦(http://www.bluewatersoft.jp/),BluewaterSoft]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.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

使用しているメモリサイズを表示するメソッドの例(上:C#、下:VB)
GCクラス(System名前空間)のGetTotalMemoryメソッドは、現在割り当てられていると考えられるバイト数を取得する。
このメソッドを呼び出すと、その時点でプログラムが使用しているメモリサイズの概数がMBytes単位でコンソールに表示される。

よくある失敗例

 最もよくある失敗は、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

途中で新しいコレクションを「実体化」させてしまう、悪いコードの例(上:C#、下:VB)
これはよくない例である。このように途中でToList拡張メソッドを使うと、そこで新しいコレクションが「実体化」されてしまい、無駄にメモリを消費する(筆者の環境では、コメントに書いたように10MBytesほど増加した)。
このコードは、偶数だけを取り出すためにEnumerableクラスのWhere拡張メソッドを使っている。また、int/IntegerのままSum拡張メソッドに渡すとオーバーフローしてしまうので、渡す前にSelect拡張メソッドを使ってlong/Longへキャストすることでそれを避けている*1
また、冒頭で使っているEnumerableクラスのRangeメソッドは、引数に指定した範囲の整数シーケンスを生成してくれる(第1引数は開始数、第2引数は生成数)。コレクションのインスタンスを作成し、そこに「1」から「1,000,000」(=100万)までの整数を追加することと同じである。
なお、この他に、Enumerableクラスの拡張メソッドを使用するため、System.Linq名前空間のインポートが必要だ。

*1 ここでWhere拡張メソッドとSelect拡張メソッドの引数に記述してあるのは、ラムダ式である。ラムダ式について詳しくは、次のMSDNのドキュメントを参照していただきたい。


LINQの処理中に使うメモリを節約するには?

 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

途中で新しいコレクションを「実体化」させない、よいコードの例(上:C#、下:VB)
この例では、ToList拡張メソッドを取り除くことで、使用メモリの増加は全くなくなった。
なお、この他に、Enumerableクラスの拡張メソッドを使用するため、System.Linq名前空間のインポートが必要だ。

 これは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]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。