62

CIL命令「Call」と「Callvirt」の違いは何ですか?

4

6 に答える 6

60

ランタイムがcall命令を実行すると、コード (メソッド) の正確な部分が呼び出されます。それがどこに存在するかについては疑問の余地はありません。 IL が JIT されると、呼び出しサイトで生成されるマシン コードは無条件jmp命令になります。

対照的に、callvirt命令は多態的な方法で仮想メソッドを呼び出すために使用されます。メソッドのコードの正確な場所は、呼び出しごとに実行時に決定する必要があります。結果として得られる JITted コードには、vtable 構造による間接化が含まれます。したがって、呼び出しの実行は遅くなりますが、多態的な呼び出しが可能になるという点でより柔軟です。

コンパイラはcall仮想メソッドの命令を発行できることに注意してください。例えば:

sealed class SealedObject : object
{
   public override bool Equals(object o)
   {
      // ...
   }
}

コードの呼び出しを検討してください。

SealedObject a = // ...
object b = // ...

bool equal = a.Equals(b);

whileSystem.Object.Equals(object)は仮想メソッドですが、この使用法では、Equalsメソッドのオーバーロードが存在する方法はありません。 SealedObjectは封印されたクラスであり、サブクラスを持つことはできません。

このため、.NET のsealedクラスは、封印されていないクラスよりもメソッド ディスパッチのパフォーマンスが向上します。

編集:私が間違っていたことがわかりました。thisオブジェクトの参照 (メソッド内の の値) が null である可能性があるため、C# コンパイラはメソッドの場所に無条件にジャンプできません。代わりにcallvirt、必要に応じてヌル チェックを行い、スローするものを発行します。

これは、Reflector を使用して .NET フレームワークで見つけたいくつかの奇妙なコードを実際に説明しています。

if (this==null) // ...

ポインター (local0)の null 値を持つ検証可能なコードをコンパイラが出力する可能性がありますがthis、これを行わないのは csc だけです。

callしたがって、クラスの静的メソッドと構造体にのみ使用されると思います。

sealedこの情報を考えると、これは API セキュリティにのみ役立つように思えます。クラスを封印してもパフォーマンス上の利点がないことを示唆していると思われる別の質問を見つけました。

編集 2:これには、見た目以上のものがあります。たとえば、次のコードはcall命令を発行します。

new SealedObject().Equals("Rubber ducky");

明らかに、このような場合、オブジェクト インスタンスが null になる可能性はありません。

興味深いことに、DEBUG ビルドでは、次のコードが出力されcallvirtます。

var o = new SealedObject();
o.Equals("Rubber ducky");

これは、2 行目にブレークポイントを設定して の値を変更できるためですo。リリース ビルドでは、呼び出しはcallではなくになると思いcallvirtます。

残念ながら、私の PC は現在動作していませんが、再開したらこれを試してみます。

于 2008-10-11T10:51:59.837 に答える
51

call非仮想メソッド、静的メソッド、またはスーパークラスメソッドを呼び出すためのものです。つまり、呼び出しのターゲットはオーバーライドの対象ではありません。callvirt仮想メソッドを呼び出すためのものです(メソッドthisをオーバーライドするサブクラスの場合、代わりにサブクラスバージョンが呼び出されます)。

于 2008-10-11T10:45:14.327 に答える
11

このため、.NET のシールされたクラスは、シールされていない対応するクラスよりもメソッド ディスパッチのパフォーマンスが向上する可能性があります。

残念ながら、そうではありません。Callvirt は、それを便利にするもう 1 つのことを行います。オブジェクトに呼び出されたメソッドがある場合、callvirt はオブジェクトが存在するかどうかを確認し、存在しない場合は NullReferenceException をスローします。Call は、オブジェクト参照が存在しない場合でもメモリ ロケーションにジャンプし、そのロケーションでバイトを実行しようとします。

つまり、callvirt は常に C# コンパイラ (VB については不明) によってクラスに使用され、call は常に構造体に使用されます (構造体は null またはサブクラス化できないため)。

編集Drew Noakes コメントへの応答: はい、コンパイラに任意のクラスの呼び出しを発行させることができるようですが、次の非常に特殊なケースでのみ:

public class SampleClass
{
    public override bool Equals(object obj)
    {
        if (obj.ToString().Equals("Rubber Ducky", StringComparison.InvariantCultureIgnoreCase))
            return true;

        return base.Equals(obj);
    }

    public void SomeOtherMethod()
    {
    }

    static void Main(string[] args)
    {
        // This will emit a callvirt to System.Object.Equals
        bool test1 = new SampleClass().Equals("Rubber Ducky");

        // This will emit a call to SampleClass.SomeOtherMethod
        new SampleClass().SomeOtherMethod();

        // This will emit a callvirt to System.Object.Equals
        SampleClass temp = new SampleClass();
        bool test2 = temp.Equals("Rubber Ducky");

        // This will emit a callvirt to SampleClass.SomeOtherMethod
        temp.SomeOtherMethod();
    }
}

これが機能するために、クラスをシールする必要はありません。

したがって、これらすべてが true の場合、コンパイラは呼び出しを発行するように見えます。

  • メソッド呼び出しはオブジェクト作成直後
  • メソッドは基本クラスに実装されていません
于 2008-10-11T11:02:48.770 に答える
7

MSDNによると:

電話

呼び出し命令は、命令とともに渡されたメソッド記述子によって示されるメソッドを呼び出します。メソッド記述子は、呼び出すメソッドを示すメタデータトークンです...メタデータトークンは、呼び出しが静的メソッド、インスタンスメソッド、仮想メソッド、またはグローバル関数のいずれであるかを判断するのに十分な情報を伝達します。これらすべての場合において、宛先アドレスはメソッド記述子から完全に決定されます(これを、仮想メソッドを呼び出すためのCallvirt命令と比較してください。宛先アドレスは、Callvirtの前にプッシュされたインスタンス参照の実行時タイプにも依存します)。

CallVirt

callvirt命令は、オブジェクトのレイトバウンドメソッドを呼び出します。つまり、メソッドは、メソッドポインタに表示されるコンパイル時クラスではなく、実行時型のobjに基づいて選択されます。Callvirtは、仮想メソッドとインスタンスメソッドの両方を呼び出すために使用できます。

したがって、基本的に、オーバーライドされているかどうかに関係なく、オブジェクトのインスタンスメソッドを呼び出すためにさまざまなルートが使用されます。

呼び出し:変数->変数の型オブジェクト->メソッド

CallVirt:変数->オブジェクトインスタンス->オブジェクトのタイプオブジェクト->メソッド

于 2010-10-03T13:34:05.793 に答える