静态方法和实例方法的性能比较

147

我的问题与静态方法和实例方法的性能特征及其可扩展性有关。假设在此场景中,所有类定义都在单个程序集中,并且需要多个离散指针类型。

考虑以下情况:

public sealed class InstanceClass
{
      public int DoOperation1(string input)
      {
          // Some operation.
      }

      public int DoOperation2(string input)
      {
          // Some operation.
      }

      // … more instance methods.
}

public static class StaticClass
{
      public static int DoOperation1(string input)
      {
          // Some operation.
      }

      public static int DoOperation2(string input)
      {
          // Some operation.
      }

      // … more static methods.
}

以上类代表了一个辅助样式模式。

在实例化的类中,解决实例方法需要花费一些时间,而与 StaticClass 相反。

我的问题是:

  1. 当状态不是关注点(没有需要的字段或属性)时,始终使用静态类是否更好?

  2. 如果有大量这些静态类定义(例如说100个,每个有若干个静态方法),与同样数量的实例类定义相比,这会对执行性能或内存消耗产生负面影响吗?

  3. 当调用同一实例类中的另一个方法时,实例化解析是否仍然存在?例如,在同一实例中从 DoOperation1 中使用 [this] 关键字像 this.DoOperation2("abc") 一样。


“实例解析”是什么意思?在IL级别上,“this”指针就像任何其他本地变量一样可用。事实上,在一些旧的CLR/JIT版本中,你可以在NULL上调用一个实例方法,只要它不涉及“this” - 代码会直接执行并在没有任何东西的情况下崩溃..现在CLR/JIT在每个成员调用上都包含显式的null检查.. - quetzalcoatl
1
@quetzalcoatl 我认为他的意思是,“当一个类在自身上调用实例方法时,编译器是否会消除检查this指向某个东西的操作?” - Jon Hanna
@quetzalcoatl 是的,Jon是正确的。 - Bernie White
如果您有许多像这样的带有静态方法的“helper”类,那么我建议您更加努力地思考设计。它们可能合适,但我通常认为它们是一种不良反应或甚至是一种反模式。 - JonnyRaa
显示剩余4条评论
3个回答

195

理论上,所有其他条件相同的情况下,静态方法应该比实例方法表现略好一些,因为它多了一个隐藏的this参数。

实际上,这种差异微乎其微,会被各种编译器决策所掩盖。(因此两个人可能会“证明”一个比另一个更好,但结果却不一致)。尤其是由于this通常是通过寄存器传递的,并且通常一开始就在寄存器中。

这最后一点意味着,从理论上讲,我们应该预期,一个将对象作为参数并对其执行某些操作的静态方法,与在同一对象上的等效实例相比稍逊一筹。然而,这种差异如此微小,以至于如果你试图衡量它,你可能最终测量到的是其他编译器决策。(特别是由于该引用始终在寄存器中的可能性非常高)。

真正的性能差异将取决于您是否人为地将对象保留在内存中以执行本应自然为静态的操作,或者您正在以复杂的方式纠缠对象传递链以执行本应自然为实例的操作。

因此,在不考虑状态的情况下,对于第1点,始终最好使用静态方法,因为这就是静态方法的用途。尽管这不是性能问题,但有一个总体规则是要与编译器优化搭配良好——更可能的是,某人会投入精力来优化常规使用中出现的情况,而不是那些在奇怪的使用中出现的情况。

第2点没有任何差异。每个成员在每个类中都有一定的成本,涉及元数据的数量、实际的DLL或EXE文件中有多少代码以及将有多少JIT代码。这对于静态和实例都是相同的。

对于第3点,this就像它所做的那样。然而请注意:

  1. 在调用一个实例方法时,this 参数被传递到特定的寄存器中。如果在同一类中调用一个实例方法,它很可能已经在那个寄存器中(除非它被存储并因某种原因使用了其他寄存器),因此不需要执行任何操作来设置 this 到它应该设置的值。这也适用于例如方法的前两个参数是其调用所做的第一个和第二个参数。

  2. 由于明确了 this 不是空的,因此在某些情况下可以用来优化调用。

  3. 由于明确了 this 不是空的,因此这可能会使内联方法调用再次更加有效,因为生成用于模拟方法调用的代码可以省略一些可能需要的空检查。

  4. 尽管如此,空检查的成本很低!

  5. 值得注意的是,在作用于对象而不是实例方法的通用静态方法中,可以减少有关成本的一些讨论,详见http://joeduffyblog.com/2011/10/23/on-generics-and-some-of-the-associated-overheads/。在给定类型的情况下不调用该静态方法时,他说:“顺便说一句,扩展方法是使通用抽象更具付费性的好方法。”

    但是,请注意,这仅涉及方法使用的其他类型的实例化,而这些类型在其他情况下不存在。因此,它实际上并不适用于许多情况(使用了该类型的某些其他实例方法,在其他任何地方使用了该类型的某些其他代码)。

    摘要:

    1. 大多数情况下,实例与静态之间的性能成本可以忽略不计。
    2. 成本通常会在您滥用静态或实例之一时出现。如果您不将其作为静态和实例之间的选择的一部分,则更有可能获得正确的结果。
  6. 有时,其他类型中的静态泛型方法创建的类型数量比实例泛型方法要少,这可能会使得它在很少使用时有一些小的优势(“很少”是指应用程序生命周期内与之使用的类型,而不是调用频率)。一旦你理解了他在那篇文章中谈论的内容,你就会发现它对于大多数静态与实例方法的决策都是100%无关的。编辑:并且它基本上只有在ngen中具有成本,在jitted代码中没有。
  7. 编辑:关于空引用检查的廉价性(我在上面声称的),需要注意的是,.NET 中的大多数空引用检查根本不检查 null,而是继续执行它们原来要做的事情,并假设它能正常工作,如果出现访问异常,则将其转换为 NullReferenceException。因此,大多数情况下,由于C#代码概念上涉及空引用检查,因为它正在访问实例成员,所以如果成功,成本实际上为零。一个例外是一些内联调用(因为它们希望表现得好像调用了实例成员),它们只是访问一个字段来触发相同的行为,因此它们也非常便宜,并且通常仍然可以省略(例如,如果该方法的第一步涉及访问字段)。


你能否评论一下静态与实例问题是否对缓存一致性有任何影响?依赖于其中之一是否更容易导致缓存未命中?是否有一个好的概述来解释原因? - scriptocalypse
@scriptocalypse 并不是很明显。指令缓存不会有任何区别,在那个层面上,通过this或显式参数访问数据之间的差异并不大。更大的影响在于数据与相关数据的接近程度(值类型字段或数组值比引用类型字段中的数据更接近),以及访问模式。 - Jon Hanna
从理论上讲,我们应该预期一个以对象作为参数并对其进行某些操作的静态方法比在同一对象上等效的实例略逊一筹。-- 你的意思是如果上面的示例方法将参数作为对象而不是字符串,则非静态方法更好吗?例如:我的静态方法以对象作为参数,将其序列化为字符串并返回字符串。您是否建议在这种情况下使用非静态方法? - Emil
1
@batmaci 我的意思是 obj.DoSomehting(2) 的执行成本可能会比 DoSomething(obj, 2) 稍微便宜一些,但正如我之前所说,这种差异非常微小,并且取决于最终编译中可能有所不同的微小因素,因此根本不值得担心。如果你正在做一些相对昂贵(相对于这里所涉及的差异)的事情,比如将某些东西序列化为字符串,那么这种优化就更加毫无意义了。 - Jon Hanna
1
这个回答非常好,但有一件显而易见却很重要的事情被忽略了:实例方法需要一个实例,而创建实例并不便宜。即使是默认的 ctor 也需要初始化所有字段。一旦你已经有了一个实例,这个回答就适用了(“其他条件相等”)。当然,一个昂贵的 cctor 也会使静态方法变慢,但这只发生在第一次调用时,并且同样适用于实例方法。另请参阅 https://learn.microsoft.com/en-us/previous-versions/dotnet/articles/ms973852(v=msdn.10)?redirectedfrom=MSDN - Abel

14

当没有保留状态(不需要字段或属性)时,是否总是最好使用静态类?

我会说,是的。因为声明某些东西为static就是声明它是无状态执行的意图(虽然不是强制性的,但是这是人们期望的一种情况)。

当有大量这样的静态类(比如100个,每个都有一些静态方法)时,与同样数量的实例化类相比,会对执行性能或内存消耗产生负面影响吗?

我认为不会,除非你确信静态类是真正无状态的,否则很容易导致内存分配混乱并造成内存泄漏问题。

在同一实例类中使用[this]关键字调用另一个方法时,实例解析是否仍会发生?

我不确定这点(这是CLR的一个纯粹的实现细节),但我认为是的。


静态方法无法进行模拟,如果你进行 TDD 或者仅仅是单元测试,这将极大地影响你的测试。 - trampster
@trampster 为什么?这只是一段逻辑代码。你可以轻松地模拟你所提供的内容,以获得正确的行为。而且很多静态方法最终都会成为函数中的私有逻辑代码块。 - M. Mimpen
只要你将它留在小的私有部分,那就没问题。如果它是一个公共方法,并且你从其他地方使用它并需要在测试中更改它的功能,那么你就会陷入困境。例如文件IO、数据库访问或网络调用等,如果放在静态方法中,就会变得无法模拟,除非像你所说的那样,在静态方法中注入可模拟的依赖作为参数。 - trampster

1

静态方法速度更快,但面向对象程度较低。如果您将使用设计模式,则静态方法可能是糟糕的代码。业务逻辑最好编写为非静态方法。文件读取、WebRequest等常见函数最好作为静态方法。您的问题没有通用答案。


23
你的主张没有任何论据支持。 - ymajoros
2
这里有两个参数:1. 对于设计模式和业务逻辑,实例可能更好;2. 对于文件读取和网络请求,静态可能更好。 - rupweb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接