繁体   English   中英

C ++虚拟函数与成员函数指针(性能比较)

[英]c++ virtual function vs member function pointer (performance comparison)

由于虚拟调用需要对v表进行额外的索引引用,因此虚拟函数调用的速度可能会很慢,这可能导致数据高速缓存未命中以及指令高速缓存未命中。

因此,我一直在思考一种克服虚拟功能性能问题的方法,但仍具有虚拟功能所提供的某些功能。

我相信以前已经做过,但是我设计了一个简单的测试,该测试允许基类存储可由任何派生类设置的成员函数指针。 当我在任何派生类上调用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;
}

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

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

由于虚拟调用必须遍历v表,因此虚拟函数调用可能会变慢,

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

这可能会导致数据高速缓存未命中以及指令高速缓存未命中。

您的解决方案也不适合(通常)对性能至关重要的应用程序,因为它是通用的。

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

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

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

编辑

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

因为它看起来像一个通用的解决方案(普遍应用会降低性能而不是提高性能),所以解决了一个不存在的问题(由于虚拟函数调用,您的应用程序通常不会减慢速度)。

虚函数不会“遍历”表,而只是从某个位置一次获取指针并调用该地址。 就像您已经手动实现指向功能的指针,并将其用于呼叫而不是直接呼叫一样。

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

使用指针到成员的功能可能比PTF还要糟糕,它可能对相同的偏移访问使用相同的VMT结构,只是一个变量而不是固定变量。

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

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

实际上,某些编译器可能会使用thunkthunk本身会转换为普通的函数指针,因此,基本上,编译器会为您完成您要手动执行的操作(并可能使人陷入困境)。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM