Location>code7788 >text

Three forms of method invocation

Popularity:814 ℃/2024-08-20 08:46:54

In theCan I call an instance method of Null?In my article, I talked about the three forms of calling .NET methods, and now we'll focus on that topic. Specifically, the three types of method calls mentioned here correspond to the three IL instructions: Call, CallVirt, and Calli.

I. Three method invocation instructions
II. Three forms of method invocation
III. Distribution of virtual methods (virtual dispatch)
IV. Performance differences

I. Three method invocation instructions

Although C# has static and instance methods, at the IL level there is no difference between them, they are simply "functions", and the first parameter of the function is always of the type of the method. So at the IL level, methods are always "static", calling an instance method is essentially taking the target instance as the first parameter, and for static methods, the first parameter is always Null/Default (the value type). I wrote about this inIs there a difference between instance methods and static methods?This issue was highlighted in the

The Call and CallVirt directives execute methods in a two-step process:Press all parameters onto the stack + execute the methodThe difference between them is that the Call directive determines the method to be executed at compile time, whereas CallVirt determines the final method to be executed at runtime based on the type of the instance that is the first argument. The difference between them is that the Call directive compiles with the method to be executed, whereas CallVirt determines the final method to be executed at runtime based on the type of the instance that is the first parameter.The Calli directive is different in the sense that we need to specify the pointer to the target method when executing this directive, and the whole process consists of three steps:Press all parameters onto the stack + Press target method pointer onto the stack + Execute method

II. Three forms of method invocation

Next, we demonstrate the use of the above three methods to invoke instructions in the form of dynamic methods. Specifically, we use three ways to call the Add method defined in Calculator to perform addition, for which we use the CreateInvoker method to generate a corresponding Func<Calculator, based on the specified instruction.int, int, int> Delegation. In the CreateInvoker method, we create a delegate that is the same as Func<Calculator, theint, int, int> Delegate matching dynamic methods. During IL Emit, we first press the three parameters (the Calculator object and the Add method's parameters a and b) onto the stack. If the Call and CallVirt directives are specified, we execute them directly. If the Calli instruction is specified, we have to execute theLdftninstruction presses the pointer to the Add method onto the stack (the method pointer is provided via the specified MethodInfo object) and then executes the Calli instruction.

var calculator = new Calculator();

var invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Call]");

invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]");

invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]");

static Func<Calculator, int, int, int> CreateInvoker(OpCode opcode)
{
    var method = typeof(Calculator).GetMethod("Add")!;
    var dynamicMethod = new DynamicMethod("Add", typeof(int), [typeof(Calculator), typeof(int), typeof(int)]);
    var il = ();
    (OpCodes.Ldarg_0);
    (OpCodes.Ldarg_1);
    (OpCodes.Ldarg_2);

    if (opcode == )
    {
        (, method);
    }
    else if (opcode == )
    {
        (, method);
    }
    else if (opcode == )
    {
        (, method);
        (, , typeof(int), [typeof(Calculator), typeof(int), typeof(int)]);
    }

    ();
    return (Func<Calculator, int, int, int>)(typeof(Func<Calculator, int, int, int>));
}

public class Calculator
{
    public virtual int Add(int a, int b) => a + b;
}

The demo program creates the corresponding Func<Calculator, using the three method directives specified.int, int, int>, then specify the same parameters (Calculator instance, integers 1, 2) to execute them, and we end up with the following output on the console.

III. Distribution of virtual methods (virtual dispatch)

Although Calculator's Add is a dummy method, since the target method executed by the Call instruction is determined at compile time, Calli is the method we specify to be executed in the form of a pointer, regardless of the specific type of the target object we specify, the execution will always be the Add method defined in the Calculator type. Object-Oriented"polymorphic"The ability to do this can only be achieved through the CallVirt command.

var calculator = new FakeCalculator();

var invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Call]");

invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]");

invoker = CreateInvoker();
($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]");


public class FakeCalculator : Calculator
{
    public override int Add(int a, int b) => a - b;
}

Take the above program as an example, we define a Calculator derived class FakeCalculator, and perform the "subtraction operation" in the overridden Add method. If we call the three delegates with this FakeCalculator object as parameter, we will get the output as shown below, which shows that the CallVirt instruction is the only way to get the result we want.

IV. Performance differences

Since Call, CallVirt and Calli all help us with method execution, it's only natural that we would further relate their performance differences, and for that reason let's do a simple performance test.

<Test>();

public class Test
{
    private static readonly Func<Calculator, int, int, int> _call = CreateInvoker();
    private static readonly Func<Calculator, int, int, int> _callvirt = CreateInvoker();
    private static readonly Func<Calculator, int, int, int> _calli = CreateInvoker();
    private static readonly Calculator _calculator = new FakeCalculator();

    [Benchmark]
    public int Call() => _call(_calculator, 1, 2);

    [Benchmark]
    public int Callvirt() => _callvirt(_calculator, 1, 2);

    [Benchmark]
    public int Calli() => _calli(_calculator, 1, 2);
}

The test program as shown above is simple, we call the CreateInvoker method to place the Func<Calculator, targeting the three instructions.int, int, intThe delegate and target object FakeCalculator are created and executed in the three Benchmark methods. As you can see from the following test results, Call does not need to do "Virtual Dispatch" performance than Callvirt execution is a little better, but in general the difference is not significant, howeverThe performance of Calli directives calling methods is much worse