c++与java中运行时多态性的成本比较

cost comparison of run-time polymorphism in c++ vs java

本文关键字:比较 多态性 运行时 java c++      更新时间:2023-10-16

我经常看到有人抱怨C++中的虚拟函数成本。但同样的人并没有对java中的运行时多语言主义说任何负面的话。

如果我必须在c++中开发一个运行时,该运行时承载着同样在c++中开发的应用程序,我会采取以下方法:

  1. 具有带有虚拟方法的基类,这些方法表示"抽象"应用程序。这是运行时对应用程序的视图。应用程序开发人员继承基类,并将其应用程序编译为可动态加载的共享库
  2. 具有带有虚拟方法的基类,该方法表示运行时的"服务"。运行时实现这些服务。应用程序通过这些接口类使用这些服务。运行时加载应用程序动态库,并使用其服务的实现对象初始化应用程序

现在,应用程序和运行时都有彼此的句柄,并使用虚拟方法调用来调用彼此。是的,这是有成本的,但这也让我们将应用程序与运行时脱钩。如果没有虚拟方法,应用程序将始终与运行时具有链接时间依赖关系。

现在考虑Java中的类似情况。Java也有需要实现的接口,并且运行时多态性也必须有类似的成本。

我对java运行时polymorhism的理解正确吗

如果java也涉及成本,为什么c++总是会得到愤怒的评论,"哦,有虚拟函数成本,使用虚拟函数解耦应用程序部分的方法肯定很糟糕"。如果是Java,这些人会去哪里?他们什么也没说。

我的问题是如何处理这些评论?有什么合理的论据可以支持C++?

Java基本上是完全端到端设计的,以加速精确的场景:

  • 对象总是通过引用访问的(这样你就可以在同一个类层次结构中混合和匹配不同的类型。你可以制作一个Fruit对象数组,它也可以自然地存储Banana对象。这不会影响性能,但它使使用运行时多态性的代码更容易编写。)
  • 它使用了一个垃圾收集器,它允许对象在分配后在内存中移动,这样,即使数组只存储引用,被引用的对象也可以在连续的内存中打包在一起,以最大限度地减少缓存未命中,否则会严重损害性能
  • 该语言是JIT’ed(或者在某些情况下是解释的),因此在运行时,JVM可以查看虚拟调用,并在许多情况下将其优化为常规函数调用

C++并不具备所有这些机制:必须在C++中通过引用/指针来存储和访问对象既乏味又容易出错,但效率也很低(数组成员指向的对象不会相邻分配,因此访问每个对象可能会导致缓存未命中)。当C++编译器遇到虚拟调用时,它通常无法确定要调用哪个函数,因此无法优化虚拟性。当它不能做到这一点时,它也不能内联调用(C++编译器在性能上非常依赖调用)。

但另一方面,C++也不需要那么依赖它。相反,C++为您提供了强大的静态多态性,可以使用来代替,从而完全消除了开销。

因此,是的,运行时多态性,带有虚拟调用和继承,通常在C++中成本更高,因为它没有加速所需的大量管道。但同时,C++也使运行时多态性更难使用,在许多情况下,它提供了可以使用的替代方案。

人们经常声称"虚拟调用的代价只是指针间接性",但正如上文所暗示的那样,它有许多微妙的代价:它禁止函数内联,并且要求使用引用语义来处理对象,这再次影响了内存局部性,损害了CPU缓存的利用率。它具有广泛的影响,Java是从零开始有效地设计的,以尽可能多地弥补这些影响。C++不是,而且在大多数情况下,当使用运行时多态性时,必须承受性能打击。

当然,典型的C++程序员比Java程序员更关心性能(例如,你不经常听到Java程序员讨论他们代码的CPU缓存利用率)

如果我必须在c++中开发一个运行时,该运行时上承载着同样用c++开发的应用程序,我会采取以下方法<…>

请不要。如上所述,C++代码通常不会也不应该使用运行时多态性来解决每一个问题。在Java中,它实际上是您可以访问的唯一工具,而且它已经被使用,而且必须被使用,并且应该被广泛使用。在C++中,它是整个选项工具箱中的一个工具。当有替代方案时,通常最好避免。

这涉及到成本,但这也使我们能够将应用程序与运行时脱钩。如果没有虚拟方法,应用程序将始终与运行时具有链接时间依赖关系。

然后呢?这样的链接时间依赖性有问题吗?你打算在应用程序运行时换掉它吗?

我担心在这里写答案是徒劳的,因为这里必须已经有答案了。

在不太"语言战争"的情况下,我认为使用Java的人不一定像C++程序员那样专注于性能,这可能反映了"哦,我们必须担心开销"之间的部分概念差异。

虚拟函数和非虚拟函数之间肯定存在一些开销[1],而非虚拟函数有时可以在虚拟版本无法内联的地方内联。但考虑到其他选择(使用开关或if语句来决定做什么)也很少是好的,只要有一点好的设计(不要设计调用虚拟函数将两个整数相加的东西,如果它要在一个紧密的循环中调用——因为开销会相当大——使用一个知道有数百或数千个整数要相加的函数——当然,如果可能的话)。

[1] 开销包括间接读取this指针以找到vtable,然后在偏移X处调用函数。主要开销往往是使用了"更多的寄存器",这可能会对代码的效率产生负面影响。

C++和Java在使用虚拟方法方面的主要区别在于Java开发人员别无选择,因此Java开发人员不会考虑这种情况下的性能影响。

此外,Java虚拟机是为处理虚拟调用而设计的。这有一些性能含义,但你无法衡量,因为你没有其他方法。此外,较新的虚拟机也可以在运行时内联虚拟方法。

在C++中,你有一段历史。C++源于C,早期没有动态链接。随着时间的推移,添加了动态链接,并且这个链接过程必须以兼容的方式进行。添加虚拟函数的方式应该与此兼容。此外,第一个C++编译器只是C的预处理器,因此虚拟函数必须映射到现有的C结构。

因此,我认为C++中虚拟函数的性能影响是基于历史的。

可能有多种因素导致您所看到的意见差异。

一个因素是,担心运行时多态性的性能损失的C++人群可能与在Java中免费使用运行时多态的人群不同。

另一个因素是,当一个项目使用C++时,这通常意味着有一些严格的性能要求。在这些情况下,程序员无论如何都会不遗余力地调整输入、算法和数据结构,以减少动态绑定,并帮助编译器和CPU预测和优化关键操作。相反,在使用Java的环境中,通常有比严格的性能更重要的要求,在这些情况下,运行时多态性是不可避免的。

另一个非常重要的因素是,C++程序通常是AOT编译的,默认情况下编译器不能利用运行时出现的大量新信息。另一方面,JVM可以使用JIT编译器来调整关键区域的本地代码,以便获得更高的性能以获得最可能的结果。面向对象语言的框架在优化运行时绑定方面往往非常高效。