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

推荐订阅源

C
Cisco Blogs
Schneier on Security
Schneier on Security
T
Tor Project blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
NISL@THU
NISL@THU
L
Lohrmann on Cybersecurity
Scott Helme
Scott Helme
Webroot Blog
Webroot Blog
Project Zero
Project Zero
Google Online Security Blog
Google Online Security Blog
The Last Watchdog
The Last Watchdog
Spread Privacy
Spread Privacy
Hacker News: Ask HN
Hacker News: Ask HN
PCI Perspectives
PCI Perspectives
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
W
WeLiveSecurity
Attack and Defense Labs
Attack and Defense Labs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News | PayPal Newsroom
Help Net Security
Help Net Security
The Hacker News
The Hacker News
H
Heimdal Security Blog
O
OpenAI News
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
Cyberwarzone
Cyberwarzone
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 叶小钗
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
A
Arctic Wolf
I
Intezer
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security Affairs
P
Proofpoint News Feed
S
Secure Thoughts
腾讯CDC
Google DeepMind News
Google DeepMind News
量子位
罗磊的独立博客

博客园 - 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]: 定制应用模型 ASP.NET Core MVC应用模型的构建[1]: 应用的蓝图 基于HTTP2/3的流模式消息交换如何实现? 编写高效的代码,你应该了解Array、Memory、ReadOnlySequence . . . WebAssembly核心编程[4]: Memory WebAssembly核心编程[3]: Module 与 Instance WebAssembly核心编程[2]:类型系统 WebAssembly核心编程[1]:wasm模块实例化的N种方式 WebAssembly入门笔记[4]:利用Global传递全局变量 WebAssembly入门笔记[3]:利用Table传递引用
方法的三种调用形式
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