调用空函数需要多长时间
本文关键字:长时间 函数 调用 | 更新日期: 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"递增。