【发布时间】:2010-10-27 19:15:12
【问题描述】:
我的一位同事告诉我,实现接口会带来开销。这是真的吗?
我不关心微优化;我只是想知道这需要更深层次的细节。
【问题讨论】:
标签: c# java .net performance interface
我的一位同事告诉我,实现接口会带来开销。这是真的吗?
我不关心微优化;我只是想知道这需要更深层次的细节。
【问题讨论】:
标签: c# java .net performance interface
无法抗拒并对其进行了测试,看起来几乎没有开销。
参与者是:
Interface IFoo defining a method
class Foo: IFoo implements IFoo
class Bar implements the same method as Foo, but no interface involved
所以我定义了
Foo realfoo = new Foo();
IFoo ifoo = new Foo();
Bar bar = new Bar();
并调用该方法,该方法执行 20 次字符串连接,每个变量 10,000,000 次。
realfoo: 723 Milliseconds
ifoo: 732 Milliseconds
bar: 728 Milliseconds
如果该方法什么都不做,实际调用会更加突出。
realfoo: 48 Milliseconds
ifoo: 62 Milliseconds
bar: 49 Milliseconds
【讨论】:
从 Java 的角度来看,至少在最近的 Hotspot 版本中,答案通常是在重要时很少或没有开销。
例如,假设您有如下方法:
public void doSomethingWith(CharSequence cs) {
char ch = cs.charAt(0);
...
}
CharSequence 当然是一个接口。因此,您可能期望该方法必须做额外的工作,检查什么对象类型,找到方法,并且这样做可能最后搜索接口等等 - 基本上所有您可以想象的恐怖故事......
但实际上,VM 可以比这聪明得多。如果在实践中,你总是传递特定类型的对象,那么它不仅可以跳过对象类型检查,甚至可以内联方法。例如,如果您在一系列字符串的循环中调用上述方法,Hotspot 实际上可以内联对 charAt() 的调用,以便从字面上获取字符变成一对 MOV 指令——换句话说,接口上的方法调用可能会变成根本没有方法调用。 (P.S. 此信息基于 1.6 更新 12 的调试版本的程序集输出。)
【讨论】:
接口确实会产生开销,因为调用方法或访问属性时会执行额外的间接操作。很多实现多态的系统,包括接口的实现,一般都使用虚拟方法表,根据运行时类型映射函数调用。
理论上,编译器可以优化对普通函数调用或内联代码的虚函数调用,前提是编译器能够证明进行调用的对象的历史记录。
在绝大多数情况下,使用虚函数调用的好处远远大于缺点。
【讨论】:
我不关心微优化,只是想知道这需要更深入的细节。
有开销,但这是一个微优化级别的开销。例如,一个接口可以在 IL 中进行多次调用,从 call 切换到 callvirt,但这非常小。
【讨论】:
是的,接口会产生开销。事实上,您在逻辑和处理器之间添加的每一层都会增加开销。显然,您应该在汇编中编写所有内容,因为这是唯一不会产生开销的东西。 GUI 也会产生开销,所以不要使用这些。
我在开玩笑,但重点是您必须在清晰、可理解、可维护的代码和性能之间找到自己的平衡点。对于 99.999%(当然是重复)的应用程序,只要您注意避免不必要地重复执行任何更昂贵的方法,您就永远不会达到需要使某些东西更难维护的地步只是为了让它跑得更快。
【讨论】:
除非您有非常具体的要求(即“我们正在制作一款游戏,它必须以 60fps 的速度运行,数据布局、缓存一致性和所述数据的转换非常重要”),否则我什至不会费心去想编写软件时接口调用的额外性能成本。只有在特定情况下才会引人注目。
例如:通过紧密循环中的接口进行无数次调用(即使这样,执行方法主体的成本也可能会使接口调用开销相形见绌),或者在处理数据时进行大量“指针追逐”。
此外,没有什么可以阻止您更改接口的合同以使调用更粗粒度,例如IMyInterface.Process(Car[] cars) 而不是 IMyInterface.Process(Car car)
在 Code Complete 中,Steve McConnell 建议不要进行持续的微优化。他说最好使用良好实践(即专注于可维护性、可读性等)编写程序,然后在完成后,如果性能不够好,请对其进行分析并采取措施解决主要瓶颈。
仅仅因为它可能更快,就推测性地优化你的所有代码是没有意义的。如果 80% 的执行时间花在执行 20% 的代码上,那么“仅仅因为”它可能会在这里或那里减少 10 微秒,就在各处牺牲松散耦合显然是愚蠢的。因此,您节省了 10 微秒,但如果某些其他功能正在吞噬 CPU,您的程序将不会变得更快。
如果您正在开发非性能关键软件,我会将避开接口归类为微优化。如果需要,它也可以在之后轻松删除(假设您不会很快发布软件)。大多数软件不需要极快的速度。也有例外(游戏、模拟器、实时股票交易等),但即便如此,罪魁祸首并不总是界面。
【讨论】:
开销与什么相比?
通过接口调用比调用非虚拟方法更昂贵,是的。我没有亲自测试过,但我认为它的大小与虚拟通话相似。
也就是说,在大多数情况下,性能通常不是不为某事使用接口的正当理由——大多数情况下,调用量还不够重要。
【讨论】:
不幸的是,Java 还可以进行一些优化来提高界面性能。是的,与invokespecial 相比,invokevirtual 和invokeinterface 指令“几乎没有开销”,但是有一个Da Vinci project,它针对接口的非常常见的琐碎使用中的性能缺陷:一个只实现单个接口并且永远不会实现的对象重载。
请参阅this Java bugparade request for enchancement,了解您希望获得的所有技术细节。
与往常一样(您似乎明白这一点),当您对此类微优化提出质疑时,请咨询Amdahl's Law。如果您要进行如此多的方法调用并且需要速度,请考虑将重构与彻底的基准测试相结合。
【讨论】:
虽然接口不应该产生开销,但它确实会产生开销。我不直接知道这一点,但是二手的,我们在有线电视盒上工作,它们的动力太弱了,以至于我们测试了不同的性能场景。 (实例化的类数有很大的不同)。
这不应该是因为接口在运行时影响很小,它不像调用“通过”接口,接口只是编译器链接两段代码的方式——在运行时它不应该是不仅仅是一个指针调度表。
然而,它确实会影响性能。我猜它与元数据/反射有关,因为如果您不需要元数据,则使用接口的唯一时间是从不太具体的接口转换到该接口时,然后它只需要做个标签,看看有没有可能。
我会关注这个问题,因为我很想知道是否有人知道技术原因。您可能希望将其扩展到 Java,因为这将是完全相同的原因,并且您更有可能使用 Java 获得答案,因为一切都是开源的。
【讨论】:
出于好奇,我实现了small benchmark。根据我的想法,代码应该阻止 JVM 缓存方法解析结果,因此应该在 Java 中显示 invokeinterface 和 invokevirtual 之间的明显区别。
基准测试的结果是 invokeinterface 比 invokevirtual 慢 38%。
【讨论】:
通过接口调用比其他形式的 virtual 方法调用成本略高,因为 vtable 中有额外的间接层。在大多数情况下,这无关紧要,因此您不必过于担心性能并坚持良好的设计。
话虽如此,最近我重构了一些类,引入接口并通过接口进行所有调用。我非常自信(或懒惰),以至于我们在没有性能检查的情况下发布它不会产生任何影响。事实证明,这对整个应用程序的性能(不仅仅是调用)有 10% 的影响。我们进行了许多更改,这是我们怀疑的最后一件事。最终,当我们切换回具体类时,原始性能恢复了。
这是一个高度优化的应用程序,以上可能不适用于其他情况。
【讨论】:
虚拟存根分派不同于接口分派。 Vance Morrison,CLR JIT 负责人在这篇博文中详细描述了这一点。 http://blogs.msdn.com/vancem/archive/2006/03/13/550529.aspx
【讨论】: