首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

方法的三种调用形式

编程知识
2024年08月20日 07:46

在《可以调用Null的实例方法吗?》一文中,我谈到.NET方法的三种调用形式,现在我们就来着重聊聊这个话题。具体来说,这里所谓的三种方法调用形式对应着三种IL指令:Call、CallVirt和Calli。

一、三个方法调用指令
二、三种方法调用形式
三、虚方法的分发(virtual dispatch)
四、性能差异

一、三个方法调用指令

虽然C#的方法具有静态方法和实例方法之分,但是在IL层面,它们之间并没有什么不同,就是单纯的“函数”而已,而且这个函数的第一个参数的类型永远是方法所在的类型。所以在IL层面,方法总是“静态”的,调用实例方法的本质就是将目标实例作为第一个参数,对于静态方法,第一个参数永远是Null/Default(值类型)。我在《实例方法和静态方法有区别吗?》中曾经着重谈到过这个问题。

Call和CallVirt指令执行方法的流程只有两步:将所有参数压入栈中 + 执行方法。它们之间的不同之处在于:Call指令编译时就已经确定了执行的方法,而CallVirt则是在运行时根据作为第一个参数的实例类型决定最终执行的方法。Calli指令则有所不同,我们执行该指令时需要指定目标方法的指针,整个流程包括三步:将所有参数压入栈中 + 将目标方法指针压入栈中+执行方法

二、三种方法调用形式

接下来我们使用动态方法的形式演示上述三种方法调用指令的使用。具体来说,我们采用三种方式调用定义在Calculator中用来进行加法运算的Add方法,为此我们利用CreateInvoker方法根据指定的指令生成一个对应的Func<Calculator, int, int, int>委托。在CreateInvoker方法中,我们创建一个与Func<Calculator, int, int, int>委托匹配的动态方法。在IL Emit过程中,我们先将三个参数(Calculator对象和Add方法的参数a和b)压入栈中。如果指定的是Call和CallVirt指令,我们直接执行它们就可以了。如果指定的是Calli指令,我们得执行Ldftn指令将Add方法的指针压入栈中(方法指针通过指定的MethodInfo对象提供),然后再执行Calli指令。

var calculator = new Calculator();

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

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

invoker = CreateInvoker(OpCodes.Calli);
Console.WriteLine($"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 = dynamicMethod.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Ldarg_2);

    if (opcode == OpCodes.Call)
    {
        il.Emit(OpCodes.Call, method);
    }
    else if (opcode == OpCodes.Callvirt)
    {
        il.Emit(OpCodes.Callvirt, method);
    }
    else if (opcode == OpCodes.Calli)
    {
        il.Emit(OpCodes.Ldftn, method);
        il.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(int), [typeof(Calculator), typeof(int), typeof(int)]);
    }

    il.Emit(OpCodes.Ret);
    return (Func<Calculator, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<Calculator, int, int, int>));
}

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

演示程序利用指定的三种方法指令创建了对应的Func<Calculator, int, int, int>,然后指定相同的参数(Calculator实例、整数1、2)执行它们,我们最终会在控制台上得到如下的输出结果。

image

三、虚方法的分发(virtual dispatch)

虽然Calculator的Add是个虚方法,由于Call指令执行的目标方法在编译时就确定,Calli则是我们以指针的形式指定了执行的方法,不论我们指定的目标对象具体是何类型,执行的永远是定义在Calculator类型的那个Add方法。面向对象“多态”的能力只能通过CallVirt指令来实现。

var calculator = new FakeCalculator();

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

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

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


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

以如上的程序为例,我们定义了Calculator的派生类FakeCalculator,在重写的Add方法中执行“减法运算”。我们将这个FakeCalculator对象作为参数调用三个委托,会得到如下所示的输出结果,可以看出CallVirt指令才能得到我们希望的结果。
image

四、性能差异

既然Call、CallVirt和Calli都是能帮助我们完成方法的执行,我们自然会进一步关系它们的性能差异了,为此我们来做一个简单的性能测试。

BenchmarkRunner.Run<Test>();

public class Test
{
    private static readonly Func<Calculator, int, int, int> _call = CreateInvoker(OpCodes.Call);
    private static readonly Func<Calculator, int, int, int> _callvirt = CreateInvoker(OpCodes.Callvirt);
    private static readonly Func<Calculator, int, int, int> _calli = CreateInvoker(OpCodes.Calli);
    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);
}

如上所示的测试程序很简单,我们调用CreateInvoker方法将针对三种指令的Func<Calculator, int, int, int>委托和目标对象FakeCalculator创建出来,并在三个Benchmark方法中执行它们。从如下的测试结果可以看出,Call由于不需要进行”虚方法分发(Virtual Dispatch)”性能会比Callvirt执行好一些,但总体来说差别不大,但是Calli指令调用方法的性能会差很多
image

From:https://www.cnblogs.com/artech/p/18363117/method-invocation-dotnet
本文地址: http://www.shuzixingkong.net/article/1252
0评论
提交 加载更多评论
其他文章 Antd-React-TreeSelect前端搜索过滤
Antd-React-TreeSelect前端搜索过滤,antd本事是带有搜索的功能,但是在开发过程中发现自带的搜索功能与我们要使用的搜索过滤还是差了好多,在一些时候搜索为了迎合需要不得不这么操作,那么该操作结合了antd官方的搜索操作,因而在看了网上的一些操作后还是与需求不符合,最后实在没有解决办
Antd-React-TreeSelect前端搜索过滤 Antd-React-TreeSelect前端搜索过滤 Antd-React-TreeSelect前端搜索过滤
5 个有趣的 Python 开源项目「GitHub 热点速览」
本期,我从上周的开源热搜项目中精心挑选了 5 个有趣、好玩的 Python 开源项目。 首先是 PyScript,它可以让你直接在浏览器中运行 Python 代码,不仅支持在 HTML 中嵌入,还能安装第三方库。然后是用 Python 写的“魔法虫洞” magic-wormhole,这是一个无需服务
5 个有趣的 Python 开源项目「GitHub 热点速览」 5 个有趣的 Python 开源项目「GitHub 热点速览」 5 个有趣的 Python 开源项目「GitHub 热点速览」
C#/.NET/.NET Core技术前沿周刊 | 第 1 期(2024年8.12-8.18)
前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录、追踪C#/.NET/.NET Core领域、生态的每周最新、最实用的技术文章、社区动态、优质项目和学习资源等。让你时刻站在技术前沿,助力技术成长与视野拓宽。 欢迎投稿,推荐或自荐文章/项目/学习资源等。 &#128240
C#/.NET/.NET Core技术前沿周刊 | 第 1 期(2024年8.12-8.18)
Viper:强大的Go配置解析库
1 介绍 Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。目前Star 26.6k, 它支持以下特性: 设置默认值 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息
Viper:强大的Go配置解析库 Viper:强大的Go配置解析库
DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024
Differentiable Model Scaling(DMS)以直接、完全可微的方式对宽度和深度进行建模,是一种高效且多功能的模型缩放方法。与先前的NAS方法相比具有三个优点:1)DMS在搜索方面效率高,易于使用。2)DMS实现了高性能,可与SOTA NAS方法相媲美。3)DMS是通用的,与各种
DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024 DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024 DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024
使用CyFES对配体运动轨迹进行数据透视
分子动力学模拟是一个以时间换空间的方法,那么在时间尺度上留下轨迹之后,如何把轨迹做一个静态的展现,正是数据透视所解决的问题。CyFES是一个开源的、基于GPU硬件加速的数据透视高性能计算工具,我们通过一个蛋白-配体相互作用的运动轨迹的示例,演示一下CyFES的基本使用方法。
使用CyFES对配体运动轨迹进行数据透视 使用CyFES对配体运动轨迹进行数据透视 使用CyFES对配体运动轨迹进行数据透视
线上问题排查——接口长时间未响应
刚看到鱼皮的文章,一下午连续故障两次,谁把我们接口堵死了?!,想起之前刚进公司时遇到了一个类似问题 线上接口访问不通,超时等待,但是看后台日志是正常运行的,进服务器看监控,CPU 占用100%,经典面试题了 使用jsp -l 和 jstack &lt;进程PID&gt; &gt; stack.txt
线上问题排查——接口长时间未响应
RISC-V全志D1多媒体套件文章汇总
提示 此开发板的任何问题都可以在我们的论坛交流讨论&#160;https://forums.100ask.net/c/aw/d1/57 文章目录汇总 教程共计14章,下面是章节汇总: 第0章_RISC-V全志D1多媒体套件 第1章_快速启动 1_1 快速开始使用 1_2 学习路线 第2章_安装并配置