連載:深入り.NETプログラミング

.NETと安全なポインタ

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2009)
2009/01/27
Page1 Page2

 連載第2回となる今回は、「管理下ポインタ」(Managed Pointer)について取り上げる。

C#は参照渡しをサポートする

 C#は関数引数の参照渡しをサポートしている。といっても、C#コンパイラがすべき仕事はほとんどない。参照渡しの仕組みはCommon Language Infrastructure(CLI)によって提供されており、C#コンパイラは必要なメタデータと中間言語(IL)コードを出力しているだけである。

 対照的に、Java Virtual Machine(JVM)は関数引数の参照渡しをサポートしない。JVM上で動く言語を設計するとして、その言語で参照渡しをサポートしたければ道は1つ、JVMがサポートする機能を用いて参照渡しをエミュレートすることである。つまりコンパイル時に参照渡しを「消去」する必要がある。

 実際にC#のコード例で見てみよう。クラスFooのインスタンスを作り、そのValueフィールドを参照渡しで書き換えるというプログラムだ。

using System;

public class Foo
{
  public int Value;
}

public static class Program
{
  public static void Test(ref int value)
  {
    value = 0x76543210;
  }
  public static void Main()
  {
    var foo = new Foo();

    foo.Value = 0x01234567;
    Console.WriteLine(foo.Value);

    Test(ref foo.Value);
    Console.WriteLine(foo.Value);
  }
}
関数引数の参照渡しを行うサンプル・プログラム(C#)
Testメソッドの引数が(C#の)refキーワードにより参照渡しになっている。

 このプログラムは次のようなILコードに変換される。

.method public hidebysig static void Test(int32& 'value') cil managed
{
  .maxstack 8
  L_0000: ldarg.0
  L_0001: ldc.i4 0x76543210
  L_0006: stind.i4
  L_0007: ret
}

.method public hidebysig static void Main() cil managed
{
  .entrypoint
  .maxstack 2
  .locals init ([0] class Foo foo)
  L_0000: newobj instance void Foo::.ctor()
  L_0005: stloc.0
  L_0006: ldloc.0
  L_0007: ldc.i4 0x1234567
  L_000c: stfld int32 Foo::Value
  L_0011: ldloc.0
  L_0012: ldfld int32 Foo::Value
  L_0017: call void [mscorlib]System.Console::WriteLine(int32)
  L_001c: ldloc.0
  L_001d: ldflda int32 Foo::Value
  L_0022: call void Program::Test(int32&)
  L_0027: ldloc.0
  L_0028: ldfld int32 Foo::Value
  L_002d: call void [mscorlib]System.Console::WriteLine(int32)
  L_0032: ret
}
関数引数の参照渡しを行うサンプル・プログラムの中間言語コード(IL)

 L_001dでValueフィールドのアドレスを取得し、それを引数としてTestメソッドを呼び出している。Fooクラスのインスタンス(foo)は参照型オブジェクトなので、このアドレスはガベージ・コレクション(GC)ヒープ上を指すことになる。これは管理下ポインタであり、ここでは「int32&」という型を持つ。

 MicrosoftのCLI実装であるCommon Language Runtime(CLR)のJITコンパイラは、上記のコードを非常に素直なx86コードに変換する。デバッガでネイティブ・コードを見てみれば、CLRは管理下ポインタを本当の意味でのメモリ・アドレスとして受け渡していることが分かる。確かにこれなら実行効率は良い。しかしGCヒープ上のオブジェクトはGCによって移動することがあるはずだ。なぜこれは問題にならないのだろうか?

 実は、問題になりそうな場面でCLRが自動的に管理下ポインタを書き換えている。JITコンパイル後の管理下ポインタがどこにあるかは、スタックやレジスタに至るまですべてCLRによって追跡されている。これらの情報と、GCによってアドレスが変化したオブジェクトのリストが分かれば、GC発生時にどの管理下ポインタをどんな値に修正すればよいかが完全に決定できる。すべての修正はGCによってスレッドが停止している間に行われるため、各スレッドから見れば、管理下ポインタは常に正しいオブジェクトを指しているように見えるわけだ。

 アドレス書き換えは、管理下ポインタのみが利用しているのだろうか? 否、このような書き換えは、参照型のオブジェクトの追跡でも活用されている。CLRのJITコンパイラが出力するコードでは、参照型オブジェクトのメモリ上の実アドレスを受け渡すことでパフォーマンスを稼いでいる。このアドレスはGC発生時に変化することがあるため、やはり上記のようなアドレスの書き換えが必要になる。同様の技法は、パフォーマンスを重視した多くのJVM実装でも使用されている。ならば、最初から複雑なVM実装を覚悟し、管理下ポインタをVMレベルでサポートするという選択もひとつの解なのだろう。

管理下ポインタ(Managed Pointer)の特徴

 管理下ポインタについてもう少し詳しく見てみよう。

 管理下ポインタは、以下のような条件を満たす限り、正当性の検証が可能である。この条件の下では、ガベージ・コレクション(GC)でオブジェクトが移動されても管理下ポインタは有効なままである。

●管理下ポインタが次の場所をポイントしている

  • メソッド引数、ローカル変数、ボックス化された値型
  • インスタンス・フィールド
  • 静的フィールド
  • 配列の要素(と最終要素の次の要素)

●管理下ポインタはメソッド内で作成され、これを外部の記憶領域(ヒープや静的変数)に持ち出さない。メソッド内で管理下ポインタの複製を作ってもよいが、すべての複製はメソッドのローカル変数、メソッド引数、またはメソッドの戻り値にのみ存在する

  • ただし、メソッドからのリターンで無効化する領域(メソッド引数やメソッドのローカル変数)を指す管理下ポインタをメソッド戻り値として持ち出さない

●管理下ポインタはnullではない

●管理下ポインタを数値として扱って、別の領域の管理下ポインタを作成しない

●管理下ポインタで別の管理下ポインタを指さない

 管理下ポインタは、C++の参照型をさらに扱いやすくしたものと考えれば分かりやすい。正当性検証可能なコードでは、管理下ポインタが無効領域を指すことはない。たとえメモリ・コンパクションが行われる処理系での実行であっても、管理下ポインタは意味的に同じ場所を指し続ける。

管理下ポインタ(Managed Pointer)の利用事例

 C#やVisual Basic(VB)でプログラミングしていると気付きにくいが、実はいろいろなところで管理下ポインタは使われている。いくつか利用例を見てみよう。

●引数の参照渡し

 先ほど見たように、C#のref/outキーワード(VBではByRef/<Out> ByRefキーワード)を使った参照渡しは、管理下ポインタによって実現されている。

●値型オブジェクトの周辺

 値型オブジェクトとともに管理下ポインタは多用される。インスタンス・メソッドの呼び出し、フィールド・アクセス、値型配列の要素へのアクセス、いずれも管理下ポインタの出番である。

 値型オブジェクトのインスタンス・メソッドを呼び出すときには、thisポインタとしてまず対象オブジェクトの管理下ポインタを取得し、それを評価スタックにプッシュする。

 フィールド・アクセスでも管理下ポインタが利用される。

using System.Diagnostics;

struct Foo{ public int Value; }
struct Bar{ public Foo Foo; }
struct Buz{ public Bar Bar; }

static class Program
{
  static void Main(string[] args)
  {
    Buz buz = new Buz();
    buz.Bar.Foo.Value = 1;

    Debug.Assert(buz.Bar.Foo.Value == 1);
  }
}
フィールド・アクセスを行うサンプル・プログラム(C#)

 このコードが正しく動くことを不思議に思ったことはないだろうか? Buz.Bar、Bar.Foo、Foo.Valueはいずれも値型である。いったんフィールドのコピーを取得して、それを更新し、さらに再代入しているのだろうか?

 そうではない。このコードでは、管理下ポインタを利用して構造体の中に埋め込まれたフィールドを直接ポイントしている。以下のILコードが示すように、最終的にはスタック上のbuz.Bar.Foo.Valueを指す管理下ポインタが取得され、そのアドレスに1をセットしている。

.locals init ([0] valuetype Buz buz)
ldloca.s buz
initobj Buz
ldloca.s buz
ldflda valuetype Bar Buz::Bar
ldflda valuetype Foo Bar::Foo
ldc.i4.1
stfld int32 Foo::Value
フィールド・アクセスを行うサンプル・プログラムの中間言語コード(IL)

 値型配列の要素にアクセスするときにも管理下ポインタが必要になる。

using System.Diagnostics;

struct Foo
{
  public int Value;
}

static class Program
{
  static void Main(string[] args)
  {
    var foos = new Foo[1];
    foos[0].Value = 1;

    Debug.Assert(foos[0].Value == 1);
  }
}
値型配列の要素にアクセスするサンプル・プログラム(C#)

 状況は先ほどのフィールドへのアクセスと同じだ。ここでも管理下ポインタによって0番目の配列要素を直接ポイントしている。もちろん、管理下ポインタ取得後にGCが発生して、配列が移動しても問題ない。アドレスの修正はCLRが自動的に行ってくれる。

 .NETの特色である値型は、管理下ポインタによって支えられているといえる。


 INDEX
  [連載]深入り.NETプログラミング
  .NETと安全なポインタ
  1.管理下ポインタ(Managed Pointer)の特徴と具体例
    2.管理下ポインタによって支えられている.NETの値型

インデックス・ページヘ  「深入り.NETプログラミング」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間