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

C++虚拟函数与成员函数指针(性能比较),c++,performance,function-pointers,virtual-functions,vtable,C++,Performance,Function Pointers,Virtual Functions,Vtable,虚拟函数调用可能会很慢,因为虚拟调用需要对v表进行额外的索引遵从,这可能会导致数据缓存未命中以及指令缓存未命中。。。不适用于性能关键型应用程序 因此,我一直在想一种方法来克服虚拟函数的性能问题,但仍然具有虚拟函数提供的一些相同的功能 我相信这以前已经做过,但我设计了一个简单的测试,允许基类存储一个成员函数指针,该指针可以由派生类中的任何一个设置。当我在任何派生类上调用Foo时,它将调用适当的成员函数,而不必遍历v-table 我只是想知道这种方法是否是虚拟调用范例的可行替代品,如果是,为什么它不

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

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

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


我只是想知道这种方法是否是虚拟调用范例的可行替代品,如果是,为什么它不是更普遍

提前感谢您抽出时间!:

类基类 { 受保护的: //成员函数指针 typedef voidBaseClass::*FooMemFuncPtr; FooMemFuncPtr m_memfn_ptr_Foo; 空基类 { printfoobseclass\n; } 公众: 基类 { m_memfn_ptr_Foo=&BaseClass::FooBaseClass; } 福娃 { *这个。*m_memfn_ptr_Foo; } }; 类派生类:公共基类 { 受保护的: void foodderiveddclass { PrintFfooderivedClass\n; } 公众: 派生类:基类 { m_memfn_ptr_Foo=FooMemFuncPtr&DerivedClass::fooderidvedclass; } }; 整数main argc,_TCHAR*argv[] { 派生类派生工具; 派生的_inst.Foo;//FooDeriveddClass 基类基类仪器; base_inst.Foo;//FooBaseClass 基类*派生的堆安装=新的派生类; 派生的\u heap\u inst->Foo; 返回0; }
虚拟函数不遍历表,只需从某个位置提取指针并调用该地址。这就好像您有一个指向function的指针的手动实现,并将其用于调用,而不是直接调用

因此,您的工作只适用于模糊处理,并破坏编译器可以发出非虚拟直接调用的情况


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

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

因此,您的工作只适用于模糊处理,并破坏编译器可以发出非虚拟直接调用的情况


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

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


此外,在现代CPU中,由于在调用之前有一个上下文切换,而另一个程序接管了缓存,因此缓存丢失的可能性更大,甚至这种情况也很可能发生。

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

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

虚拟函数调用可能会很慢,因为虚拟调用必须遍历v表

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

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

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

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

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

无论如何,所有这些都是学术性的,直到你有一个具体的案例需要优化,并且你已经衡量出你最糟糕的问题是虚拟函数调用

编辑:


我只是想知道这种方法是否是虚拟调用范例的可行替代品,如果是,为什么它不是更普遍

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

虚拟函数调用可能会很慢,因为虚拟调用必须遍历v表

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

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

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

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

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

无论如何,所有这些都是学术性的,直到你有一个具体的案例需要优化,并且你已经衡量出你最糟糕的问题是虚拟函数调用

编辑:


我只是想知道这种方法是否是虚拟调用范例的可行替代品,如果是,为什么它不是更普遍


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

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

$ 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++4.7.2.

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

$ 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++4.7.2中。

实际上有些编译器可能会使用,这些编译器本身会转换为普通的函数指针,因此基本上编译器会为您手动执行您试图执行的操作,并且可能会让很多人感到困惑

另外,如果有一个指向虚函数表的指针,那么虚函数的空间复杂度就是指针。另一方面,如果您在类中存储函数指针,那么复杂性在于您的类现在包含的指针数量与虚拟函数的数量相同。如果有很多函数,你就要为此付出代价——在预取对象时,你要加载缓存线中的所有指针,而不是一个指针和你可能需要的前几个成员。听起来像是浪费

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

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

$ 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

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

事实上,有些编译器可能会使用它,它们本身会转换为普通的函数指针,因此基本上编译器会为您手动执行您试图执行的操作,并且可能会让很多人感到困惑

另外,如果有一个指向虚函数表的指针,那么虚函数的空间复杂度就是指针。另一方面,如果您在类中存储函数指针,那么复杂性在于您的类现在包含的指针数量与虚拟函数的数量相同。如果有很多函数,你就要为此付出代价——在预取对象时,你要加载缓存线中的所有指针,而不是一个指针和你可能需要的前几个成员。听起来像是浪费

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

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

$ 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


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

1。在您提出类似问题之前,请先分析代码。你基本上是说为我分析代码。2.查找编译时多态性。虽然很老,但对您来说可能很有趣:是的,我计划分析代码,但我很好奇在性能上是否有任何概念上的差异为什么它不是更普遍?因为同样的原因,汇编语言和Brainf***并不普遍。。。哦,而且速度要慢一些。通过为每个对象存储一个函数指针,而不是为每个类存储一个函数指针,以及失去不变性(即函数指针的可预测性),您将为可能节省一次缓存未命中付出代价。我确信,为了实现虚拟调用的通用实现,已经反复衡量了这种权衡。或者,所有C++实现者都是不知道这样做的想法,但我不知怎么怀疑。1。在您提出类似问题之前,请先分析代码。你基本上是说为我分析代码。2.查找编译时多态性。虽然很老,但对您来说可能很有趣:是的,我计划分析代码,但我很好奇在性能上是否有任何概念上的差异为什么它不是更普遍?因为同样的原因,汇编语言和Brainf***并不普遍。。。哦,而且速度要慢一些。通过为每个对象存储一个函数指针,而不是为每个类存储一个函数指针,以及失去不变性(即函数指针的可预测性),您将为可能节省一次缓存未命中付出代价。我确信,为了实现虚拟调用的通用实现,已经反复衡量了这种权衡。或者,所有C++实现者都是不知道这样做的想法,但是我不知怎么地怀疑。他们必须另外获取VPTR,这可能会或可能不会导致相同的缓存行被加载,这取决于函数是什么。上下文切换确实比v形表更具威胁性。推测缓存未命中的可能性不是很高,但需要进行测量。但是给出的备选方案看起来至少具有相同数量的间接寻址…@Plasmah:请参阅VCTR另一个答案中的实际测量结果。他们必须额外获取vptr,这可能会导致加载相同的缓存线,也可能不会导致加载相同的缓存线,具体取决于函数的功能。true,但缓存较大,代码较小。。上下文切换确实比v形表更具威胁性。推测缓存未命中的可能性不是很高,但需要进行测量。但目前的备选方案看起来至少有相同数量的间接性…@Plasmah:请参阅VCA另一个答案中的实际测量值非常感谢您的回答,但是,你能解释一下什么是它不起作用,分支预测是如何在我的具体例子中发挥作用的吗?基本上,它是一个预先优化,被抢占,但事实上,你正在一个现代的非合作多任务操作系统上运行代码。此外,TLB非常擅长预测下一步将使用什么,因为vTable中的函数往往位于同一个代码页中,它们几乎总是在缓存中。非常感谢您的回答,但是,你能解释一下什么是它不起作用,分支预测是如何在我的具体例子中发挥作用的吗?基本上,它是一个预先优化,被抢占,但事实上,你正在一个现代的非合作多任务操作系统上运行代码。此外,TLB非常擅长预测下一步将使用什么,而且由于vTable中的函数往往位于同一个代码页中,它们几乎总是位于缓存中。非常有趣。。非常感谢您分析它!我想知道这与VCH和指令在CACHE中的新鲜程度有什么关系,这也可能是因为虚拟表已经在C++中已经很长时间了,因此编译器编写者已经学会了如何和何时优化它们。与你的代码一样,编译器可以做更少的假设,并且必须做更少的优化。。非常感谢您分析它!我想知道这与vtable和正在使用的指令有多大关系
在CaseCH中,这也可能是由于虚拟表已经在C++中很长时间,因此编译器编写者已经学会了如何以及何时优化它们。与您的代码一样,编译器可以做出更少的假设,并且必须做不太优化的事情。