【问题标题】:Why is this virtual function call so expensive?为什么这个虚函数调用如此昂贵?
【发布时间】:2017-06-04 18:25:25
【问题描述】:

我最近修改了一个程序以使用虚函数(代替序列 if 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中的main函数。

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


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

【问题讨论】:

  • 告诉我你没有#define foreach for...
  • 没有。这个例子不是来自我的真实代码,因为它有很多模板,所以要复杂得多。在回到 C++ 之前,我教了 Java 多年。旧习惯很难改掉:)
  • 问题很可能是你用静态调用代替了分支预测器的好处,而动态调用是两个间接的。您的虚拟功能正在浪费 CPU 周期,而分支则没有。
  • 也许不要复制不需要复制的东西?
  • 你应该看到它。你认为Graph g : graphsToStudy() 在做什么?

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


【解决方案1】:

在可能的情况下,我更喜欢静态而不是动态分派——动态分派可能会因阻止函数内联等优化而使您付出代价,并且由于 vtable 涉及的双重取消引用,您可能会遭受局部性不佳(指令缓存未命中)的困扰。

我怀疑性能差异的最大部分是由于失去了对静态调度执行的优化的好处。尝试禁用原始代码的内联可能会很有趣,看看您享受了多少好处。

【讨论】:

    【解决方案2】:

    这很昂贵,因为您实际上是在使用多态性,这会破坏分支预测器。

    如果将集合迭代替换为内部链表,它可能会对分支预测器有所帮助:

    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% 的分支预测。

    【讨论】:

      【解决方案3】:

      让我们先解决显而易见的问题:

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

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

      除非图形/分区易于复制且很小,否则您的大部分工作都将在这里。

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

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

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

       foreach (ObjectiveFunction* fp : funcs) {
      

      【讨论】:

      • 这可能会修改原始集合。编译器可以通过对这些引用使用 const 关键字来帮助您避免这种情况。
      • 假设用户运行theProgram --m1 --m2 --m5,那么funcs 包含三个对象:每个选定函数一个对象。此循环不会遍历所有可能的函数,只会遍历那些为当前执行选择的函数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-11-06
      • 2015-07-12
      • 2011-05-03
      • 1970-01-01
      • 2014-05-18
      • 2011-06-18
      • 1970-01-01
      相关资源
      最近更新 更多