はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】インターンプールを用いてstring生成によるヒープのメモリ確保を防ぐ方法(メモリを占有しつづけるのでそこは注意)

はじめに

今回はなるべくstringでヒープへのメモリ確保を抑えるためにインターンプールを利用する方法について紹介したいと思います。

概要

通常stringはヒープにメモリが確保され、ガベージコレクションの対象になります。ただし同一の文字列なら同一のメモリ領域を利用した方が良いのではということで利用されるのがインターンプールです。

new stringで生成した文字列は、文字列の値として同一のものも、異なるメモリ領域に確保しています。ただし定数文字列のみ、インターンプールと呼ばれるアプリケーション共有の領域から、一意の参照を取得します。

ZString – Unity/.NET CoreにおけるゼロアロケーションのC#文字列生成 | Cygames Engineers' Blog

共通言語ランタイムは、インターン プールと呼ばれるテーブルを自動的に保持します。このテーブルには、プログラムで宣言された各一意のリテラル文字列定数の 1 つのインスタンスと、 メソッドを呼び出してプログラムによって追加した一意の String インスタンスが Intern 含まれます。

インターン プールは文字列ストレージを節約します。 リテラル文字列定数を複数の変数に割り当てると、各変数は、同じ値を持つ の複数の異なるインスタンス String を参照する代わりに、インターン プール内の同じ定数を参照するように設定されます。

String.IsInterned(String) メソッド (System) | Microsoft Learn

string.Internとstring.IsInterned

インターンプールへの文字列の登録・インターンプールに存在する文字列の取得にはstring.Internstring.IsInternedを用います。
String.Intern(String) メソッド (System) | Microsoft Learn
String.IsInterned(String) メソッド (System) | Microsoft Learn

  • string.Intern : インターンプールに文字列が存在する場合はその参照を取得し、存在しない場合は登録しその参照を返します
  • string.IsInterned : インターンプールに文字列が存在する場合はその参照を取得し、存在しない場合はnullを返します
// インターンプールに存在する場合はそれの参照を取得、存在しない場合は登録しその参照を返します
// 引数にnullを渡すとArgumentNullExceptionが返ります
string a = string.Intern("ここの文字列はインターンプールに登録されます。");
string b = string.Intern("ここの文字列はインターンプールに登録されます。");
        
// ここの文字列はインターンプールに登録されます。
Console.WriteLine(a);

// True
Console.WriteLine(ReferenceEquals(a, b));
                
        
// インターンプールに存在する場合はその参照を取得し、存在しない場合はnullを返します
// 引数がnullならArgumentNullExceptionが返ります
string c = string.IsInterned(new StringBuilder().Append("インターンプールに登録されていない文字列").Append("ならnullを返します。").ToString());
string d = string.IsInterned(new StringBuilder().Append("ここの文字列は").Append("インターンプールに登録されます。").ToString());
        
// True (== null)
Console.WriteLine(c == null);
        
// False (!= null)
Console.WriteLine(d == null);
        
// True
Console.WriteLine(ReferenceEquals(a, d));

明示的に登録しなくても登録される場合

実はソースコードの中にハードコーディングされているリテラル文字列は自動でインターンプールに登録されています。

var str1 = "あいうえお";
var str2 = "あいうえお";
                
// True
Console.WriteLine(ReferenceEquals(str1, str2));
        
// False
Console.WriteLine(ReferenceEquals(str1, new StringBuilder().Append("あいう").Append("えお").ToString()));

どうやらコンパイル時点で文字列リテラルに置き換わっているものが対象のようです。(要調査)

注意点

一度インターンプールに文字列が登録されると、アプリケーションが終了するまでメモリを占有し続けるのでそこは注意してください。