优化C++中的函数指针和虚函数

optimising function pointers and virtual functions in C++

本文关键字:函数 指针 C++ 优化      更新时间:2023-10-16

我知道以前已经问过很多关于这些主题的问题,但我有一个特定的情况,速度(适度(重要,使用函数指针而不是虚函数时的速度提高约为 25%。我想知道(主要是出于学术原因(为什么?为了提供更多细节,我正在编写一个由一系列单元格组成的模拟。细胞通过链接连接,这些链接决定了细胞之间的相互作用。Link 类有一个名为 update(( 的虚拟函数,它使它链接的两个 Cell 进行交互。它是虚拟的,因此我可以创建不同类型的链接来提供不同类型的交互。例如,目前我正在模拟无粘性流动,但我可能需要一个具有粘度或应用边界条件的链接。实现相同效果的第二种方法是将函数指针传递给 Link 类,并使函数指针的目标成为友元。我现在可以有一个使用函数指针的非虚拟 update((。派生类可以使用指向不同函数的指针,从而提供多态行为。

当我使用 Very Sleepy 构建两个版本和配置文件时,我发现函数指针版本明显快于虚函数版本,并且 Link 类似乎已完全优化 - 我只看到从我的主函数到指向的函数的调用。

我只是想知道是什么让我的编译器(MSVC++ 2012 Express(比虚拟函数大小写更容易优化函数指针大小写?

下面的一些代码如果它对函数指针情况有帮助,我相信很明显如何用虚函数完成等效。

void InviscidLinkUpdate( void * linkVoid )
{
    InviscidLink * link=(InviscidLink*)linkVoid;
    //do some stuff with the link
    //e.g.
    //link->param1=
}
void ViscousLinkUpdate( void * linkVoid )
{
    ViscousLink * link=(ViscousLink*)linkVoid;
    //do some stuff with the link
    //e.g.
    //link->param1=
}
class Link
{
public:
    Link(Cell *cell1, Cell*cell2, float area, float timeStep, void (*updateFunc)( void * ))
        :m_cell1(cell1), m_cell2(cell2), m_area(area), m_timeStep(timeStep), m_update(updateFunc)
    ~Link(){};
    void update() {m_update( this );}
protected:
    void (*const m_update)( void *, UNG_FLT );
    Cell *m_cell1;
    Cell *m_cell2;
    float m_area;
    float m_timeStep
    //some other parameters I want to modify in update()
    float param1;
    float param2;
};
class InviscidLink : public Link
{
    friend void InviscidLinkUpdate( void * linkVoid )
public:
    InviscidLink(Cell *cell1, Cell*cell2, float area, float timeStep)
        Link(cell1, cell2, area, timeStep, InvicedLinkUpdate)
    {}
};
class ViscousLink : public Link
{
    friend void ViscousLinkUpdate( void * linkVoid )
public:
    ViscousLink(Cell *cell1, Cell*cell2, float area, float timeStep)
        Link(cell1, cell2, area, timeStep, ViscousLinkUpdate)
    {}
};

编辑

我现在已经把完整的源代码放在GitHub上 - https://github.com/philrosenberg/ung将提交 5ca899d39aa85fa3a86091c1202b2c4cd7147287(函数指针版本(与提交 aff249dbeb5dfdbdaefc8483becef53053c4646f(虚拟函数版本(进行比较。不幸的是,我最初将测试项目基于 wxWidgets 项目,以防我想玩一些图形显示,所以如果你没有 wxWidgets,那么你需要将其破解到命令行项目中来编译它。我用非常困来基准测试它

进一步编辑:

milianw关于配置文件引导优化的评论被证明是解决方案,但作为评论,我目前无法将其标记为答案。使用具有按配置优化的Visual Studio专业版提供了与使用内联函数类似的运行时。我想这是 http://msdn.microsoft.com/en-us/library/e7k32f4k.aspx 描述的虚拟呼叫推测。我仍然觉得使用函数指针而不是虚函数更容易优化这段代码有点奇怪,但我想这就是为什么每个人都建议 TEST 而不是假设某些代码比另一个代码更快。

在使用函数指针与虚函数时,我可以想到的两件事有所不同:

  • 您的班级规模会更小,因为它不会分配 vftable,因此规模更小,缓存更友好
  • 函数指针少了一个间接寻址(对于虚函数:对象间接、vftable 间接寻址、虚拟函数间接寻址,带函子:对象间接寻址、函子间接寻址 ->更新函数在编译时解析,因为它不是虚拟的(

按照要求,在这里我再次发表评论作为答案:

尝试在此处使用按配置文件优化。然后,探查器可能会应用去虚拟化来加快代码速度。另外,不要忘记将您的实现标记为最终实现,这可以进一步提供帮助。例如,请参阅 http://channel9.msdn.com/Shows/C9-GoingNative/C9GoingNative-12-C-at-BUILD-2012-Inside-Profile-Guided-Optimization 或 http://hubicka.blogspot.de/search/label/devirtualization 的优秀海湾合作委员会文章系列。

虚拟函数调用的实际成本通常微不足道。但是,正如您所观察到的,虚函数可能会对代码速度产生很大影响。主要原因是通常虚函数调用是实函数调用 - 向堆栈添加一个帧。之所以如此,是因为虚拟函数调用是在运行时解析的。

如果函数不是虚拟的,则编译器C++内联它要容易得多。调用在编译时解析,因此编译器可能会将调用替换为被调用函数的主体。这允许更积极的优化 - 例如只执行一次计算而不是每次循环运行等。

根据此处提供的信息,我的最佳猜测是,您正在对大量对象进行操作,并且虚拟表引起的一个额外的间接寻址正在增加缓存未命中数,以至于从内存中重新获取的 I/O 变得可测量。

作为另一种替代方法,您是否考虑过对Link类使用模板和 CRTP 或基于策略的方法?根据您的需要,可以完全删除动态调度。