问题

C#委托的性能开销具体在哪里,有哪些使用指导?

回答
委托,这个C中的基石之一,它的强大之处在于能够将方法像变量一样传递和调用。但凡事皆有代价,委托也不例外。理解它的性能开销,以及如何在实践中规避这些开销,是写出更高效C代码的关键。

我们先来拆解委托的“内功心法”,看看它到底做了什么,以及在这个过程中可能产生的“损耗”。

委托的幕后:方法调用的“代理人”

本质上,委托是一种类型安全的函数指针。当你在一个地方声明一个委托类型,然后在另一个地方创建这个委托的一个实例,并将其指向某个方法时,你实际上是在创建一个“代理人”。这个代理人知道具体要调用哪个方法,以及如何调用它。

这个“代理人”的创建过程,以及后续的调用过程,就是产生性能开销的源头。

1. 委托实例的创建(装箱的幽灵,虽然不总是):
当你创建一个委托实例时,CLR(.NET Common Language Runtime)需要做一些事情。如果委托指向的是一个实例方法(非静态方法),CLR 需要将 `this` 引用(也就是对象本身)打包进委托的内部结构中。这个打包的过程,在某些情况下,可能会涉及装箱(Boxing)。

装箱是什么? 想象一下,你有一个值类型(比如 `int` 或 `struct`)。如果你想把它当作一个对象(`object`)来处理,CLR 就需要为它在托管堆上分配一块内存,并将值类型的内容复制过去,然后返回一个指向这块内存的引用。这就是装箱。
为何有时会发生? 委托指向实例方法时,需要知道是哪个对象的方法。这个“哪个对象”的信息,通常是以引用类型(`object`)的形式存储在委托实例中的。如果委托指向一个值类型的方法,那么 `this` 引用就需要被装箱成 `object`。
性能影响: 装箱操作涉及到堆内存分配和数据复制,这比直接在栈上操作值类型要慢得多,并且会增加垃圾回收的压力。好消息是,如果委托指向的是一个引用类型的方法,或者指向一个静态方法,通常就不会发生装箱。

2. 委托的调用(方法调用的“间接跳转”):
当你调用一个委托实例时,CLR 并不是直接跳到目标方法执行。它需要通过委托实例内部的指针,找到实际要调用的方法。这个过程涉及到:

虚方法查找(Delegate invocation): 委托的调用机制和虚方法调用有些类似,它需要查找目标方法的具体地址。这个查找过程虽然经过高度优化,但终究是比直接方法调用多了一个查找的步骤。
链式调用(Multicast Delegates): 当一个委托指向多个方法(通过 `+` 操作符组合),调用委托时,CLR 需要遍历委托链中的每一个目标方法,依次执行。这无疑增加了调用的次数和开销。

3. 垃圾回收(GC)的负担:
前面提到的委托实例创建,尤其是涉及装箱时,会在托管堆上分配对象。这些对象也需要被垃圾回收器(GC)管理。频繁地创建和丢弃大量委托实例,会给GC带来额外的压力,导致GC暂停应用程序执行,影响响应速度。

我们该如何“驯服”委托的性能影响?

理解了开销的来源,我们就能对症下药。以下是一些关键的使用指导,让你能更优雅、更高效地使用委托:

避免委托指向值类型的方法(除非必要):
这是最容易引起装箱的场景。如果你发现一个委托实例需要指向一个 `struct` 的方法,仔细思考一下是否有替代方案。例如,是否可以重构 `struct`,让其方法变成静态方法,或者将 `struct` 包装在一个类中。如果确实需要,并且性能敏感,要意识到这里可能存在潜在的开销。

优先使用静态方法:
静态方法不与任何特定对象实例关联,因此创建指向静态方法的委托时,不会涉及到 `this` 引用的装箱,也不会有实例方法查找的复杂性。这通常是最轻量级的委托使用方式。

小心链式委托(Multicast Delegates)的过度使用:
虽然链式委托在事件处理等场景下非常有用,但如果一个委托链变得非常长,或者频繁地动态添加/移除委托项,每次调用委托都会遍历整个链,累积起来的开销是不可忽视的。如果你的应用场景需要非常大量的、动态的、独立的“回调”,考虑使用更适合的数据结构(如 `List`)来管理,并在需要时手动遍历调用,这可能比一个超长的委托链更具可控性。

委托和 Lambda 表达式的权衡:
Lambda 表达式常常被用来简洁地创建委托。很多时候,Lambda 表达式的性能与直接创建委托实例相当。然而,Lambda 表达式如果捕获了外部变量(closure),也可能导致这些变量被装箱到堆上,产生额外的开销。
捕获值类型: Lambda 捕获值类型变量时,这些变量会被装箱到堆上。
捕获引用类型: Lambda 捕获引用类型变量时,通常只是引用被捕获,对象本身不会被移动,开销相对较小。
因此,如果你的 Lambda 表达式捕获了大量值类型变量,也要留心潜在的装箱开销。

考虑 `Action`/`Func` 等泛型委托:
.NET 提供了 `Action` 和 `Func` 系列的泛型委托。它们是预定义的委托类型,非常通用。使用它们可以避免你自行声明委托类型,减少代码量,并且由于它们通常是针对非值类型的场景优化的,在很多情况下可以更好地控制性能。

性能分析工具是你的好朋友:
当你怀疑委托是性能瓶颈时,不要凭空猜测。使用 Visual Studio 的性能分析器(Profiler)或 dotTrace 等工具,可以精确地定位到委托的创建和调用的开销,了解实际发生的装箱情况,以及方法调用的具体耗时。

Delegate.CreateDelegate 和 MethodInfo.Invoke 的对比:
在某些高级场景下,你可能会直接使用 `Delegate.CreateDelegate` 来创建委托,或者使用 `MethodInfo.Invoke` 来调用方法。`Delegate.CreateDelegate` 的创建开销通常比直接实例化委托要高一些,因为它需要通过反射来查找和绑定方法。而 `MethodInfo.Invoke` 则是一个纯粹的反射调用,其性能开销远高于委托调用,因为它涉及到方法查找、参数打包/解包、类型检查等一系列操作。委托调用是对 `MethodInfo.Invoke` 的一种性能优化。

总结一下,委托的性能开销主要体现在:

实例创建时的潜在装箱: 尤其是指向值类型实例方法的委托。
调用时的间接跳转: 相较于直接方法调用,多了一个查找目标方法的步骤。
链式委托的累积调用开销: 委托链越长,调用开销越大。
垃圾回收的压力: 委托实例(特别是装箱后的)需要GC管理。

而优化的关键在于:

尽量保持委托指向静态方法。
谨慎处理委托指向值类型实例的方法。
审慎使用长链式的多播委托。
理解 Lambda 捕获变量的潜在装箱。
善用预定义的泛型委托。
利用性能分析工具进行实证分析。

委托是C中强大而灵活的工具,掌握其性能特性,并能在需要时进行优化,是成为一名优秀C开发者的必经之路。并非所有委托调用都需要极致优化,只有在性能关键路径上,才需要我们对这些细微的开销格外关注。

网友意见

user avatar

会实际去做测试是一件很好的事情,但是带着先入为主的观点去做测试就很容易忽略真正的问题


从测试结果中最显而易见的事情应该是:

创建对象是一个成本比较高的事情。



这个代码是每次都新建一个delegate对象的,循环中平均每次耗时0.01157us。
然后将InRange的赋值语句移动到循环外边,循环中每次只是调用委托,平均耗时0.005406us。
最后去掉委托,直接调用函数,平均耗时0.002895us

每次新建一个delegate对象所花费的时间是不创建对象所花费的时间的一倍。



如果你进行更多的实验,你会得到更明确的结论:

大量创建托管对象是一件比较损耗性能的事情。


所以,这个实验告诉我们的结论应当是:

应尽量避免大量的创建托管对象。


和委托的关系并不大


因为在这个实验中很显然通过委托调用带来的性能损失,还不如创建委托对象所带来的性能损失。

类似的话题

  • 回答
    委托,这个C中的基石之一,它的强大之处在于能够将方法像变量一样传递和调用。但凡事皆有代价,委托也不例外。理解它的性能开销,以及如何在实践中规避这些开销,是写出更高效C代码的关键。我们先来拆解委托的“内功心法”,看看它到底做了什么,以及在这个过程中可能产生的“损耗”。委托的幕后:方法调用的“代理人”本.............
  • 回答
    在C中,匿名委托(Lambda表达式)本身并不能直接“获取自身的方法”,因为匿名委托不是一个具有独立名称和实体的方法,它更像是一个嵌入到代码中的一段行为。但是,如果你想在匿名委托的执行过程中,引用并调用定义该匿名委托的代码块中的某个已经存在的方法,那倒是可以实现的。我们来拆解一下这个问题,看看实际能.............
  • 回答
    在 C++ 中,循环内部定义与外部同名变量不报错,是因为 作用域(Scope) 的概念。C++ 的作用域规则规定了变量的可见性和生命周期。我们来详细解释一下这个过程:1. 作用域的定义作用域是指一个标识符(变量名、函数名等)在程序中可以被识别和使用的区域。C++ 中的作用域主要有以下几种: 文件.............
  • 回答
    C 语言的设计理念是简洁、高效、接近硬件,而其对数组的设计也遵循了这一理念。从现代编程语言的角度来看,C 语言的数组确实存在一些“不改进”的地方,但这些“不改进”很大程度上是为了保持其核心特性的兼容性和效率。下面我将详细阐述 C 语言为何不“改进”数组,以及这种设计背后的权衡和原因:1. 数组在 C.............
  • 回答
    C 语言王者归来,原因何在?C 语言,这个在编程界已经沉浮数十载的老将,似乎并没有随着时间的推移而消逝,反而以一种“王者归来”的姿态,在许多领域焕发新生。它的生命力如此顽强,甚至在 Python、Java、Go 等语言层出不穷的今天,依然占据着不可动摇的地位。那么,C 语言究竟为何能实现“王者归来”.............
  • 回答
    C罗拒绝同框让可口可乐市值下跌 40 亿美元,可口可乐回应「每个人都有不同的口味和需求」,这件事可以说是近几年体育界和商业界结合的一个典型案例,也引发了很多的讨论和思考。我们来详细地分析一下:事件本身: 核心行为: 在2021年欧洲杯小组赛葡萄牙对阵匈牙利的赛前新闻发布会上,葡萄牙球星克里斯蒂亚.............
  • 回答
    C++20 的协程(coroutines)和 Go 的 goroutines 都是用于实现并发和异步编程的强大工具,但它们的设计理念、工作方式以及适用的场景有显著的区别。简单地说,C++20 协程虽然强大且灵活,但与 Go 的 goroutines 在“易用性”和“轻量级”方面存在较大差距,不能完全.............
  • 回答
    在 C++ 中,为基类添加 `virtual` 关键字到析构函数是一个非常重要且普遍的实践,尤其是在涉及多态(polymorphism)的场景下。这背后有着深刻的内存管理和对象生命周期管理的原理。核心问题:为什么需要虚析构函数?当你在 C++ 中使用指针指向一个派生类对象,而这个指针的类型是基类指针.............
  • 回答
    在 C/C++ 中,采用清晰的命名规则是编写可维护、易于理解和协作代码的关键。一个好的命名规范能够让其他开发者(包括未来的你)快速理解代码的意图、作用域和类型,从而提高开发效率,减少 Bug。下面我将详细阐述 C/C++ 中推荐的命名规则,并提供详细的解释和示例。核心原则:在深入具体规则之前,理解这.............
  • 回答
    C++之所以没有被淘汰,尽管其被普遍认为“复杂”,其原因绝非单一,而是由一系列深刻的历史、技术和生态系统因素共同作用的结果。理解这一点,需要深入剖析C++的定位、优势、以及它所代表的工程哲学。以下是详细的解释: 1. 历史的沉淀与根基的稳固 诞生于C的土壤: C++并非凭空出现,它是对C语言的强.............
  • 回答
    C++ 模板:功能强大的工具还是荒谬拙劣的小伎俩?C++ 模板无疑是 C++ 语言中最具争议但也最引人注目的一项特性。它既能被誉为“代码生成器”、“通用编程”的基石,又可能被指责为“编译时地狱”、“难以理解”的“魔法”。究竟 C++ 模板是功能强大的工具,还是荒谬拙劣的小伎俩?这需要我们深入剖析它的.............
  • 回答
    C 语言本身并不能直接“编译出一个不需要操作系统的程序”,因为它需要一个运行环境。更准确地说,C 语言本身是一种编译型语言,它将源代码转换为机器码,而机器码的执行是依赖于硬件的。然而,当人们说“不需要操作系统的程序”时,通常指的是以下几种情况,而 C 语言可以用来实现它们:1. 嵌入式系统中的裸机.............
  • 回答
    C++ 中实现接口与分离(通常是通过抽象类、纯虚函数以及对应的具体类)后,确实会增加文件的数量,这可能会让人觉得“麻烦”。但这种增加的文件数量背后,隐藏着巨大的好处,使得代码更加健壮、灵活、可维护和可扩展。下面我将详细阐述这些好处:核心思想:解耦 (Decoupling)接口与实现分离的核心思想是解.............
  • 回答
    C++ 是一门强大而灵活的编程语言,它继承了 C 语言的高效和底层控制能力,同时引入了面向对象、泛型编程等高级特性,使其在各种领域都得到了广泛应用。下面我将尽可能详细地阐述 C++ 的主要优势: C++ 的核心优势:1. 高性能和底层控制能力 (Performance and LowLevel C.............
  • 回答
    C语言指针是否难,以及数学大V认为指针比范畴论还难的说法,是一个非常有趣且值得深入探讨的话题。下面我将尽量详细地阐述我的看法。 C语言指针:理解的“门槛”与“终点”首先,我们需要明确“难”的定义。在编程领域,“难”通常指的是: 学习曲线陡峭: 需要花费大量时间和精力去理解和掌握。 容易出错:.............
  • 回答
    在 C/C++ 中,指针声明的写法确实存在两种常见的形式:`int ptr;` 和 `int ptr;`。虽然它们最终都声明了一个指向 `int` 类型的指针变量 `ptr`,但它们在语法上的侧重点和历史演变上有所不同,导致了后者(`int ptr;`)更为普遍和被推荐。下面我将详细解释为什么通常写.............
  • 回答
    C++ 的核心以及“精通”的程度,这是一个非常值得深入探讨的话题。让我尽量详细地为您解答。 C++ 的核心究竟是什么?C++ 的核心是一个多层次的概念,可以从不同的角度来理解。我将尝试从以下几个方面来阐述:1. 语言设计的哲学与目标: C 的超集与面向对象扩展: C++ 最初的目标是成为 C 语.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    在 C/C++ 编程中,确实存在一些写法,它们本身可能不是最优的解决方案,甚至在大多数情况下是多余的,但却能让有一定经验的开发者眼前一亮,感到“不明觉厉”。这些写法往往巧妙地利用了语言的特性、预处理指令、或者是一些不常用的语法糖。同时,它们又不会像一些“炫技”般的操作那样显得过于怪异而难以理解。下面.............
  • 回答
    在C/C++中,当您声明一个 `int a = 15;` 这样的局部变量时,它通常存储在 栈 (Stack) 上。下面我们来详细解释一下,并涉及一些相关的概念:1. 变量的生命周期与存储区域在C/C++中,变量的存储位置取决于它们的生命周期和作用域。主要有以下几个存储区域: 栈 (Stack):.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有