C++中的人工智能应用:虚拟功能的成本有多高?可能的优化是什么

AI Applications in C++: How costly are virtual functions? What are the possible optimizations?

本文关键字:是什么 优化 应用 人工智能 虚拟 C++ 功能      更新时间:2023-10-16

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

  1. 没有太多的数值计算
  2. 有很多结构需要运行时多态性
  3. 在计算过程中,经常会有几个多态结构相互作用

在这种情况下,有什么优化技术吗?虽然我现在不想优化应用程序,但在项目中选择C++而不是Java的一个方面是为了能够更好地进行优化,并能够使用非面向对象的方法(模板、过程、重载)。

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

除此之外,C++中的数据本地化技术是否有很好的参考?这些技术将减少计算所需的数据提取到二级缓存的等待时间。

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

虚拟功能非常高效。假设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数组完全相同的内存位置。调用不同的实例不会将不同的内存位置拉入二级缓存。

msdn中的这个例子显示了类A的vtable,其中包含虚拟func1、func2和func3。不超过12个字节。在编译的库中,不同类的vtables也很有可能在物理上相邻(您需要验证这一点,这是您特别关心的),这可以从微观上提高缓存效率。

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在位置[aax]加载了该值,因此具有类A vtable。然后用[ebp]加载ecx,因为ecx表示"this",它现在保存A*,最后调用位置[edx+8]处的值,这是vtable中的第三个函数地址。

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

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

您是否真正分析并找到了需要优化的位置和内容?

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

我能想到的唯一优化是Java的JIT编译器。如果我理解正确,它会在代码运行时监视调用,如果大多数调用只转到特定的实现,那么当类正确时,它会插入条件跳转到实现。通过这种方式,大多数时候都没有vtable查找。当然,在极少数情况下,当我们通过一个不同的类时,仍然使用vtable。

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

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

优化通常围绕着在调用堆栈的更高层表达可变性,这样您就不需要在热点中多次调用虚拟函数。

您可以在运行时使用虚拟函数和在编译时使用模板来实现polymorfism。您可以用模板替换虚拟函数。查看本文了解更多信息-http://www.codeproject.com/KB/cpp/SimulationofVirtualFunc.aspx

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

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

维基百科上的解释很清楚,如果你真的确定虚拟方法调用是性能瓶颈的来源,也许它可以帮助你

正如其他答案所述,虚拟函数调用的实际开销相当小。它可能会在每秒被调用数百万次的闭环中产生影响,但这很少是什么大事。

然而,它可能仍然有更大的影响,因为编译器更难优化。它无法内联函数调用,因为它在编译时不知道将调用哪个函数。这也使得一些全局优化变得更加困难。这需要多少性能?这取决于情况。这通常没什么好担心的,但在某些情况下,这可能意味着性能会受到重大影响。

当然,这也取决于CPU架构。对一些人来说,它可能会变得相当昂贵。

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

优化这一点的唯一可靠方法是将部分工作转移到编译时。如果可以将它的一部分实现为静态多态性,那么可能会有一些加速。

但首先,确保你有问题。代码是否真的太慢而无法接受?其次,通过探查器找出是什么导致它变慢。第三,修复它。

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

  • 如果你实际上并不知道这是一个问题,那么任何关于修复它的担忧都可能是错误的

你想知道的是:

  • 在调用方法的过程中花费了多少执行时间(当它实际运行时),特别是哪些方法成本最高(按此衡量)

一些评测程序可以间接地为您提供这些信息。它们需要在语句级别进行总结,但不包括在方法本身中花费的时间。

我最喜欢的技术是在调试器下暂停它多次。

如果在虚拟函数调用过程中花费的时间很长,比如说20%,那么平均五分之一的样本将在调用堆栈的底部,在反汇编窗口中显示用于遵循虚拟函数指针的指令。

如果你没有真正看到这一点,那就不是问题。

在这个过程中,您可能会在调用堆栈的更高层看到其他东西,这些东西实际上是不需要的,可以为您节省大量时间。

静态多态性,正如一些用户在这里回答的那样。例如,WTL使用这种方法。有关WTL实现的详细说明,请访问http://www.codeproject.com/KB/wtl/wtl4mfc1.aspx#atltemplates

虚拟调用不会比正常函数带来更大的开销。尽管如此,最大的损失是以多态方式调用虚拟函数时无法内联。在很多情况下,内联将代表性能上的一些实际增益。

在某些情况下,您可以做一些事情来防止该功能的浪费,那就是将函数声明为内联虚拟函数。

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实现。但是,只有当您确信实例类型是什么时,才能执行此操作,因为自动多态性功能将消失,而这对以后的代码读取器来说并不明显。

您很少需要担心此类常用项的缓存问题,因为它们会被提取一次并保存在那里。

缓存通常只在处理以下大型数据结构时才是一个问题:

  1. 足够大,并且由单个函数使用很长一段时间,以便该函数可以将您需要的所有其他内容从缓存中推出,或者
  2. 随机访问的次数足够多,以至于从中加载时数据结构本身不一定在缓存中

像Vtables这样的东西通常不会成为性能/缓存/内存问题;通常每个对象类型只有一个Vtable,并且该对象包含指向Vtable的指针,而不是指向Vtable本身。所以,除非你有几千种类型的对象,否则我不认为Vtables会破坏你的缓存。

1) 顺便说一句,这就是为什么像memcpy这样的函数使用缓存绕过movnt(dq|q)这样的流指令来处理超大(兆字节)数据输入的原因。

对于最近的CPUS,成本与现在的正常函数大致相同,但它们不能内联。如果你调用该函数数百万次,影响可能会很大(试着调用同一个函数数百万次(例如,一次带inline,一次不带inline),你会发现如果函数本身做一些简单的事情,它可能会慢两倍;这不是理论上的情况:它在很多数值计算中都很常见)。

使用现代、前瞻性的多调度CPU,虚拟功能的开销可能为零。纳达。拉链

如果AI应用程序不需要大量的数字运算,我就不会担心虚拟函数的性能劣势。只有当它们出现在重复评估的复杂计算中时,才会对性能产生边际影响。我认为您也不能强制虚拟表留在二级缓存中。

有几个可用于虚拟功能的优化,

  1. 人们已经编写了编译器,这些编译器采用代码分析和程序转换。但是,这些不是生产级编译器
  2. 您可以将所有虚拟函数替换为等效的"switch…case"块,以便根据层次结构中的类型调用适当的函数。通过这种方式,您将摆脱编译器管理的虚拟表,并以交换机的形式拥有自己的虚拟表。。。案例块。现在,您自己的虚拟表在二级缓存中的几率很高,因为它在代码路径中。记住,您需要RTTI或您自己的"typeof"函数来实现这一点