c ++虚函数vs成员函数指针(性能比较)

时间:2013-06-27 13:39:20

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

由于虚拟调用需要对v表进行额外的索引干扰,虚拟函数调用可能会很慢,这可能导致数据高速缓存未命中以及指令高速缓存未命中...对性能关键应用程序不利。< / p>

所以我一直在想办法克服虚拟功能的性能问题,但仍然具有虚拟功能提供的一些功能。

我确信之前已经完成了这项工作,但我设计了一个简单的测试,允许基类存储可由任何派生类设置的成员函数指针。当我在任何派生类上调用Foo()时,它将调用相应的成员函数而不必遍历v表...

我只是想知道这种方法是否可以替代虚拟呼叫范例,如果是这样,为什么它不是更普遍?

提前感谢您的时间! :)

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;
}

5 个答案:

答案 0 :(得分:3)

我做了一个测试,使用虚拟函数调用的版本在我的系统上进行优化更快。

$ 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。

答案 1 :(得分:2)

  

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

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

  

可能导致数据高速缓存未命中以及指令高速缓存未命中...对性能关键应用程序不利。

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

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

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

无论如何,所有这些都是学术性的,直到你有一个具体案例要进行优化(并且你已经测量过你最糟糕的罪犯是虚函数调用)。

修改

  

我只是想知道这种方法是否可以替代虚拟呼叫范例,如果是这样,为什么它不是更普遍?

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

答案 2 :(得分:1)

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

因此,您的工作仅适用于混淆,并破坏编译器可以发出非虚拟直接调用的情况。

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

实际上有些编译器可能会使用thunks,它会转换为普通的函数指针本身,所以基本上编译器会为你做的是你手动做的事情(并且可能会混淆地狱之人)。

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

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

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

根据经验,不要担心语言结构,因为它们似乎不熟悉。只有在测量并确定了瓶颈所在的位置后才会担心和优化。