CIL命令「Call」と「Callvirt」の違いは何ですか?
6 に答える
ランタイムが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 は現在動作していませんが、再開したらこれを試してみます。
call
非仮想メソッド、静的メソッド、またはスーパークラスメソッドを呼び出すためのものです。つまり、呼び出しのターゲットはオーバーライドの対象ではありません。callvirt
仮想メソッドを呼び出すためのものです(メソッドthis
をオーバーライドするサブクラスの場合、代わりにサブクラスバージョンが呼び出されます)。
このため、.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 の場合、コンパイラは呼び出しを発行するように見えます。
- メソッド呼び出しはオブジェクト作成直後
- メソッドは基本クラスに実装されていません
MSDNによると:
電話:
呼び出し命令は、命令とともに渡されたメソッド記述子によって示されるメソッドを呼び出します。メソッド記述子は、呼び出すメソッドを示すメタデータトークンです...メタデータトークンは、呼び出しが静的メソッド、インスタンスメソッド、仮想メソッド、またはグローバル関数のいずれであるかを判断するのに十分な情報を伝達します。これらすべての場合において、宛先アドレスはメソッド記述子から完全に決定されます(これを、仮想メソッドを呼び出すためのCallvirt命令と比較してください。宛先アドレスは、Callvirtの前にプッシュされたインスタンス参照の実行時タイプにも依存します)。
callvirt命令は、オブジェクトのレイトバウンドメソッドを呼び出します。つまり、メソッドは、メソッドポインタに表示されるコンパイル時クラスではなく、実行時型のobjに基づいて選択されます。Callvirtは、仮想メソッドとインスタンスメソッドの両方を呼び出すために使用できます。
したがって、基本的に、オーバーライドされているかどうかに関係なく、オブジェクトのインスタンスメソッドを呼び出すためにさまざまなルートが使用されます。
呼び出し:変数->変数の型オブジェクト->メソッド
CallVirt:変数->オブジェクトインスタンス->オブジェクトのタイプオブジェクト->メソッド