为什么这个虚函数调用如此昂贵?

时间:2017-01-19 21:41:39

标签: c++ c++11 performance-testing virtual-functions

我最近修改了一个程序来使用虚函数(如果if-else条件与静态调用一起代替序列。)修改后的程序运行速度比原始程序慢8%。这似乎是使用虚函数的成本太高,所以我必须在设置类层次结构和虚函数的方式上做一些低效的事情;但是,我对如何追查问题感到茫然。 (我在Mac上使用clang和在Linux上使用gcc时看到类似的性能下降。)

该程序用于研究不同的社区检测算法。该程序使用嵌套循环将一系列用户指定的目标函数应用于各种(图形,分区)对。

以下是原始代码的粗略轮廓

int main(int argc, char* argv[]) {
    bool use_m1;
    bool use_m2;
    ...
    bool use_m10;

    //  set the various "use" flags based on argv

    for (Graph& g : graphsToStudy()) {
        for (Partition& p : allPartitions()) {
            if (use_m1) {
                M1::evaluate(g, p);
            }
            if (use_m2) {
                M2::evaluate(g,p);
            }
            // and so on
        }
    }

为了使代码更易于维护,我为不同的目标函数创建了一个类结构,并通过指针数组进行迭代:

class ObjectiveFunction {
public:
    virtual double eval(Graph& g, Partition& p) = 0;
}

class ObjFn1 : public ObjectiveFunction {
public:
    virtual double eval(Graph& g, Partition& p) {
        return M1::evaluate(g,p);
   }
}

class ObjFn2 : public ObjectiveFunction {
public:
    virtual double eval(Graph& g, Partition& p) {
        return M2::evaluate(g,p);
   }
}


int main(int argc, char* argv[]) {
    vector<ObjectiveFunction*> funcs;
    fill_funcs_based_on_opts(funcs, argc, argv);

    for (Graph& g : graphsToStudy()) {
        for (Partition& p : allPartitions()) {
            // funcs contains one object for each function selected by user.
            for (ObjectiveFunction* fp : funcs) {
                fp->evaluate(g, p);
            }
        }
    }

鉴于生成图形和分区以及目标函数本身是中等计算密集型的,虚拟函数调用的添加应该几乎不可察觉。任何想法我可能做错了;或者如何追踪它?我尝试使用callgrind,但没有看到任何见解。

也许我只是错误地解释了callgrind_annotate的输出。在下面的示例中,Neo::Context::evaluatePartition类似于上例中的ObjFn1::evaluate

  1. 为什么此功能列出四次不同的时间 源文件?只从函数main调用此方法 在timeMetrics.cpp

  2. src/lib/PartitionIterator.h:main指的是什么?没有     PartitionIterator.h中的主要功能。

  3. 为什么414,219,420在源代码清单中出现两次     evaluatePartition?不是第一个应该代表的数字     函数调用的开销?

  4. 35,139,513,913  PROGRAM TOTALS
    17,029,020,600  src/lib/metrics/Neo.h:gvcd::metrics::Neo::Context<unsigned int, unsigned char, unsigned int>::evaluatePartition(gvcd::Partition<unsigned int, unsigned int> const&, bool)  [bin/timeMetrics_v]  
    7,168,741,865  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/vector:gvcd::Partition<unsigned int, unsigned int>::buildMembersh ipList()  
    4,418,473,884  src/lib/Partition.h:gvcd::Partition<unsigned int, unsigned int>::buildMembershipList() [bin/timeMetrics_v]  
    1,459,239,657  src/lib/PartitionIterator.h:main  
    1,288,682,640  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/vector:gvcd::metrics::Neo::Context<unsigned int, unsigned char, u nsigned int>::evaluatePartition(gvcd::Partition<unsigned int, unsigned int> const&, bool)  
    1,058,560,740  src/lib/Partition.h:gvcd::metrics::Neo::Context<unsigned int, unsigned char, unsigned int>::evaluatePartition(gvcd::Partition<unsigned int, unsigned int> const&, bool)  
    1,012,736,608  src/perfEval/timeMetrics.cpp:main [bin/timeMetrics_v]    443,847,782  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/vector:main 
    368,372,912  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:gvcd::Partition<unsigned int, unsigned int>::buildMembersh ipList()    
    322,170,738  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/ostream:main
        92,048,760  src/lib/SmallGraph.h:gvcd::metrics::Neo::Context<unsigned int, unsigned char, unsigned int>::evaluatePartition(gvcd::Partition<unsigned int, unsigned int> const&, bool)
        84,549,144  ???:szone_free_definite_size [/usr/lib/system/libsystem_malloc.dylib]
        54,212,938  ???:tiny_free_list_add_ptr [/usr/lib/system/libsystem_malloc.dylib]
    
    
    
                .          virtual double
      414,219,420          evaluatePartition(const Partition <VertexID, SetBitmap> &p, bool raw = false) {
      414,219,420            uint_wn_t raw_answer = Neo::evaluatePartition(*(this->g), p);
                .            return (double) (raw ? raw_answer : max_neo - raw_answer);
                .          }
                .        }; // end Context
    

3 个答案:

答案 0 :(得分:2)

让我们先解决明显的问题:

在这两个版本中,您都可以这样做:

foreach (Graph g : graphsToStudy()) {
    foreach (Partition p : allPartitions()) {

除非图表/分区很容易复制,否则你的大部分工作都会在这里。

foreach (Graph& g : graphsToStudy()) {
           // ^
    foreach (Partition& p : allPartitions()) {
                   // ^

我的第二个问题。这似乎不是虚函数的正确用法。在这个用例中,您的原始代码看起来完全正常,在每个evaluate()对象对上调用了多个版本的(g, p)

现在如果你只调用evaluate()函数中的每一个,那么它可能是一个更好的用例,但是你不再需要那个内循环:

 foreach (ObjectiveFunction* fp : funcs) {

答案 1 :(得分:1)

它很昂贵,因为你实际上使用多态,这会破坏分支预测器。

如果用内部链表替换集合迭代,它可能有助于分支预测器:

class ObjectiveFunction
{
    ObjectiveFunction* m_next;
    virtual double evaluate(Graph& g, Partition& p) = 0;

  protected:
    ObjectiveFunction(ObjectiveFunction* next = nullptr) : m_next(next) {}

    // for gcc use __attribute__((always_inline))
    // for MSVC use __forceinline
    void call_next(Graph& g, Partition& p)
    {
        if (m_next) m_next->eval(g, p);
    }
  public:
    virtual void eval(Graph& g, Partition& p) = 0;
};

现在,代替循环中的一行代码到达许多不同的函数,call_next()函数(应该是每个eval重载的最后一步)应该内联到每个函数中重载,并且在运行时,该间接调用指令的每个内联副本将重复调用一个函数,从而导致100%的分支预测。

答案 2 :(得分:0)

尽我所能,我更喜欢静态而不是动态调度 - 动态调度可能会因为阻止函数内联等优化而损失成本,而且与vtable相关的双重引用可能会导致局部性不佳(指令缓存未命中)。

我怀疑狮子在性能上的不同之处在于失去了对静态调度执行优化的好处。尝试禁用原始代码的内联可能会很有趣,看看你享受了多少好处。