none
同じ内容の文字列を大量に作った時のGCの動作について RRS feed

  • 質問

  • 同じ文字列を大量に作った時、オブジェクトの参照は同じでないとしても実際の文字列が格納されているメモリが同じ場所を使っていてくれるなら

    わざわざデータをDictionary等を使ってまとめなくてもよいという事になります。

    または、仮に別々に使っていたとしても、ガベージコレクタが回収時に同一領域に圧縮してくれるなら自分は何もしなくて済みます。

    こういう処理はなされているのでしょうか?

    2010年8月31日 23:38

回答

すべての返信

  • これが回答になるのかわかりませんが、GCはメモリの分断化を防ぐため、メモリ内のオブジェクトの再配置を行います。したがって、知らぬ間にオブジェクトのアドレスが移動する可能性があります。これを防ぐためにはfixedステートメントでGCによる移動の対象外にすることができますが、このようなことをするのは特殊なケースでしょう。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    2010年9月1日 0:14
    モデレータ
  • >同じ文字列を大量に作った時、オブジェクトの参照は同じでないとしても実際の文字列が格納されているメモリが同じ場所を使っていてくれるなら

    文字列に関しては、そのような動作になっているはずです(インターンプール)。

    ただ、どんな長さの文字列も同じ動作とは限りません。

    内部の詳細まで分かりませんが、あまりに長い文字列だとプールから探し出すのに時間がかかるので、個人的には非常に長い文字列はプールしてないと予想してます。

    2010年9月1日 0:40
  • String.Intern()メソッドの解説 が参考になりますが、期待する動作をとることもできます。
    2010年9月1日 0:40
  • ガベージコレクトをするタイミングでそれらしい処理をしているという内容は、今しがたぱっと見したのですがマニュアルにはないような雰囲気です。

    何処かで見聞きされたのでしょうか、もし開発者の人の誰かが言っていたのであれば、このままシステムにお任せしてしまおうかと目論んでいます。

    元記事が何処かにあれば教えていただけますでしょうか?

    2010年9月1日 1:48
  • > 文字列に関しては、そのような動作になっているはずです(インターンプール)。

    4.0 は分かりませんが、1.1/2.0/3.5 では文字列のプール処理は一切されていないはずです。

    インターンプール(文字列定数プール)は、文字列定数を保持するだけで、実行時に作成された文字列はすべて string のインスタンス単位で個別のメモリ領域に格納されています。インターンプールを参照するためには、コーディング上で文字列定数に対してアクセスするか、実行時に (既に紹介されているように) Intern() メソッドを個別に呼び出して、インターンプール内を参照する string のインスタンスをちくいち取得するように実装する必要があります。

    インターンプールは CLR のホスト単位で管理されて破棄しずらいこともあり、大量の文字列を生成し、中長期間保持されるようであれば、書かれているように Dictionary などを利用したアプリケーション独自の文字列プールを作成されることは有効な手段です。アプリケーションに static な場所に保存するなど、ほぼ永続的に保持するような文字列であれば、インターンプールへの登録を考えてもよいかもしれません。

    3.5 で大規模アプリケーションを構築していないので、3.5 でのパフォーマンス変化はわかりませんが、1.1/2.0 では大規模なアプリケーションでは、独自の文字列プールを作成するのは常套手段でした。 (.NET 1.1 の頃は Generics がない都合もあって、コレクションに格納するための Int32 等のボックス化されたインスタンスもアプリケーションの持つプールから処理するのが非常に効果的でした。)

    GC については、詳細に確認はしていませんが、.NET 3.5 SP1 で簡単に大量の文字列生成と GC のマニュアル起動を試してみる限り、格納先を移動することはあっても同一内容の文字列の実体を1つにまとめることはしないようでした。C# in Depth とか Hongliang さんの部ログなどでもありますが、string 型が文字列実体へのポインタではなく文字列実体そのものを保持しているため、GC から操作することは困難だと思います。

    個人的には、ReferenceEquals の結果を違えてしまってよいならば、実体ではなく参照のすげかえは GC 側で出来るのでしょうけど、文字列の実体が同じ内容であることを確認するだけのコストを GC スレッドが捻出できないでしょう。

    2010年9月1日 3:39
  • >4.0 は分かりませんが、1.1/2.0/3.5 では文字列のプール処理は一切されていないはずです。

    文字列定数の事を指してます。

    仰る通り、.NET Frameworkのバージョンにより微妙に差異がありますが(仕様を変えたり戻したり)、どのバージョンでもプールはされてます。
    Intern メソッドを呼び出す必要はありませんし、ngenしてるアセンブリの場合、文字列定数は実行時にメモリを確保する事すらしてません(この挙動を変更することは可能)。

    更に、コンパイル時に同じ文字列定数をまとめてもいます(少なくともcsc.exeは)。

    参考まで、過去に私がブログで書いた記事です(非常につながりにくいのでGoogleのキャッシュです…)。

    http://webcache.googleusercontent.com/search?q=cache:lez4vUy4ok0J:blogs.wankuma.com/shuujin/archive/2007/10/11/101557.aspx+%E5%9B%9A%E4%BA%BA%E3%81%AE%E3%82%B8%E3%83%AC%E3%83%B3%E3%83%9E+string&cd=3&hl=ja&ct=clnk&gl=jp

    2010年9月1日 4:31
  • GC については、詳細に確認はしていませんが、.NET 3.5 SP1 で簡単に大量の文字列生成と GC のマニュアル起動を試してみる限り、格納先を移動することはあっても同一内容の文字列の実体を1つにまとめることはしないようでした。


    ildasmでSystem.Stringを見るとわかりますが、.NET 3.5まではReplaceCharInPlace()メソッドがあったりして、実はドキュメントに書かれているような不変値ではありません。そのためGCが自動的にインターンプールの文字列に置き換えるのは無理に思えます。

    ただし.NET 4ではどうやらこのメンバが廃止されstringは不変値になったようで、この辺りの挙動が変わっているかもしれません。

    2010年9月1日 4:57
  • ひろくん7 さん
    > ガベージコレクタが回収時に同一領域に圧縮してくれるなら自分は何もしなくて済みます。

    ガベージコレクタはメモリの再配置はしますが、String の Intern 化しないと思います。
    以下の資料にもするとは書いていませんでした。
    (追記:再配置(とかpin)の話も書いてませんね・・・根拠のために示したのですが、根拠にはなりませんでした)
    http://msdn.microsoft.com/ja-jp/library/bb985010.aspx
    http://msdn.microsoft.com/ja-jp/library/dd297765.aspx

    佐祐理さん
    > ReplaceCharInPlace()メソッド

    不変値じゃなかったんですね。
    ただ、これが不用意に行われると Hash 値が変わってしまうことになり、とても大きな問題になるはずですから、ユーザーからは不変と考えても大丈夫なぐらいとても限られた状況の下でのみ使われたのもだったのではと想像しました。(私のこの感想は、GC の話とは無関係です。)
    訂正:勝手に文字列値を変えられたら大変ですが、そうじゃないので Hash 値云々の話は間違っていました。

    それと、4のバージョンで以下のコードを試してみたところ、これまで通り Intern まではしていないようでした。これで確認になっているかは不安なため、間違っていたらご指摘ください。

    var s1 = "MyTest";
    var s2 = new StringBuilder().Append("My").Append("Test").ToString();
    MessageBox.Show(object.ReferenceEquals(s1, s2).ToString()); // false
    GC.Collect();
    MessageBox.Show(object.ReferenceEquals(s1, s2).ToString()); // false
    s2 = string.Intern(s2);
    MessageBox.Show(object.ReferenceEquals(s1, s2).ToString()); // true

    それと、よほどでない限り Intern の心配は不要で、システムに任せればよいと思いました。

    • 編集済み TH01 2010年9月2日 6:05 追記(訂正ばかりですみません)
    2010年9月2日 5:48
  • 前回に書いていることの繰り返しになってしましますが、

    > ガベージコレクタはメモリの再配置はしますが、String の Intern 化しないと思います。

    文字列プールを利用した文字列を保持するメモリの同一化と、Intern の使用する文字列定数プールは違うものです。後者は「定数」とつくことからもわかるように、永続的に保持する文字列定数を管理するものです。それに対して、前者は動的な文字列そのものが管理されて、変更・作成・破棄が行われるのが一般的な実装です。(Delphi .NET は Pascal String の参照管理はやめた…ですね?).NET 的には、前者は GC が管理するメモリ領域を利用し、後者は CLR が管理するメモリ領域を利用します。

    var s1 = "MyTest";
    var s2 = new StringBuilder().Append("My").Append("Test").ToString();
    var s3 = new StringBuilder().Append("My").Append("Test").ToString();
    
    

    このような3つの文字列オブジェクトにおいて、s1 が CLR の管理するメモリを参照する文字列オブジェクト (GC によって回収されない) であり、s2 と s3 が GC によって管理されたメモリを参照する文字列オブジェクトになります。

    最初の話題に戻ると、このとき s2 と s3 の保持する文字列が同じメモリ領域を指しているかどうか、指していないなら、それが GC によって同じになるように調整されるのか、また s1 と同じ領域に調整されることはあるのか?という話だと思います。(その答えは、すべて前回の投稿に書いたつもりです)

    2010年9月3日 4:00
  • K.Takaoka さん
    > 前回に書いていることの繰り返しになってしましますが、

    ごめんなさい。
    K.Takaoka さんの返信の文字は私の環境では小さすぎて、読んでませんでした。
    (T-Yokoo さんの style 指定の影響ですかね)

    > インターンプール(文字列定数プール)は、文字列定数を保持するだけで、実行時に作成された文字列はすべて string のインスタンス単位で個別のメモリ領域に格納されています。

    細かなことを言いますけど、K.Takaoka さんのこの文(とそれに続く文)からは、Intern はプールからの取得のみのように読めますが、存在しない場合はプールに追加されますね。

    var s1 = new StringBuilder().Append("My").Append("Test").ToString();
    MessageBox.Show((string.IsInterned(s1) == null).ToString()); // true
    var s2 = string.Intern(s1);
    MessageBox.Show(object.ReferenceEquals(s1, s2).ToString());  // true
    MessageBox.Show((string.IsInterned(s2) == null).ToString()); // false

    > また s1 と同じ領域に調整されることはあるのか?という話だと思います。

    ご指摘ありがとうございます。
    私の返信では、メモリの同一化は Intern しかないという前提で返信を書いてしまっていました。
    失礼しました。

    2010年9月3日 8:11