BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース メモリ使用量を減らし .NET のパフォーマンスを改善する

メモリ使用量を減らし .NET のパフォーマンスを改善する

原文(投稿日:2017/12/11)へのリンク

あなたのリクエストに応じて、ノイズを減らす機能を開発しました。大切な情報を見逃さないよう、お気に入りのトピックを選択して、メールとウェブで通知をもらいましょう。

.NET のパフォーマンスチューニングで誤解されがちな概念のひとつに、メモリアロケーション回避の重要性がある。メモリアロケーションは高速なため、パフォーマンスに大きな影響を与えることはほとんど無い、と考えられている。

この誤解の原因を理解するためには、C++ と Visual Basic 4・ で見られた COM プログラミングの時代に遡る必要がある。 COM ではメモリ管理は参照カウントのガベージコレクターによって行われていた。オブジェクトが参照変数に割り当てられる度、隠れカウンターが加算される。この変数が再割り当てされたりスコープから除外されると、カウンターは減算される。そしてカウンターがゼロになるとオブジェクトは削除され、メモリを解放し他の用途で使用できるようになる。

このメモリ管理のシステムは「決定的」である。注意深く分析すると、あるオブジェクトがいつ削除されるかを決定することができる。これは同様に、データベースコネクションのようなリソースを自動的に解放することができるよういうことも意味する。 .NET は対称的に、非メモリリソースがタイムリーに解放されることを保証するため別の機構(IDisposable/using など)が必要となる。

参照カウントのガベージコレクターには主に3つの欠点がある。1つめは、「循環参照」に影響を受けやすいということだ。もし2つのオブジェクトが互いに参照しあっていると、直接的でなかったとしても、参照カウントはゼロになってしまいメモリリークが発生する可能性がある。コードは、循環参照を避けるために注意深く書くか、オブジェクトが不要になった場合のループを抜けるような何らかのデコンストラクトメソッドを記述する必要がある。

2つめの主な欠点はマルチスレッド環境で動作している場合に発生する。競合状態を避けるため、参照カウントが常に正確であることを保証するような何らかのロック機構(Interlocked.Increment や spinlock など)が必要である。これらのオペレーションは非常にコストが高い。

最後の欠点は、使用可能なメモリ位置のリストが、細かく、存続しているオブジェクトの隙間の利用できない領域に断片化されてしまう可能性がある、ということだ。メモリアロケーションは、理想的なオブジェクトのための十分な領域を探すため、連続したフリーな位置の連結リストを辿ることがよくある。(メモリ断片化は .NET の Large Object Heap, LOH でも発生し得る。)

対称的に、 .NET や Java などのマーク・アンド・スイープ型のガベージコレクターにおけるメモリアロケーションでは、ポインターの加算だけの問題になる。ANDの割り当ては整数の割り当てよりもコストが低い。 GC が実行されたときにはじめて実際のコストが発生し、またそのコストは多くの場合、ジェネレーショナルコレクターによって緩和される。

.NET が生まれたときに立ち返ると、多くの人々が .NET のガベージコレクターの非決定的な振る舞いはパフォーマンス劣化をもたらし、またその原因も探りづらいものになると批判した。Microsoft の反論は、多くのユースケースにおいてマーク・アンド・スイープのガベージコレクターは、 GC が断続的に停止したとしても実際には高速である、というものだった。

不幸にも、このメッセージは時間とともに捻じ曲げられてきた。マーク・アンド・スイープのガベージコレクションが参照カウントよりも高速であるとする理論を受け入れたとしても、それ則ち絶対的な意味で高速である、とはならない。メモリアロケーション、そして関連付けられたメモリプレッシャーは、しばしばパフォーマンスの問題の検知を難しくする。

更に、メモリがアクティブに使用される量が増えると、 CPU のキャッシュ効率は下がる。メインの RAM は大きいため、ほとんどのユースケースでディスクベースの仮想メモリが使用されることは無いが、 CPU のキャッシュは比較するとかなり小さい。また、 RAM から CPU にキャッシュを読み込むためには数十から数百の CPU サイクルが必要となる。

最近の記事で、 Frans Bouma 氏がいくつかのメモリ使用を最適化するためのテクニックを紹介している。彼は特に ORM のパフォーマンスを改善しているが、その提案は様々な状況で役立つ。その一部は以下のようなものだ。

params 配列を避ける

params キーワードは便利だが、メモリアロケーションが必要なため通常の関数呼び出しよりも高コストとなる。 API は、よく使用されるパラメーターに対して非 params のオーバーロードを提供するべきだ。

また、IEnumerable<T> や IList<T> のオーバーロードは、関数呼び出しの前にコレクションが必ずしも配列にコピーする必要がないように提供されるべきだ。

直後にデータを追加するのであればデータ構造のサイズを規定しておく

List<T> や他のコレクションクラスは格納される度にリサイズされる。それぞれのリサイズ処理は内部の配列を、前回の配列が必要としていたサイズよりも大きくアロケートする。コレクションのコンストラクターでキャパシティをパラメーターで渡せば、このコストを回避できる。

メンバーを遅延初期化する

もし、与えられたオブジェクトが実際は常には必要でないことがわかっている場合は、アロケーションがあまりに早く実行されてしまうことを避けるため、遅延初期化を採用すべきだ。これは通常は手で行われるが、 Lazy<T> クラス自身はアロケーションが必要となる。

2011年に遡り、我々はMicrosoft による Task のサイズ削減について同様の方法で報告した。彼らは Task<Int32> を生成する時間を 49% から 55% 削減し、また Task<Int32> 自体のサイズを 52% 削減したと述べた。

 
 

Rate this Article

Adoption Stage
Style
 
 

この記事に星をつける

おすすめ度
スタイル

BT