惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
Hacker News: Ask HN
Hacker News: Ask HN
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
MyScale Blog
MyScale Blog
月光博客
月光博客
W
WeLiveSecurity
T
Threat Research - Cisco Blogs
Martin Fowler
Martin Fowler
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
The GitHub Blog
The GitHub Blog
Webroot Blog
Webroot Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
F
Full Disclosure
U
Unit 42
Jina AI
Jina AI
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
腾讯CDC
T
Threatpost
H
Hacker News: Front Page
P
Palo Alto Networks Blog
博客园 - 聂微东
Last Week in AI
Last Week in AI
有赞技术团队
有赞技术团队
Help Net Security
Help Net Security
L
LINUX DO - 热门话题
N
News and Events Feed by Topic
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy

博客园 - Artech

[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案 [MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF [MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现 [MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力 [MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控 [MAF预定义ChatClient中间件-07]PerServiceCallChatHistoryPersistingChatClient——基于ReAct循环的一步一存档 [MAF预定义ChatClient中间件-06]利用ImageGeneratingChatClient开发专业图片生成Agent [MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息 [MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义 [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间 [MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣 [MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志 [MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 - Artech [MAF的Agent管道详解-05]对话历史的持久化和输入输出的增强 [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据? [MAF的Agent管道详解-03]连接LLM的IChatClient对象 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 [对比学习LangChain和MAF-04]针对消息的设计 [对比学习LangChain和MAF-03]完全不同的Agent设计哲学 [对比学习LangChain和MAF-02]基本编程模式的差异(下篇) [对比学习LangChain和MAF-01]基本编程模式的差异(上篇) 我所理解的Python元模型 除了按值和引用,方法参数的第三种传递方式 可以调用Null的实例方法吗? 自定义Key类型的字典无法序列化的N种解决方案 为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate 深入解析ASP.NET Core MVC的模块化设计[下篇] 深入解析ASP.NET Core MVC的模块化设计[上篇] ASP.NET Core MVC应用模型的构建[4]: Action的选择 ASP.NET Core MVC应用模型的构建[3]: Controller的收集 ASP.NET Core MVC应用模型的构建[2]: 定制应用模型
方法的三种调用形式
Artech · 2024-08-20 · via 博客园 - Artech

在《可以调用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

当我们在使用Calli指令时, 由于我们是利用Ldfnt指令指定的定义在Calculator类型中的Add方法,所以起不到“多态”的作用。如果要实现多态,我们得按照如下得方式使用Ldvirtfnt,该指令会从提取目标对象(通过执行Ldarg_0指令压入栈中)并解析出对应得方法。

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.Ldarg_0);
        il.Emit(OpCodes.Ldvirtftn, 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>));
}

再次执行演示程序,就会得到我们希望得结果:

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