1

由于虚拟调用需要对 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;
}
4

5 回答 5

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。

于 2013-06-27T14:02:27.537 回答
2

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

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

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

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

作为一项规则,性能关键应用程序会在每个案例的基础上进行优化(测量、选择模块内性能问题最差的代码并进行优化)。

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

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

编辑

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

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

于 2013-06-27T13:58:08.960 回答
1

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

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

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

于 2013-06-27T13:43:03.923 回答
0

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

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

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

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

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

于 2015-02-18T20:36:22.127 回答
0

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

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

于 2013-06-27T13:46:11.760 回答