我有一个实现接口的项目列表。对于这个问题,让我们使用这个示例界面:
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
s,我可能会调用空函数。 这会对性能产生影响吗?如果是这样,多少钱? 对于所有意图和目的,空函数是否会被优化掉?
注意:在真实的例子中, ImmortalPerson
有其他方法确实有代码 - 它不仅仅是一个什么都不做的对象。
这会对性能产生影响吗?
极不可能有一个 富有意义的 绩效影响。
如果是这样,多少钱?
你可以量化它 究竟 使用分析器的特定代码路径。我们不能,因为我们不知道代码路径。我们可以猜测,并告诉你它几乎肯定没关系,因为这不太可能是你的应用程序的瓶颈(你真的坐在那里呼叫) Person.AgeAYear
紧紧的循环?)。
只有你可以找到 恰恰 通过取出一个分析器和测量。
对于所有意图和目的,空函数是否会被优化掉?
这肯定是可能的,但可能不会;它甚至可能在未来版本的JITter中发生变化,或者从平台变为平台(不同的平台有不同的JITter)。如果您真的想知道,请编译您的应用程序,并查看反汇编的JITted代码(而不是IL!)。
但我会这样说:这几乎可以肯定,几乎绝对不值得担心或花时间进入。除非你打电话 Person.AgeAYear
在性能关键代码的紧密循环中,它不是应用程序的瓶颈。你可以花时间在这上面,或者你可以花时间改进你的应用程序。你的时间也有机会成本。
是 也许,如果调用该函数,则调用本身将花费少量时间。
您永远不会注意到任何实际应用程序的差异 - 与执行任何“真实”工作的成本相比,调用方法的成本非常小。
我怀疑它 - CLR 无疑 如果方法在不同的程序集中,则可能不会执行此类优化,因为该方法将来可能会更改。它 威力 是可行的,这种优化是为程序集内部的方法调用完成的,但它在很大程度上取决于代码,例如在下面的示例中:
foreach (IPerson person in people)
{
person.AgeAYear();
}
方法调用 不能 因为不同的实现而被优化 IPerson
可能会提供实际上在这种方法中做的事情。这肯定是这样的 任何 打电话给 IPerson
接口所在的编译器无法证明它始终与a一起工作 ImmortalPerson
实例。
最终你必须问自己“有什么选择?”并且“这确实有足够大的影响来保证替代方法吗?” 。在这种情况下,影响将是 非常 小 - 我会说在这种情况下以这种方式使用空方法是完全可以接受的。
你的逻辑似乎对我来说是错误的,无论性能影响如何,调用一个空方法都会闻到糟糕的设计。
在您的情况下,您有一个接口 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
。
编译器无法理解将要调用哪两个函数,因为这是在运行时设置的函数指针。
你可以通过检查Person中的一些变量,确定它的类型或使用dynamic_cast来检查它来避免它。如果函数不需要调用,那么你可以忽略它。
调用函数包含一些指令:
- 推送进程堆栈上的参数(在这种情况下没有)
- 推送返回地址和一些其他数据
- 跳到功能
当功能结束时:
- 从功能中跳回来
- 更改堆栈指针以有效地删除被调用函数的堆栈帧
它可能看起来很多,但也许它只是检查变量类型和避免调用的成本的两倍或三倍(在另一种情况下,你检查一些变量和可能的跳转,几乎与调用空函数几乎相同。你只会保存返回成本。但是,你要检查需要调用的函数,所以最后你可能没有保存任何东西!)
在我看来,与单纯的函数调用相比,您的算法对代码性能的影响要大得多。所以,不要用这样的小事来欺骗自己。
调用大量的空函数(可能是数百万)可能会对程序的性能产生一些影响,但是如果发生这种情况,则意味着你正在做一些算法错误的事情(例如,认为你应该将NormalPersons和ImmortalPersons放在同一个地方名单)
在相对现代的工作站上,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纳秒只是一个大概的数字。
在您的情况下,您循环遍历列表。循环开销可能主导方法调用开销,因为 内部循环涉及许多方法调用。在您的情况下,性能不太可能是一个问题,但如果您有数百万个项目和/或您经常需要更新它们,请考虑更改您表示数据的方式。例如,您可以拥有一个全局增量的“年”变量,而不是增加每个人的“年龄”。