I came across the castclass
opcode, which is defined at Standard ECMA - 335, III.4.3
. I wrote several examples of usage callvirt
opcode with casting and without. It turns out, that castclass
opcode has big impact on performance.
For testing, I used following "rough" (in terms of inexact timing of methods' execution) program (compiled in Release mode
by msvc 2015
):
public class Program
{
public interface IShow { string show(); }
public class ObjectWithShow : IShow
{
public string show() => "Hello, that's the show method.";
}
// Such functions remains unchanged
public static string showWithCast(object o) => ((IShow)o).show();
public static string show(IShow o) => o.show();
// Such function will be patched later
public static string showWithoutCast(object o) => ((IShow)o).show();
public static void Main()
{
int N = 10000000;
var show_object = new ObjectWithShow();
{
var watch = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < N; ++i)
{
showWithCast(show_object);
}
watch.Stop();
Console.WriteLine($"With cast {watch.ElapsedMilliseconds}");
}
{
var watch = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < N; ++i)
{
showWithoutCast(show_object);
}
watch.Stop();
Console.WriteLine($"Without cast {watch.ElapsedMilliseconds}");
}
{
var watch = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < N; ++i)
{
show(show_object);
}
watch.Stop();
Console.WriteLine($"Without cast {watch.ElapsedMilliseconds}");
}
}
}
Here is IL
codes of show/showWitCast
functions:
.method public hidebysig static string show (class IShow o) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: callvirt instance string IShow::show()
IL_0006: ret
} // end of method Program::show
.method public hidebysig static string showWithCast (object o) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: castclass IShow
IL_0006: callvirt instance string IShow::show()
IL_000b: ret
} // end of method Program::showWithCast
Here is code for showWithoutCast
(NOTE: I patched it by removing castclass IShow
in IL
editor. Original version was the same as showWithCast
)
.method public hidebysig static string showWithoutCast (object o) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: callvirt instance string IShow::show()
IL_0006: ret
} // end of method Program::showWithoutCast
Results of execution (i7-3370 [email protected], 8GB RAM) show following result:
With cast 46
Without cast 24
Without cast 23
It turns out that callvirt
on object without castclass
shows almost the same result as we used interface instance instead. So, what's the purpose of castclass
? I guess that c# compiler
emits such code to ensure, that callvirt
opcode will not be used on incorrent types (because it cannot perform such checks in compile time). So, following question - Is it consistent CIL
code, where I intentionally remove usage of castclass
in places, where I GUARANTEE that method will be used only types which implements IShow
?
P.S. Of course, You can ask, maybe show
method should be used then? But there is cases, where such function cannot be used. If to be brief, I dynamically generate code and I want to implement generic container (it inherits IShow
), but its generic parameter can optionally implement interface IShow
. If generic parameter does not implement interface (for example it is int
) then I guarantee that the method show
of container will not be used.
All of the callvirt instance string IShow::show
instructions call the same exact stub, which jumps to a lookup stub associated with the interface method. The lookup stub will resolve the method to be called depending on the type of the object that receives the call. In this case, the object does implement IShow
, so everything works out fine as you can see. However, if you pass an object that does not implement IShow
, the lookup stub will not find IShow::show
in the object's method table and so an exception of type EntryPointNotFoundException
is thrown.
The evaluation stack of the IL virtual machine contains an object of type object
at the time the callvirt
instruction is executed. The target method is IShow::show()
. According to the CLI specification Section III.4.2, the type object
needs to be verifier-assignable-to IShow
in order for the IL code to be verifiable. castclass
makes the code verifiable. In this case, since the code is fully trusted, verification is automatically skipped and so a verification exception is not thrown and method gets JIT compiled and executed.
Technically, in this case, showWithoutCast
does not contain any IL instruction that should cause an exception of type EntryPointNotFoundException
to be thrown as per the specification. However, since the code is not verifiable, Section II.3 of the standard states that the behavior is unspecified in case of a verification failure. That is, the implementation is not required to document which behavior occurs. On the other hand, the behavior of verifiable code is specified and castclass
makes verification succeeds.
Note that when you build the IL code locally on your machine and run it, it will be automatically considered as fully trusted. So the JIT compiler will not verify any method.