调用空函数需要多长时间

本文关键字:长时间 函数 调用 | 更新日期: 2023-09-27 18:06:34

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

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有其他具有代码的方法—它不仅仅是一个什么都不做的对象。

调用空函数需要多长时间

这会对性能产生影响吗?

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

如果有,多少钱?

您可以使用分析器精确地量化您的特定代码路径。我们不能,因为我们不知道代码路径。我们可以猜测,并告诉您这几乎肯定无关紧要,因为这极不可能成为应用程序的瓶颈(您真的坐在那里在一个紧密的循环中调用Person.AgeAYear吗?)

只有你可以找到精确地通过拿出一个分析器和测量。

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

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

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

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

可能,如果函数被调用,那么调用本身将花费少量的时间。

  • 如果是,增加多少?

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

  • 空函数会被优化掉吗?

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

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

方法调用不能被优化出来,因为可能会提供IPerson的不同实现,它实际上在这个方法中做了一些事情。对于任何针对IPerson接口的调用,如果编译器不能证明它总是与ImmortalPerson实例一起工作,则肯定是这种情况。

最终你必须问自己"有什么替代方法?"answers"这真的有足够大的影响来保证另一种方法吗?"。在这种情况下,影响将是非常小-我想说,在这种情况下,以这种方式使用空方法是完全可以接受的。

在我看来,您的逻辑似乎是错误的,而且不管对性能的影响如何,调用空方法就像糟糕的设计。

在您的情况下,您有一个接口是Of Person。你是说为了成为一个人,你必须能够变老,就像你的AgeAYear方法所强制的那样。然而,根据您的ageyear方法中的逻辑(或缺乏逻辑),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时才能被认为是一个人。

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

可以通过检查Person内部的某个变量,确定其类型或使用dynamic_cast来检查它,从而避免这种情况。如果函数不需要调用,则可以忽略它。

调用一个函数由几个指令组成:

  • 将参数压入进程堆栈(在这种情况下没有)
  • 推送返回地址和其他一些数据
  • 跳转到函数

当函数结束时:

  • 从函数返回
  • 改变堆栈指针以有效地移除被调用函数的堆栈帧

这对您来说可能看起来很多,但可能只是检查变量类型并避免调用的成本的两倍或三倍(在另一种情况下,您需要检查某些变量并可能进行跳转,这几乎与调用空函数花费相同的时间)。你只会省下退货的费用。但是,您会对需要调用的函数进行检查,因此最终您可能不会保存任何内容!)

在我看来,你的算法对你的代码性能的影响比一个简单的函数调用要大得多。所以,不要为这样的小事而烦恼。

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

在一个相对现代的工作站上,c#委托或接口调用大约需要2纳秒。比较:

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

因此,除非您正在优化紧密循环,否则方法调用不太可能成为瓶颈。如果你正在优化一个紧密循环,考虑一个更好的算法,比如在处理它们之前在Dictionary中对它们进行索引。

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

在您的示例中,您正在遍历列表。循环开销可能会主导方法调用开销,因为内部循环涉及大量方法调用。在您的情况下,性能不太可能是一个问题,但如果您有数百万项和/或您需要定期更新它们,请考虑更改表示数据的方式。例如,您可以有一个单独的"year"变量,您可以对其进行全局递增,而不是对每个人的"age"递增。