【问题标题】:c++ virtual function vs member function pointer (performance comparison)c++虚函数vs成员函数指针(性能对比)
【发布时间】:2013-06-25 01:11:19
【问题描述】:

由于虚拟调用需要对 v-table 进行额外的索引遵从,虚拟函数调用可能会很慢,这可能导致数据缓存未命中以及指令缓存未命中...不利于性能关键型应用程序。

所以我一直在想一种方法来克服虚函数的性能问题,但仍然具有虚函数提供的一些相同功能。

我相信以前已经这样做过,但我设计了一个简单的测试,允许基类存储一个可以由任何派生类设置的成员函数指针。当我在任何派生类上调用 Foo() 时,它将调用适当的成员函数,而无需遍历 v-table...

我只是想知道这种方法是否可以替代虚拟调用范式,如果可以,为什么它没有更普遍?

提前感谢您的宝贵时间! :)

class BaseClass
{
protected:

    // member function pointer
    typedef void(BaseClass::*FooMemFuncPtr)();
    FooMemFuncPtr m_memfn_ptr_Foo;

    void FooBaseClass() 
    {
        printf("FooBaseClass() \n");
    }

public:

    BaseClass()
    {
        m_memfn_ptr_Foo = &BaseClass::FooBaseClass;
    }

    void Foo()
    {
        ((*this).*m_memfn_ptr_Foo)();
    }
};

class DerivedClass : public BaseClass
{
protected:

    void FooDeriveddClass()
    {
        printf("FooDeriveddClass() \n");
    }

public:

    DerivedClass() : BaseClass()
    {
        m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDeriveddClass;
    }
};

int main(int argc, _TCHAR* argv[])
{
    DerivedClass derived_inst;
    derived_inst.Foo(); // "FooDeriveddClass()"

    BaseClass base_inst;
    base_inst.Foo(); // "FooBaseClass()"

    BaseClass * derived_heap_inst = new DerivedClass;
    derived_heap_inst->Foo();

    return 0;
}

【问题讨论】:

  • 1.在您提出此类问题之前,请先分析代码。你基本上说的是“为我分析代码”。 2.查找编译时多态性。
  • 旧但可能对您很感兴趣:codeproject.com/Articles/7150/…
  • 是的,我打算分析代码,但我很好奇性能上是否存在任何概念差异
  • “为什么它没有更普遍?”出于同样的原因,汇编语言和 Brainf*** 并不普遍......哦,而且速度更慢。
  • 您需要为每个对象存储一个函数指针而不是每个类一个函数指针(可能)节省一次缓存未命中失去不变性(即可预测性)的函数指针。我确信这种权衡已经被反复测量,以达到虚拟调用的通用实现。或者,所有 C++ 实现者都是傻瓜,根本没想过这样做,但我对此表示怀疑。

标签: c++ performance function-pointers virtual-functions vtable


【解决方案1】:

我做了一个测试,使用虚函数调用的版本在我的系统上经过优化后速度更快。

$ time ./main 1
Using member pointer

real    0m3.343s
user    0m3.340s
sys     0m0.002s

$ time ./main 2
Using virtual function call

real    0m2.227s
user    0m2.219s
sys     0m0.006s

代码如下:

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <stdio.h>

struct BaseClass
{
    typedef void(BaseClass::*FooMemFuncPtr)();
    FooMemFuncPtr m_memfn_ptr_Foo;

    void FooBaseClass() { }

    BaseClass()
    {
        m_memfn_ptr_Foo = &BaseClass::FooBaseClass;
    }

    void Foo()
    {
        ((*this).*m_memfn_ptr_Foo)();
    }
};

struct DerivedClass : public BaseClass
{
    void FooDerivedClass() { }

    DerivedClass() : BaseClass()
    {
        m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDerivedClass;
    }
};

struct VBaseClass {
  virtual void Foo() = 0;
};

struct VDerivedClass : VBaseClass {
  virtual void Foo() { }
};

static const size_t count = 1000000000;

static void f1(BaseClass* bp)
{
  for (size_t i=0; i!=count; ++i) {
    bp->Foo();
  }
}

static void f2(VBaseClass* bp)
{
  for (size_t i=0; i!=count; ++i) {
    bp->Foo();
  }
}

int main(int argc, char** argv)
{
    int test = atoi(argv[1]);
    switch (test) {
        case 1:
        {
            std::cerr << "Using member pointer\n";
            DerivedClass d;
            f1(&d);
            break;
        }
        case 2:
        {
            std::cerr << "Using virtual function call\n";
            VDerivedClass d;
            f2(&d);
            break;
        }
    }

    return 0;
}

编译使用:

g++ -O2    main.cpp   -o main

使用 g++ 4.7.2。

【讨论】:

  • 非常有趣.. 非常感谢您的分析!我想知道这与缓存中新鲜的 vtable 和指令有多大关系
  • 这也可能是因为虚拟表已经在 C++ 中使用了很长时间,因此编译器编写者已经学会了如何以及何时优化它们。与您的代码一样,编译器可以做出更少的假设并且必须做不太理想的事情。
【解决方案2】:

由于虚拟调用必须遍历 v-table,虚拟函数调用可能会很慢,

这不太正确。 vtable 应该在对象构造上计算,每个虚函数指针设置为层次结构中最专业的版本。调用虚函数的过程并不迭代指针,而是调用类似*(vtbl_address + 8)(args);的东西,它是在常数时间内计算出来的。

这可能导致数据缓存未命中以及指令缓存未命中...不适合性能关键型应用程序。

您的解决方案也不适用于性能关键型应用程序(通常),因为它是通用的。

通常,性能关键应用程序会根据具体情况进行优化(测量、挑选模块内性能问题最严重的代码并进行优化)。

使用这种按案例处理的方法,您可能永远不会遇到代码变慢的情况,因为编译器必须遍历 vtbl。如果是这种情况,那么缓慢可能来自通过指针而不是直接调用函数(即问题将通过内联来解决,而不是通过在基类中添加额外的指针来解决)。

无论如何,所有这些都是学术性的,直到您有一个具体的案例需要优化(并且您已经测量出最严重的问题是虚函数调用)。

编辑

我只是想知道这种方法是否可以替代虚拟调用范例,如果可以,为什么它没有更普遍?

因为它看起来像一个通用解决方案(无处不在地应用它会降低性能而不是提高它),解决一个不存在的问题(您的应用程序通常不会因为虚函数调用而变慢)。

【讨论】:

    【解决方案3】:

    虚拟函数不会“遍历”表,只需从某个位置获取一个指针并调用该地址。就好像您手动实现了一个指向函数的指针,并将其用于调用而不是直接调用。

    所以你的工作只适合混淆,破坏编译器可以发出非虚拟直接调用的情况。

    使用指向成员函数的指针可能比 PTF 更糟糕,它可能会使用相同的 VMT 结构进行类似的偏移访问,只是一个变量而不是固定的。

    【讨论】:

    • 他们必须额外获取 vptr,这可能会也可能不会导致加载相同的缓存行,具体取决于函数的作用。
    • 是的,但是缓存很大,代码很小。上下文切换确实比 v-table 威胁更大。
    • 推测缓存未命中不太可行,但要衡量。但是所提出的替代方案看起来至少具有相同数量的间接...
    • @PlasmaHH:在 VC 的另一个答案中查看实际测量结果
    【解决方案4】:

    主要是因为它不起作用。大多数现代 CPU 在分支预测和推测执行方面比您想象的要好。但是,我还没有看到 CPU 在非静态分支之外进行推测执行。

    此外,在现代 CPU 中,您更有可能发生缓存未命中,因为您在调用之前进行了上下文切换,并且另一个程序接管了缓存,而不是因为 v-table,即使这种情况是非常遥远的可能性。

    【讨论】:

    • 非常感谢您的回答,但您能解释一下“不起作用”是什么意思吗?对于我的具体示例,分支预测如何发挥作用?
    • 基本上它是一种被抢占的预优化,但事实上您是在现代非合作多任务操作系统上运行代码。此外,TLB 非常擅长预测接下来要使用的内容,并且因为 vTable 中的函数往往在同一个代码页中,所以它们几乎总是在缓存中。
    【解决方案5】:

    实际上,一些编译器可能会使用thunks,它本身会转换为普通的函数指针,所以基本上编译器会为您完成您手动尝试执行的操作(并且可能会迷惑人们)。

    另外,有一个指向虚函数表的指针,虚函数的空间复杂度是O(1)(只是指针)。另一方面,如果你在类中存储函数指针,那么复杂度是 O(N)(你的类现在包含的指针与“虚拟”函数一样多)。如果有很多函数,你就要为此付出代价——当预取你的对象时,你正在加载缓存行中的所有指针,而不是仅仅一个指针和你可能需要的前几个成员。听起来很浪费。

    另一方面,虚函数表针对一种类型的所有对象位于一个位置,并且可能永远不会在您的代码在循环中调用一些短虚函数时被推出缓存(这可能是问题当虚函数成本成为瓶颈时)。

    对于分支预测,在某些情况下,基于对象类型的简单决策树和每个特定类型的内联函数可以提供良好的性能(然后您存储类型信息而不是指针)。这并不适用于所有类型的问题,而且大多是过早的优化。

    根据经验,不要担心语言结构,因为它们看起来不熟悉。只有在您测量并确定了瓶颈的真正位置之后,才需要担心和优化。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-09-12
      • 2012-02-29
      • 1970-01-01
      • 1970-01-01
      • 2011-06-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多