Hatena::ブログ(Diary)

当面C#と.NETな記録 このページをアンテナに追加 RSSフィード

2007/9/1 (土)

[][] call と callvirt その5  call と callvirt その5 - 当面C#と.NETな記録 を含むブックマーク  call と callvirt その5 - 当面C#と.NETな記録 のブックマークコメント

5月の記事の続き。続きを書く日が来るとは自分でも驚き。IL のお話です。

インスタンスメソッドを呼ぶときにコンパイラが call を生成したり、callvirt を生成したりします。その違いは何よ?とずっと疑問だったんですが、その答えが!

vir http://blogs.msdn.com/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx

インスタンスの NULL チェックが不要なときは call だそうな。逆に NULL チェックを強制したいときが callvirt。NULL チェックの奇妙な機械語の件が The Old New Thing で取り上げられています。

NULL チェックが不要なときの例 : (new Foo()).FooNonVirtualMethod()

ちょっと実験。

using System;

class Program
{
    static void Main()
    {
        Hoge hoge = new Hoge();
        Console.WriteLine( hoge.Huga() );
        Console.WriteLine( new Hoge().Huga() );
    }
}

public class Hoge
{
    public string Huga() { return "HogeHuga"; }
}

IL。Main だけ。

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // コード サイズ       33 (0x21)
  .maxstack  1
  .locals init ([0] class Hoge hoge)
  IL_0000:  newobj     instance void Hoge::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  callvirt   instance string Hoge::Huga()
  IL_000c:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0011:  newobj     instance void Hoge::.ctor()
  IL_0016:  call       instance string Hoge::Huga()
  IL_001b:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0020:  ret
} // end of method Program::Main

IL_0007 は callvirt ですが、IL_0016 は確かに call になっています。

へぇなトリビアですね。

ちなみにエリックさんの同じエントリでC#コンパイラのバグでスレッドのデッドロックに至る可能性があるバグが告白されていたり。3.0でも健在だって。

NyaRuRuNyaRuRu 2007/09/02 13:30 >C#コンパイラのバグ

個人的な意見ですが,コンパイラのバグと言うよりは,CLRとBCLで配慮が足りなかった部分をコンパイラに押しつけているようにみえますなぁ.
http://d.hatena.ne.jp/NyaRuRu/20060605

ローカル変数への代入とメソッド呼び出しの順序について,通常は 1)メソッド呼び出し 2)ローカル変数への代入 ですが,lock や using の場面でこの順序を反転させる必要があることを,CLR や BCL の人たちがちゃんと把握していたのかちょっと怪しいと思っています.

siokoshousiokoshou 2007/09/03 01:33 なるほど〜、押し付けあっていつまでも直らないってありがちな構図かもしれませんねぇ。
そして、usingも解放漏れするんですか(´・ω・`)

2007/5/19 (土)

[][] call と callvirt その3 : struct と callvirt  call と callvirt その3 : struct と callvirt - 当面C#と.NETな記録 を含むブックマーク  call と callvirt その3 : struct と callvirt - 当面C#と.NETな記録 のブックマークコメント

MSDNライブラリの Constrained の説明によると、「通常、callvirt 命令は値型では有効ではありません。」だそうだけど、普通に動いた。ILの世界はおおらかだ。

using System;
using System.Reflection.Emit;

struct Baz
{
  private int n;
  public int Value { get { return n; } }
  public Baz( int n ) { this.n = n; }
  public override string ToString() { return "Baz: " + n; }
}

class Class5
{
  static void Main()
  {
    Baz baz = new Baz( 2 );

    Console.WriteLine( " baz.Value: {0}", baz.Value );

    // Valueプロパティを読む
    DynamicMethod dm = new DynamicMethod( "", typeof( int ),
      new Type[] { typeof( Baz ) }, typeof( Class5 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarga_S, ( byte ) 0 );
    //ilgen.Emit( OpCodes.Call, typeof( Baz ).GetProperty( "Value" ).GetGetMethod() );
    ilgen.Emit( OpCodes.Callvirt, typeof( Baz ).GetProperty( "Value" ).GetGetMethod() );
    ilgen.Emit( OpCodes.Ret );
    Console.WriteLine( dm.Invoke( null, new object[] { baz } ) );

    // ToString() を呼ぶ
    DynamicMethod dm2 = new DynamicMethod( "", typeof( string ),
      new Type[] { typeof( Baz ) }, typeof( Class5 ) );
    ILGenerator ilgen2 = dm2.GetILGenerator();

    ilgen2.Emit( OpCodes.Ldarga_S, ( byte ) 0 );
    //ilgen2.Emit( OpCodes.Call, typeof( Baz ).GetMethod( "ToString" ) );
    ilgen2.Emit( OpCodes.Callvirt, typeof( Baz ).GetMethod( "ToString" ) );
    ilgen2.Emit( OpCodes.Ret );
    Console.WriteLine( dm2.Invoke( null, new object[] { baz } ) );
    
    Console.ReadKey();
  }
}

この例では、call/callvirt共に正常に動く。

ただし、BazがToStringをオーバーライドしていないとヌルポになる。で、それを解決してくれるのが constrained。でも、残念ながら値型のインスタンスメソッド(非仮想メソッド)には対応してないみたい。値型の仮想メソッドには対応している。


[] call と callvirt その4  call と callvirt その4 - 当面C#と.NETな記録 を含むブックマーク  call と callvirt その4 - 当面C#と.NETな記録 のブックマークコメント

ILはごく少数の型しか意識しないそうだ(規格のあちらこちらで意識する型の数が違うような…(^^;)。CLS(共通言語規定)やCTS(共通型システム)はコンパイラなどのIL生成器ががんばるところらしい。


海外のcallとcallvirtの記事


規格書から目に付いたところをいくつか

  • CLIのマネージポインタと参照の違い(12.1.1.2より)

オブジェクト参照(規格ではO型)はオブジェクトの外側、すなわちオブジェクト全体を参照する。操作の数は少なめ(ceq, ldind.ref, call, dsfld, stfldなど)。

マネージポインタ(同&型)はオブジェクトの内側を参照する。オブジェクト内のフィールドや配列内部の要素を指すことが許可される。オブジェクトや配列先頭を指すことは許可されない。評価スタック、静的変数、管理外メモリ(!)を指すことも許される。

  • バイト順序(12.6.3)

バイト順序はCPU依存だそうで、順序に依存するコードはすべてのプラットフォーム上で動作しなくてもよいそうな。MacSilverlightPowerPCでも動くのかな?

  • JISメモ

委譲 - デリゲート

特性 - プロパティ (最初なんのことかわからなかったw)

2007/5/18 (金)

[][] ILの call と callvirt の違い その2  ILの call と callvirt の違い その2 - 当面C#と.NETな記録 を含むブックマーク  ILの call と callvirt の違い その2 - 当面C#と.NETな記録 のブックマークコメント

nullもガッと鳴いたよ。

using System;
using System.Reflection.Emit;

class Duck
{
  public void Say() { Console.WriteLine( "ガッ" ); }
}

class Class3
{
  static void Main()
  {
    // void Do() メソッドを作る
    DynamicMethod dm = new DynamicMethod( "Do", null, null, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    // null をスタックに push
    ilgen.Emit( OpCodes.Ldnull );
    // Duck.Say() を呼ぶ
    ilgen.Emit( OpCodes.Call, typeof( Duck ).GetMethod( "Say" ) );
    //ilgen.Emit( OpCodes.Callvirt, typeof( Duck ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );

    // Do() を実行
    dm.Invoke( null, null );
    Console.ReadKey();
  }
}

ducktypingどころの話じゃなかった。Duckは一度も生成していない。

正確にはcallそのものはオブジェクト参照がスタックになくてもよい。(thisを必要としない)static callができるのでこれはまぁ当然。ただし、Duck.Say()は暗黙にthisを取るのでthisは必要。このコードではthisがnull。

対して、callvirtはオブジェクト参照が必要。必要といってもこっちもチェックしてるんだかどうなんだか。

↑のコードのcallをEmitしている行をコメントアウトし、callvirtをEmitしている行を活かすと内部例外がNullReferenceExceptionのTargetInvocationExceptionが出る。

パラメータチェックが甘いと解釈すべきなのかなぁ?

2007/5/17 (木)

[][] ILの call と callvirt の違い  ILの call と callvirt の違い - 当面C#と.NETな記録 を含むブックマーク  ILの call と callvirt の違い - 当面C#と.NETな記録 のブックマークコメント

当面ILとC#と.NETな記録。インサイドかもしれないし、ダークサイドかもしれない実験記です。

実験1

ILの call と callvirt の違いがよくわからないのにプロパティを読もうってのがそもそもの間違いだなと思って、比較するコードを書いてみました。

よくある継承のサンプルコードでC#のコール、ILのcall、callvirtを比較。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ")" );
  }
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived();
    Console.WriteLine( " b.Say():" );
    b.Say();

    Console.WriteLine( "\n d.Say():" );
    d.Say();

    Console.WriteLine( "\n CallSay( b ):" );
    CallSay( b );

    Console.WriteLine( "\n CallSay( d ):" );
    CallSay( d );

    Console.WriteLine( "\n CallvirtSay( b ):" );
    CallvirtSay( b );

    Console.WriteLine( "\n CallvirtSay( d ):" );
    CallvirtSay( d );

    Console.ReadKey();
  }

  delegate void Call( Base b );

  static void CallSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果は…

 b.Say():
Base (Base)

 d.Say():
Derived (Derived)

 CallSay( b ):
Base (Base)

 CallSay( d ):
Derived (Derived)

 CallvirtSay( b ):
Base (Base)

 CallvirtSay( d ):
Derived (Derived)

あれ、そのまんま。

つづく


実験2

違いがないはずがないので、コードを改造してみます。

callとcallvirtに与えるMethodInfoを引数の型、Base型、Derived型で3つ呼んでみます。それって動かないんじゃ?と思って試してみたら、動いてしまいました。

わかりやすくするため、Derivedにプライベートなフィールドを追加します。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  private int n; // 追加

  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ") " + n );
  }

  public Derived( int x ) { this.n = x; } // 追加
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived( 5 );  // 変更
    Console.WriteLine( " b.Say():" );
    b.Say();

    Console.WriteLine( "\n d.Say():" );
    d.Say();

    Console.WriteLine( "\n CallSay( b ):" );
    CallSay( b );

    Console.WriteLine( "\n CallSay( d ):" );
    CallSay( d );

    Console.WriteLine( "\n CallvirtSay( b ):" );
    CallvirtSay( b );

    Console.WriteLine( "\n CallvirtSay( d ):" );
    CallvirtSay( d );

    Console.ReadKey();
  }

  delegate void Call( Base b );

  static void CallSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果は

 b.Say():
Base (Base)

 d.Say():
Derived (Derived) 5

 CallSay( b ):
Base (Base)
Base (Base)
Derived (Base) 0     ← おぉっ (^^;

 CallSay( d ):
Derived (Derived) 5
Base (Derived)       ← Baseを呼べた!
Derived (Derived) 5

 CallvirtSay( b ):
Base (Base)
Base (Base)
Base (Base)          ← これはいいの?

 CallvirtSay( d ):
Derived (Derived) 5
Derived (Derived) 5
Derived (Derived) 5

callにDerivedのthisを渡して、BaseのSayを呼べるってところがキモでしょうか。

一部、なんで動くの?ってところで混乱してます…。見なかったことにしたい…。


実験 その3

まださっぱりわからないのでもうちょっと実験。

Base/Derivedと継承関係のない、でもSay()をたまたま持っているクラスはどうなる?って実験。

Say()を持ったDuckクラスを追加してみます。意味深な名前ですが、これをDuckTypingと言えるのかどうか…。CallSayとCallvirtSayはobjectを取るようにします。Base/Derivedは変更なし。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  private int n;

  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ") " + n );
  }

  public Derived( int x ) { this.n = x; }
}

class Duck  // 追加
{
  public void Say()
  {
    Console.WriteLine( "ガー" );
  }
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived( 5 );
    Duck duck = new Duck();  // 追加

    Console.WriteLine( " b.Say():" );  b.Say();
    Console.WriteLine( "\n d.Say():" );  d.Say();
    Console.WriteLine( "\n duck.Say():" );  duck.Say();

    Console.WriteLine( "\n CallSay( b ):" );  CallSay( b );
    Console.WriteLine( "\n CallSay( d ):" );  CallSay( d );
    Console.WriteLine( "\n CallSay( duck ):" );  CallSay( duck );

    Console.WriteLine( "\n CallvirtSay( b ):" );  CallvirtSay( b );
    Console.WriteLine( "\n CallvirtSay( d ):" );  CallvirtSay( d );
    Console.WriteLine( "\n CallvirtSay( duck ):" );  CallvirtSay( duck );

    Console.ReadKey();
  }

  delegate void Call( object b );  // objectに変更

  static void CallSay( object b )  // objectに変更
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( object ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Duck ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( object b )  // objectに変更
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( object ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Duck ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果

 b.Say():
Base (Base)

 d.Say():
Derived (Derived) 5

 duck.Say():
ガー

 CallSay( b ):
Base (Base)
Base (Base)
Derived (Base) 0
ガー                 ← もう驚かない

 CallSay( d ):
Derived (Derived) 5
Base (Derived)
Derived (Derived) 5
ガー

 CallSay( duck ):
ガー
Base (Duck)          ← (^^;
Derived (Duck) 0     ← (^^;
ガー

 CallvirtSay( b ):
Base (Base)
Base (Base)
Base (Base)
ガー                 ← あれ (^^;

 CallvirtSay( d ):
Derived (Derived) 5
Derived (Derived) 5
Derived (Derived) 5
ガー                 ← あれ (^^;

 CallvirtSay( duck ):
ガー
ガー
ガー
ガー

なんとなくわかったことは、どの型のどのメソッドを呼ぶかを決めるのは言語の仕事で、ILはシグネチャが一致してれば何でも呼んじゃうよってことですか。

じゃあ、C#はどれをどう使ってるのか見てみる。Mainのはじめの部分のILから抜粋。

callvirt   instance void Base::Say() // b.Say()
callvirt   instance void Base::Say() // d.Say()
callvirt   instance void Duck::Say() // duck.Say()

なるほど。変数の型::メソッドをcallvirtで呼ぶ、と。

これまでのまとめ。

call : 指定メソッドそのものを呼ぶ。スーパークラスのメソッドを呼べる。

callvirt : 最派生を呼ぶ。

どちらも : 名前とシグネチャがあっていれば何でも呼べちゃう(?)。thisの型は無視(?)、所詮ポインタ(?)

これを手がかりに、JIS検索で JISX3016 をもっと読んでみます。


[][] DynamicMethodによる動的生成の方法  DynamicMethodによる動的生成の方法 - 当面C#と.NETな記録 を含むブックマーク  DynamicMethodによる動的生成の方法 - 当面C#と.NETな記録 のブックマークコメント

http://blogs.msdn.com/joelpob/archive/2004/03/31/105282.aspx

デリゲート化は必須ではないみたい。

2007/5/15 (火)

[] Constrained  Constrained - 当面C#と.NETな記録 を含むブックマーク  Constrained - 当面C#と.NETな記録 のブックマークコメント

Constrainedって仮想メソッド呼ぶときにしか使えないだけなのか…。素直にCall使えばよさげ。手抜きできると思ったのに…。

2005 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 06 | 09 | 11 | 12 |
2007 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 08 | 09 | 10 | 12 |
2009 | 01 | 03 | 04 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 07 |
2011 | 04 | 07 | 10 |
2012 | 04 | 12 |
2013 | 08 |
2014 | 03 | 08 |
2017 | 09 |