C ++中的AI应用程序:虚拟函数的成本有多高?有哪些可能的优化方法?

时间:2020-03-06 14:57:40  来源:igfitidea点击:

在我用C ++编写的AI应用程序中,

  • 没有太多的数值计算
  • 有很多需要运行时多态的结构
  • 很多时候,在计算过程中会有多个多态结构相互作用

在这种情况下,是否有任何优化技术?尽管我现在不在乎优化应用程序,但为该项目选择基于Java的C ++的一个方面是要启用更多的杠杆作用来进行优化,并能够使用非面向对象的方法(模板,过程,重载)。

特别是,与虚拟功能相关的优化技术是什么?虚拟功能是通过内存中的虚拟表实现的。有什么方法可以将这些虚拟表预取到L2高速缓存中(从内存/ L2高速缓存中获取的成本正在增加)?

除此之外,在C ++中是否有关于数据局部性技术的良好参考?这些技术将减少等待数据进入计算所需的L2缓存的等待时间。

更新:另请参见以下相关论坛:接口的性能惩罚,多个基类级别

解决方案

对于此类常用项目,我们几乎不必担心缓存,因为它们仅被提取一次并保存在那里。

通常,只有在处理具有以下两种情况的大型数据结构时,缓存才是一个问题:

  • 足够大并且可以由单个函数使用很长时间,以便该函数可以将我们需要的其他所有内容从缓存中推出,或者
  • 可以被足够随机地访问,以使当我们从数据结构加载数据结构本身时不必将它们存储在缓存中。

诸如Vtables之类的东西通常不会成为性能/缓存/内存问题。通常,每种对象类型只有一个Vtable,并且该对象包含指向Vtable的指针,而不是Vtable本身。因此,除非我们有数千种对象,否则我认为Vtables不会破坏缓存。

顺便说一句,1)是为什么像memcpy这样的函数对超大(多兆字节)数据输入使用诸如movnt(dq | q)之类的超高速缓存流指令。

如果AI应用程序不需要大量的数字运算,那么我就不用担心虚拟功能的性能劣势。仅当它们出现在反复评估的复杂计算中时,才会对性能产生一定的影响。我认为我们也不能强制虚拟表保留在L2缓存中。

虚拟功能有几种优化方法,

  • 人们编写了依赖于代码分析和程序转换的编译器。但是,这些不是生产级的编译器。
  • 我们可以用等效的" switch ... case"块替换所有虚拟函数,以根据层次结构中的类型调用适当的函数。这样,我们将摆脱编译器管理的虚拟表,并以switch ... case块的形式拥有自己的虚拟表。现在,我们自己的虚拟表在L2高速缓存中的机会与在代码路径中一样高。请记住,我们将需要RTTI或者自己的" typeof"函数来实现此目的。

我们是否已实际剖析并找到需要优化的地方和内容?

当我们发现虚拟函数调用实际上是瓶颈时,请进行实际优化。

虚函数非常有效。假设使用32位指针,则内存布局大致为:

classptr -> [vtable:4][classdata:x]
vtable -> [first:4][second:4][third:4][fourth:4][...]
first -> [code:x]
second -> [code:x]
...

classptr指向通常在堆上,偶尔在堆栈上的内存,并以指向该类的vtable的四字节指针开头。但是要记住的重要一点是,vtable本身未分配内存。这是一种静态资源,所有相同类类型的对象都将指向其vtable数组完全相同的内存位置。调用不同的实例不会将不同的内存位置拉到L2缓存中。

msdn的此示例显示了具有虚拟func1,func2和func3的类A的vtable。不超过12个字节。在编译的库中,不同类的vtable很有可能在物理上相邻(我们将要验证的是,我们特别担心),这可能会在微观上提高缓存效率。

CONST SEGMENT
??_7A@@6B@
   DD  FLAT:?func1@A@@UAEXXZ
   DD  FLAT:?func2@A@@UAEXXZ
   DD  FLAT:?func3@A@@UAEXXZ
CONST ENDS

另一个性能问题是通过vtable函数调用的指令开销。这也是非常有效的。与调用非虚拟函数几乎相同。再次从msdn的示例中:

; A* pa;
; pa->func3();
mov eax, DWORD PTR _pa$[ebp]
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _pa$[ebp]
call  DWORD PTR [edx+8]

在此示例中,堆栈框架基本指针ebp在零偏移处具有变量" A * pa"。寄存器eax在位置[ebp]处加载值,因此具有A *,而edx在位置[eax]处加载值,因此具有A类vtable。然后,用[ebp]加载ecx,因为ecx表示" this",它现在保存A *,最后调用位置[edx + 8],该值是vtable中的第三个函数地址。

如果此函数调用不是虚拟的,则不需要mov eax和mov edx,但是性能上的差异将非常小。

我们可以在运行时使用虚拟函数来实现多态性,并在编译时通过使用模板来实现多态性。我们可以使用模板替换虚拟功能。请参阅本文以获取更多信息http://www.codeproject.com/KB/cpp/SimulationofVirtualFunc.aspx

动态多态的一种解决方案可能是静态多态,如果类型在编译类型中是已知的,则可以使用该静态多态:CRTP(奇怪的重复模板模式)。

http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

Wikipedia上的解释很清楚,如果我们确实确定虚拟方法调用是性能瓶颈的根源,那么它可能会对我们有所帮助。

与正常功能相比,虚拟调用不会带来更大的开销。虽然,最大的损失是虚拟函数在多态调用时无法被内联。内联将在许多情况下代表性能的真正提高。

在某些情况下,我们可以做的事情是声明该函数为内联虚函数,以防止浪费该工具。

Class A {
   inline virtual int foo() {...}
};

而且,当我们处于代码点时,可以确定要调用的对象的类型,可以进行内联调用,这将避免使用多态系统并允许编译器进行内联。

class B : public A {
     inline virtual int foo() 
     {
         //...do something different
     }

     void bar()
     {
      //logic...
      B::foo();
      // more  logic
     }
};

在此示例中,对foo()的调用将变为非多态的,并绑定到foo()的B实现。但是仅当我们确定实例类型是什么时才执行此操作,因为自动多态性功能将消失,这对于以后的代码阅读器来说不是很明显。

我能想到的唯一优化是Java的JIT编译器。如果我理解正确,它将在代码运行时监视调用,并且如果大多数调用仅转到特定的实现,则当类正确时,它将向实现插入条件跳转。这样,在大多数情况下,不会进行vtable查找。当然,在极少数情况下,当我们传递不同的类时,仍使用vtable。

我不知道使用此技术的任何C ++编译器/运行时。

有关C ++性能的技术报告草稿的第5.3.3节完全致力于虚拟函数的开销。

现在的成本与最近的CPUS的正常功能或者多或者少相同,但是无法内联。如果我们调用该函数数百万次,则可能会产生很大的影响(例如,尝试对同一函数进行数百万次调用,例如,一次使用内联一次而不使用一次,如果该函数本身做一些简单的事情,我们会发现它会慢两倍;这并不是理论上的情况:在很多数值计算中都是很常见的)。

我正在强化所有有效的答案:

  • 如果我们实际上不知道这是个问题,那么对修复它的任何担心都可能放错了地方。

我们想知道的是:

  • 在调用方法的过程中,花费了多少执行时间(实际上是在其运行时),尤其是,哪种方法(根据此衡量标准)最昂贵。

一些探查器可以间接为我们提供此信息。他们需要在语句级别进行汇总,但不包括方法本身所花费的时间。

我最喜欢的技术是在调试器中将其暂停多次。

如果在虚拟函数调用过程中花费的时间很长,例如20%,那么平均而言,在调用堆栈的底部,在反汇编窗口中,将有5个样本中的1个显示用于跟踪虚拟函数的指令。函数指针。

如果我们实际上没有看到,那不是问题。

在此过程中,我们可能会在调用堆栈的上方看到其他内容,这些实际上是不需要的,可以节省大量时间。

虚函数往往是查找和间接函数调用。在某些平台上,这是快速的。在其他方面(例如,在控制台中使用的一种流行的PPC架构),这并不是那么快。

优化通常围绕在调用栈中更高的位置表达可变性,这样我们就无需在热点内多次调用虚拟函数。

正如其他答案已经指出的那样,虚拟函数调用的实际开销非常小。在每秒称为数百万次的紧密循环中,它可能会有所不同,但这很少有什么大不了的。

但是,它可能仍然会产生更大的影响,因为编译器很难进行优化。它不能内联函数调用,因为它在编译时不知道将调用哪个函数。这也使某些全局优化变得更加困难。而这要花多少性能呢?这取决于。通常无需担心,但是在某些情况下,这可能会严重影响性能。

当然,这还取决于CPU架构。在某些情况下,它可能变得相当昂贵。

但是值得记住的是,任何类型的运行时多态性或者多或者少都会带来相同的开销。通过switch语句或者类似的方法实现相同的功能以在许多可能的功能之间进行选择可能并不便宜。

唯一可靠的优化方法是将一些工作移至编译时。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。如果有可能将其部分实现为静态多态性,则可能会加快速度。

但是首先,请确保我们有问题。代码实际上太慢了以至于无法接受吗?
其次,找出通过探查器使速度变慢的原因。
第三,修复它。

使用现代的,前瞻性的,多调度的CPU,虚拟功能的开销很可能为零。娜达压缩。

静态多态性,正如一些用户在此处回答的那样。例如,WTL使用此方法。有关WTL实现的清晰说明,请参见http://www.codeproject.com/KB/wtl/wtl4mfc1.aspx#atltemplates