【问题标题】:Overhead of implementing an interface实现接口的开销
【发布时间】:2010-10-27 19:15:12
【问题描述】:

我的一位同事告诉我,实现接口会带来开销。这是真的吗?

我不关心微优化;我只是想知道这需要更深层次的细节。

【问题讨论】:

    标签: c# java .net performance interface


    【解决方案1】:

    无法抗拒并对其进行了测试,看起来几乎没有开销。

    参与者是:

    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
    

    【讨论】:

    • 需要注意的是,接口中声明的方法自动是“虚拟的”。这意味着 JIT 编译器永远不会内联此类方法调用。所以,如果你用单行方法实现接口,即使你尝试使用“[MethodImpl(MethodImplOptions.AggressiveInlining)]”(C#),你总是会付出调用成本。
    【解决方案2】:

    Java 的角度来看,至少在最近的 Hotspot 版本中,答案通常是在重要时很少或没有开销

    例如,假设您有如下方法:

    public void doSomethingWith(CharSequence cs) {
      char ch = cs.charAt(0);
      ...
    }
    

    CharSequence 当然是一个接口。因此,您可能期望该方法必须做额外的工作,检查什么对象类型,找到方法,并且这样做可能最后搜索接口等等 - 基本上所有您可以想象的恐怖故事......

    但实际上,VM 可以比这聪明得多。如果在实践中,你总是传递特定类型的对象,那么它不仅可以跳过对象类型检查,甚至可以内联方法。例如,如果您在一系列字符串的循环中调用上述方法,Hotspot 实际上可以内联对 charAt() 的调用,以便从字面上获取字符变成一对 MOV 指令——换句话说,接口上的方法调用可能会变成根本没有方法调用。 (P.S. 此信息基于 1.6 更新 12 的调试版本的程序集输出。)

    【讨论】:

      【解决方案3】:

      接口确实会产生开销,因为调用方法或访问属性时会执行额外的间接操作。很多实现多态的系统,包括接口的实现,一般都使用虚拟方法表,根据运行时类型映射函数调用。

      理论上,编译器可以优化对普通函数调用或内联代码的虚函数调用,前提是编译器能够证明进行调用的对象的历史记录。

      在绝大多数情况下,使用虚函数调用的好处远远大于缺点。

      【讨论】:

      • 至少在 Java 中,所有函数调用都是虚拟的,所以我怀疑会有任何可衡量的开销。
      【解决方案4】:

      我不关心微优化,只是想知道这需要更深入的细节。

      有开销,但这是一个微优化级别的开销。例如,一个接口可以在 IL 中进行多次调用,从 call 切换到 callvirt,但这非常小。

      【讨论】:

      • 谢谢里德。调用 callvirt 开关是什么意思?
      • 在 IL 中,有两种不同的调用:“call”用于调用方法,“callvirt”用于调用虚函数。 Callvirt 强制在 CLR 中进行虚拟方法表查找,因此它比“调用”函数慢(这就是为什么,顺便说一句,密封类可以帮助微优化级别的性能)。使用接口使接口中的方法调用使用 callvirt,有时在技术上它们“不必”使用 callvirt 时。
      • 谢谢里德。当你说有时他们不必这样做时,什么时候发生?隐式调用接口成员时?
      • 是的 - 有时您可以直接在对对象的引用上调用方法,并且编译器知道类型,并且没有子类,所以从技术上讲,您应该能够使用“呼叫”指令。如果该方法是已实现接口的一部分,它将使用“callvirt”代替,这会增加微观开销。这就是密封类的帮助所在——它使 IL 更有可能被调用而不是 callvirt,因为密封的类成员无法被覆盖。
      【解决方案5】:

      是的,接口会产生开销。事实上,您在逻辑和处理器之间添加的每一层都会增加开销。显然,您应该在汇编中编写所有内容,因为这是唯一不会产生开销的东西。 GUI 也会产生开销,所以不要使用这些。

      我在开玩笑,但重点是您必须在清晰、可理解、可维护的代码和性能之间找到自己的平衡点。对于 99.999%(当然是重复)的应用程序,只要您注意避免不必要地重复执行任何更昂贵的方法,您就永远不会达到需要使某些东西更难维护的地步只是为了让它跑得更快。

      【讨论】:

      • 99.999...% == 100% (( en.wikipedia.org/wiki/.999 )) 所以你说没有应用程序需要为了性能而牺牲可维护性。
      • 早期优化是万恶之源。在您确定未优化的应用程序不符合规范之前永远不要优化 - 然后在“优化”之前和之后进行测试以确保您使其符合规范(如果不符合规范,请回滚!)最后,让您的未优化那里的代码作为 cmets 准确地说明了它是如何失败的,因此其他人不会认为您不知道自己在做什么,并对其进行去优化以提高可读性。
      • 应用程序永远不应该为了性能而牺牲维护,除非那些性能收益超过可维护性损失的情况。这不仅仅是一句油嘴滑舌的句子。基本上,这意味着如果您的首要目标是让方法运行速度提高 10%,那么最终您可能需要牺牲可维护性来获得这 10%。但是,除非必须这样做,否则您不应该进行这些更改和牺牲,因为可维护性通常比轻微的性能提升更重要。
      • 你是对的。但是,当您有关键的实时要求时(我总是对我的工作进行处理),您必须考虑每一个微小的收获。
      【解决方案6】:

      除非您有非常具体的要求(即“我们正在制作一款游戏,它必须以 60fps 的速度运行,数据布局、缓存一致性和所述数据的转换非常重要”),否则我什至不会费心去想编写软件时接口调用的额外性能成本。只有在特定情况下才会引人注目。

      例如:通过紧密循环中的接口进行无数次调用(即使这样,执行方法主体的成本也可能会使接口调用开销相形见绌),或者在处理数据时进行大量“指针追逐”。

      此外,没有什么可以阻止您更改接口的合同以使调用更粗粒度,例如IMyInterface.Process(Car[] cars) 而不是 IMyInterface.Process(Car car)

      在 Code Complete 中,Steve McConnell 建议不要进行持续的微优化。他说最好使用良好实践(即专注于可维护性、可读性等)编写程序,然后在完成后,如果性能不够好,请对其进行分析并采取措施解决主要瓶颈。

      仅仅因为它可能更快,就推测性地优化你的所有代码是没有意义的。如果 80% 的执行时间花在执行 20% 的代码上,那么“仅仅因为”它可能会在这里或那里减少 10 微秒,就在各处牺牲松散耦合显然是愚蠢的。因此,您节省了 10 微秒,但如果某些其他功能正在吞噬 CPU,您的程序将不会变得更快。

      如果您正在开发非性能关键软件,我会将避开接口归类为微优化。如果需要,它也可以在之后轻松删除(假设您不会很快发布软件)。大多数软件不需要极快的速度。也有例外(游戏、模拟器、实时股票交易等),但即便如此,罪魁祸首并不总是界面。

      【讨论】:

      • 这就是我想知道的。所以你的意思是如果我在接口外(隐式)调用接口方法,会和普通的方法调用一样吗?
      • 根据 Eric 的说法,是的。 blogs.msdn.com/ericgu/archive/2004/03/19/92911.aspx
      • 正如 Donald Knuth 所说:过早的优化是万恶之源 ...
      【解决方案7】:

      开销与什么相比?

      通过接口调用比调用非虚拟方法更昂贵,是的。我没有亲自测试过,但我认为它的大小与虚拟通话相似。

      也就是说,在大多数情况下,性能通常不是不为某事使用接口的正当理由——大多数情况下,调用量还不够重要。

      【讨论】:

      • 谢谢。与不实施它们相比的开销。隐式实现呢?他们会和普通方法有同样的速度吗?
      • 你为什么不简单地测量它来找出自己?
      • 我可以,但我对哪个更快不感兴趣,但为什么。
      【解决方案8】:

      不幸的是,Java 还可以进行一些优化来提高界面性能。是的,与invokespecial 相比,invokevirtual 和invokeinterface 指令“几乎没有开销”,但是有一个Da Vinci project,它针对接口的非常常见的琐碎使用中的性能缺陷:一个只实现单个接口并且永远不会实现的对象重载。

      请参阅this Java bugparade request for enchancement,了解您希望获得的所有技术细节。

      与往常一样(您似乎明白这一点),当您对此类微优化提出质疑时,请咨询Amdahl's Law。如果您要进行如此多的方法调用并且需要速度,请考虑将重构与彻底的基准测试相结合。

      【讨论】:

        【解决方案9】:

        虽然接口不应该产生开销,但它确实会产生开销。我不直接知道这一点,但是二手的,我们在有线电视盒上工作,它们的动力太弱了,以至于我们测试了不同的性能场景。 (实例化的类数有很大的不同)。

        这不应该是因为接口在运行时影响很小,它不像调用“通过”接口,接口只是编译器链接两段代码的方式——在运行时它不应该是不仅仅是一个指针调度表。

        然而,它确实会影响性能。我猜它与元数据/反射有关,因为如果您不需要元数据,则使用接口的唯一时间是从不太具体的接口转换到该接口时,然后它只需要做个标签,看看有没有可能。

        我会关注这个问题,因为我很想知道是否有人知道技术原因。您可能希望将其扩展到 Java,因为这将是完全相同的原因,并且您更有可能使用 Java 获得答案,因为一切都是开源的。

        【讨论】:

        • 我认为开销可能在于确定运行时类型。尽管如此,我的理解是每个对象都有自己的虚拟方法表,其初始化发生在构造时。我不认为从接口间接调用方法与从对象本身直接调用的执行方式有很大不同,因为这两个调用通常都必须通过同一个虚拟方法表。
        【解决方案10】:

        出于好奇,我实现了small benchmark。根据我的想法,代码应该阻止 JVM 缓存方法解析结果,因此应该在 Java 中显示 invokeinterfaceinvokevirtual 之间的明显区别。

        基准测试的结果是 invokeinterfaceinvokevirtual 慢 38%。

        【讨论】:

          【解决方案11】:

          通过接口调用比其他形式的 virtual 方法调用成本略高,因为 vtable 中有额外的间接层。在大多数情况下,这无关紧要,因此您不必过于担心性能并坚持良好的设计。

          话虽如此,最近我重构了一些类,引入接口并通过接口进行所有调用。我非常自信(或懒惰),以至于我们在没有性能检查的情况下发布它不会产生任何影响。事实证明,这对整个应用程序的性能(不仅仅是调用)有 10% 的影响。我们进行了许多更改,这是我们怀疑的最后一件事。最终,当我们切换回具体类时,原始性能恢复了。

          这是一个高度优化的应用程序,以上可能不适用于其他情况。

          【讨论】:

          • 在大多数情况下,虚拟方法调用和通过接口进行的调用在本质上是完全相同的。
          • 不,因为一个对象可能实现许多接口,但只有一个基类。这意味着来自父类的方法的 vtable 是可预测的,并且可以直接使用。接口 vtable 没有这种可预测的布局,因为对象可以实现任意数量的接口。
          【解决方案12】:

          虚拟存根分派不同于接口分派。 Vance Morrison,CLR JIT 负责人在这篇博文中详细描述了这一点。 http://blogs.msdn.com/vancem/archive/2006/03/13/550529.aspx

          【讨论】:

            猜你喜欢
            • 2013-01-29
            • 1970-01-01
            • 2021-04-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-08-26
            • 1970-01-01
            • 2013-08-03
            相关资源
            最近更新 更多