问题 调用空函数需要多长时间?


我有一个实现接口的项目列表。对于这个问题,让我们使用这个示例界面:

interface Person
{
  void AgeAYear();
}

有两个班级

class NormalPerson : Person
{
  int age = 0;

  void AgeAYear()
  {
    age++;
    //do some more stuff...
  }
}


class ImmortalPerson : Person
{
  void AgeAYear()
  {
    //do nothing...
  }
}

由于其他原因,我需要他们两个列表。但是对于这个电话,当我循环浏览我的列表时 Persons,我可能会调用空函数。 这会对性能产生影响吗?如果是这样,多少钱?  对于所有意图和目的,空函数是否会被优化掉?


注意:在真实的例子中, ImmortalPerson 有其他方法确实有代码 - 它不仅仅是一个什么都不做的对象。


3578
2017-08-25 13:49


起源

我认为不朽的定义是“一个永生”的存在,但不是一个不衰老的存在。 - NullUserException
调用空函数所需的时间有很多,但为了粗略的性能计算,你可以假设为1ns。 - Gabe


答案:


这会对性能产生影响吗?

极不可能有一个 富有意义的 绩效影响。

如果是这样,多少钱?

你可以量化它 究竟 使用分析器的特定代码路径。我们不能,因为我们不知道代码路径。我们可以猜测,并告诉你它几乎肯定没关系,因为这不太可能是你的应用程序的瓶颈(你真的坐在那里呼叫) Person.AgeAYear 紧紧的循环?)。

只有你可以找到 恰恰 通过取出一个分析器和测量。

对于所有意图和目的,空函数是否会被优化掉?

这肯定是可能的,但可能不会;它甚至可能在未来版本的JITter中发生变化,或者从平台变为平台(不同的平台有不同的JITter)。如果您真的想知道,请编译您的应用程序,并查看反汇编的JITted代码(而不是IL!)。

但我会这样说:这几乎可以肯定,几乎绝对不值得担心或花时间进入。除非你打电话 Person.AgeAYear 在性能关键代码的紧密循环中,它不是应用程序的瓶颈。你可以花时间在这上面,或者你可以花时间改进你的应用程序。你的时间也有机会成本。


9
2017-08-25 13:52





  • 这会对性能产生影响吗?

 也许,如果调用该函数,则调用本身将花费少量时间。

  • 如果是这样,多少钱?

您永远不会注意到任何实际应用程序的差异 - 与执行任何“真实”工作的成本相比,调用方法的成本非常小。

  • 对于所有意图和目的,空函数是否会被优化掉?

我怀疑它 - CLR 无疑 如果方法在不同的程序集中,则可能不会执行此类优化,因为该方法将来可能会更改。它 威力 是可行的,这种优化是为程序集内部的方法调用完成的,但它在很大程度上取决于代码,例如在下面的示例中:

foreach (IPerson person in people)
{
    person.AgeAYear();
}

方法调用 不能 因为不同的实现而被优化 IPerson 可能会提供实际上在这种方法中做的事情。这肯定是这样的 任何 打电话给 IPerson 接口所在的编译器无法证明它始终与a一起工作 ImmortalPerson实例。

最终你必须问自己“有什么选择?”并且“这确实有足够大的影响来保证替代方法吗?” 。在这种情况下,影响将是 非常 小 - 我会说在这种情况下以这种方式使用空方法是完全可以接受的。


3
2017-08-25 13:52



“CLR不会执行这种优化。” - 这对我来说很惊讶。我认为它可以像JVM一样内联。你知道为什么会有区别吗? - Péter Török
@Justin:我不同意你的评估,即CLR没有执行那种优化。 C#编译器没有,但我认为JITter可以并且可能(它肯定可以内联它,这似乎是内联的一个很好的候选者,然后它只是变成空体)。 (编辑:您的语句编辑对此评论有些过时。) - jason
@Justin:考虑更多,我不确定我是否同意你的澄清。我们在这里谈论JITter。 JITter将在运行时知道被调用的方法,即使程序集从上次运行时发生了变化。它具有运行时所需的信息,以了解此方法是否适合内联。 - jason
@Jason我不再那么肯定 - 我在“它不能因为它会破坏事物”之间来回摆动而且“但也许它可以做到这一点”。我要做一个实验...... - Justin
@Jason当任何引用的程序集发生更改时,本机映像无效,因此我不再确定交叉程序集调用是否会失效。 - Justin


你的逻辑似乎对我来说是错误的,无论性能影响如何,调用一个空方法都会闻到糟糕的设计。

在您的情况下,您有一个接口 Of Person。你是在说,为了成为一个人,你必须能够衰老,正如你的强制执行 AgeAYear 方法。但是,根据AgeAYear方法中的逻辑(或缺少它),a ImmortalPerson 不能年龄,但仍然可以 Person。你的逻辑与自己相矛盾。你可以通过多种方式解决这个问题,但这是第一个突然出现的问题。

实现此目的的一种方法是设置两个接口:

interface IPerson { void Walk(); }
interface IAgeable : IPerson { void AgeAYear();}

你现在可以清楚地区分你不必年龄变成一个人,但为了年龄,你必须是一个人。例如

class ImmortalPerson : IPerson
{
    public void Walk()
    {
        // Do Something
    }
}

class RegularPerson : IAgeable
{
    public void AgeAYear()
    {
        // Age A Year
    }

    public void Walk()
    {
       // Walk
    }
}

因此,对你而言 RegularPerson,通过实施 IsAgeable,你也需要实施 IPerson。为您 ImmortalPerson,你只需要实现 IPerson

然后,您可以执行以下操作或其变体:

List<IPerson> people = new List<IPerson>();

people.Add(new ImmortalPerson());
people.Add(new RegularPerson());

foreach (var person in people)
{
   if (person is IAgeable)
   {
      ((IAgeable)person).AgeAYear();
   }
}

鉴于上面的设置,您仍然强制执行您的类 IPerson 被视为一个人,但只有在他们实施的情况下才能被老化 IAgeable


3
2017-08-25 14:38





编译器无法理解将要调用哪两个函数,因为这是在运行时设置的函数指针。

你可以通过检查Person中的一些变量,确定它的类型或使用dynamic_cast来检查它来避免它。如果函数不需要调用,那么你可以忽略它。

调用函数包含一些指令:

  • 推送进程堆栈上的参数(在这种情况下没有)
  • 推送返回地址和一些其他数据
  • 跳到功能

当功能结束时:

  • 从功能中跳回来
  • 更改堆栈指针以有效地删除被调用函数的堆栈帧

它可能看起来很多,但也许它只是检查变量类型和避免调用的成本的两倍或三倍(在另一种情况下,你检查一些变量和可能的跳转,几乎与调用空函数几乎相同。你只会保存返回成本。但是,你要检查需要调用的函数,所以最后你可能没有保存任何东西!)

在我看来,与单纯的函数调用相比,您的算法对代码性能的影响要大得多。所以,不要用这样的小事来欺骗自己。

调用大量的空函数(可能是数百万)可能会对程序的性能产生一些影响,但是如果发生这种情况,则意味着你正在做一些算法错误的事情(例如,认为你应该将NormalPersons和ImmortalPersons放在同一个地方名单)


1
2017-08-25 14:01





在相对现代的工作站上,C#委托或接口调用需要进行 2纳秒。为了比较:

  • 分配一个小阵列:10纳秒
  • 分配一个闭包:15纳秒
  • 采取无争议的锁定:25纳秒
  • 字典查找(100个短字符串键):35纳秒
  • DateTime.Now (系统调用):750纳秒
  • 通过有线网络进行数据库调用(计入小表):1,000,000纳秒(1毫秒)

因此,除非您正在优化紧密循环,否则方法调用不可能成为瓶颈。如果您正在优化紧密循环,请考虑更好的算法,例如索引中的内容 Dictionary 在处理之前。

我在3.40 GHz的Core i7 3770上测试了这些,使用LINQPad 32位并打开了优化。但由于内联,优化,寄存器分配和其他编译器/ JIT行为,方法调用的时间将根据上下文而有很大差异。 2纳秒只是一个大概的数字。

在您的情况下,您循环遍历列表。循环开销可能主导方法调用开销,因为 内部循环涉及许多方法调用。在您的情况下,性能不太可能是一个问题,但如果您有数百万个项目和/或您经常需要更新它们,请考虑更改您表示数据的方式。例如,您可以拥有一个全局增量的“年”变量,而不是增加每个人的“年龄”。


0
2018-05-30 15:17