2

我经常看到人们抱怨 C++ 中的虚函数开销。但同样的人不会对 java 中的运行时多态性说任何负面的话。

如果我必须在 c++ 中开发一个运行时,该运行时在其上托管同样用 c++ 开发的应用程序,我将采用以下方法:

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

现在应用程序和运行时都有彼此的句柄,并使用虚拟方法调用互相调用。是的,这涉及到成本,但这也让我们将应用程序与运行时解耦。如果没有虚拟方法,应用程序将始终与运行时具有链接时间依赖性。

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

我对 java 运行时多态性的理解正确吗?

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

我的问题是如何处理这些评论?可以给出哪些合理的论据来支持 C++?

4

4 回答 4

4

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

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

C++ 没有所有这些机制:在 C++ 中必须通过引用/指针来存储和访问对象既乏味又容易出错,而且效率低下(数组成员指向的对象不会被分配到彼此,因此访问每个对象可能会导致缓存未命中)。而当 C++ 编译器遇到虚拟调用时,它通常无法确定将调用哪个函数,因此它无法优化掉虚拟性。当它不能这样做时,它也不能内联调用(C++ 编译器严重依赖于性能)。

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

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

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

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

如果我必须在 c++ 中开发一个运行时,该运行时在其上托管同样用 c++ 开发的应用程序,我将采用以下方法 <...>

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

涉及成本,但这也使我们将应用程序与运行时分离。如果没有虚拟方法,应用程序将始终与运行时具有链接时间依赖性。

和?有这样的链接时间依赖性有问题吗?您要在应用程序运行时更换它吗?

于 2013-06-30T10:15:15.933 回答
1

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

不用太“语言警惕”,我认为使用 Java 的人不一定像 C++ 程序员那样专注于性能,这可能反映了“哦,我们将不得不担心高架”。

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

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

于 2013-06-30T09:09:26.550 回答
1

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

此外,Java VM 旨在处理虚拟调用。这有一些性能影响,但您无法衡量它,因为您没有其他方法。此外,较新的 VM 还可以在运行时内联虚拟方法。

在 C++ 中,您有一段历史。C++ 来自 C,早期没有动态链接。随着时间的推移,动态链接被添加,这个链接过程必须以兼容的方式完成。然后以应与此兼容的方式添加虚拟功能。此外,第一个 C++ 编译器只是 C 的预处理器,因此必须将虚函数映射到现有的 C 结构。

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

于 2013-06-30T09:29:14.020 回答
0

可能有多种因素导致您看到的意见不同。

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

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

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

于 2013-06-30T10:41:13.347 回答