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

时间:2011-08-25 13:49:05

标签: c# performance function compiler-optimization

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

interface Person
{
  void AgeAYear();
}

有两个班级

class NormalPerson : Person
{
  int age = 0;

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


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

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


注意:在实际示例中,ImmortalPerson具有其他具有代码的方法 - 它不仅仅是一个不执行任何操作的对象。

5 个答案:

答案 0 :(得分:9)

  

这会对性能产生影响吗?

极不可能产生有意义的性能影响。

  

如果是这样,多少钱?

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

只有您可以通过取出分析器并进行测量来找出精确

  

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

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

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

答案 1 :(得分:3)

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

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

  • 如果是这样,多少钱?

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

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

我对此表示怀疑 - 如果方法在不同的程序集中,CLR 肯定可能不会执行这种优化,因为该方法将来可能会发生变化。 可能可行,这种优化是为程序集内的方法调用完成的,但它在很大程度上取决于代码,例如在下面的示例中:

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

方法调用无法优化,因为可能会提供IPerson的不同实现,这实际上在此方法中有所作为。对于IPerson接口的任何调用肯定会出现这种情况,编译器无法证明它始终使用ImmortalPerson实例。

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

答案 2 :(得分:3)

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

在您的情况下,您有一个Of Person的界面。您声明,为了成为一个人,您必须能够按照AgeAYear方法强制执行年龄。但是,根据AgeAYear方法(或缺少)的逻辑,ImmortalPerson不能老化,但仍然可以是Person。你的逻辑与自己相矛盾。你可以通过多种方式解决这个问题,但这是第一个突然爆发的问题。

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

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

您现在可以清楚地区分您不必成年为一个人,但为了年龄,您必须是一个人。 e.g。

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 :(得分:1)

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

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

调用函数包含一些指令:

  • 推送进程堆栈上的参数(在本例中为none)
  • 推送返回地址和其他一些数据
  • 跳转到该功能

当功能结束时:

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

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

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

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

答案 4 :(得分:1)

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

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

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

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

在您的情况下,您正在遍历列表。由于looping internally involves lots of method calls,循环开销很可能主导方法调用开销。在您的情况下,性能不太可能是一个问题,但如果您有数百万个项目和/或您经常需要更新它们,请考虑更改您表示数据的方式。例如,您可以使用单个“年”变量来全局递增,而不是增加每个人的“年龄”。