C ++中的AI应用程序:虚拟功能的成本是多少?有哪些可能的优化?

时间:2008-10-01 04:41:50

标签: c++ optimization

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

  1. 没有太多的数值计算
  2. 有许多需要运行时多态性的结构
  3. 经常会有几个多态结构在计算过程中相互作用
  4. 在这种情况下,有没有优化技术?虽然我现在不打算优化应用程序,但为项目选择C ++而不是Java的一个方面是提供更多的优势,以便能够使用非面向对象的方法(模板,过程,重载)。

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

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

    更新:另请参阅以下相关论坛:Performance Penalty for InterfaceSeveral Levels of Base Classes

15 个答案:

答案 0 :(得分:28)

虚拟功能非常高效。假设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缓存。

这个example from 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函数的指令开销。这也非常有效。几乎与调用非虚函数相同。再次来自example from 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。然后ecx加载[ebp],因为ecx表示“this”它现在保持A *,最后调用位置[edx + 8]的值,这是vtable中的第三个函数地址。

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

答案 1 :(得分:11)

draft Technical Report on C++ Performance的第5.3.3节完全专注于虚函数的开销。

答案 2 :(得分:3)

您是否实际剖析并找到了哪里以及需要优化的内容?

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

答案 3 :(得分:3)

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

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

答案 4 :(得分:3)

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

优化通常围绕在callstack中表达更高的可变性,因此您不需要在热点内多次调用虚函数。

答案 5 :(得分:2)

您可以使用虚函数在运行时实现多态性,并在编译时使用模板实现多态性。您可以使用模板替换虚拟功能。有关详细信息,请参阅此文章 - http://www.codeproject.com/KB/cpp/SimulationofVirtualFunc.aspx

答案 6 :(得分:2)

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

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

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

答案 7 :(得分:2)

虚拟调用与正常函数相比不会产生更大的开销。虽然,最大的损失是,多态调用时的虚函数无法内联。在许多情况下,内联将代表性能的一些实际收益。

在某些情况下,您可以采取措施防止浪费该设施,即将内联虚拟函数声明为。

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()的调用将变为非多态并绑定到B foo()的实现。但只有当您确切知道实例类型是什么时才这样做,因为自动多态性功能将会消失,这对于后来的代码阅读器来说并不是很明显。

答案 8 :(得分:2)

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

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

您想知道的是:

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

某些分析器可以间接地向您提供此信息。他们需要在声明级别进行总结,但不包括在方法本身中花费的时间。

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

如果在虚函数调用过程中花费的时间很重要,比如说20%,则平均每5个样本中就有1个会在调用堆栈的底部显示,在反汇编窗口中,指示跟随虚函数指针。

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

在这个过程中,您可能会看到调用堆栈中更高的其他内容,实际上并不需要它们,可以为您节省大量时间。

答案 9 :(得分:2)

正如其他答案所述,虚函数调用的实际开销相当小。它可能会在一个紧密的循环中发挥作用,它被称为每秒数百万次,但它很少是一个大问题。

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

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

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

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

但首先,请确保您遇到问题。代码实际上是否太慢而无法接受? 其次,找出通过分析器使它变慢的原因。 第三,解决它。

答案 10 :(得分:2)

静态多态,正如一些用户在这里回答的那样。例如,WTL使用此方法。可以在http://www.codeproject.com/KB/wtl/wtl4mfc1.aspx#atltemplates

找到有关WTL实现的明确说明

答案 11 :(得分:1)

你很少担心这些常用物品的缓存,因为它们被取出并保存在那里。

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

  1. 足够大并且通过单个函数使用了很长时间,因此该函数可以将所需的所有内容从缓存中推出,或者
  2. 随机访问,以便在从中加载时数据结构本身不一定在缓存中。
  3. 像Vtables这样的东西通常不会成为性能/缓存/内存问题;通常每个对象类型只有一个Vtable,并且该对象包含指向Vtable而不是Vtable本身的指针。因此,除非你有几千种类型的对象,否则我认为Vtables不会破坏你的缓存。

    顺便说一下,

    1)就像memcpy这样的函数使用缓存绕过流指令(如movnt(dq | q))来处理超大(兆字节)的数据输入。

答案 12 :(得分:1)

现在最近的CPUS成本与正常功能大致相同,但不能内联。如果你将函数调用数百万次,那么影响可能很大(尝试调用数百万次相同的函数,例如,一次没有内联一次,如果函数本身做的很简单,你会发现它可能慢两倍;这个不是一个理论上的例子:很多数值计算很常见。)

答案 13 :(得分:1)

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

答案 14 :(得分:0)

如果 AI 应用程序不需要大量的数字运算,我不会担心虚拟功能的性能劣势。只有当它们出现在重复计算的复杂计算中时,才会出现边际性能损失。我认为您不能强制虚拟表保留在L2缓存中。

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

  1. 人们编写了编写代码分析和程序转换的编译器。但是,这些不是生产级编译器。
  2. 您可以使用等效的“switch ... case”块替换所有虚函数,以根据层次结构中的类型调用适当的函数。这样你就可以摆脱编译器管理的虚拟表,你将以switch ... case块的形式拥有自己的虚拟表。现在,您自己的虚拟表在L2缓存中的可能性很高,就像在代码路径中一样。请记住,您需要RTTI或您自己的“typeof”功能来实现这一目标。