首页 > 解决方案 > 如何在进入循环之前选择嵌套循环内调用的函数?

问题描述

如以下代码所示,在函数中调用了几个原子例程之一messagePassing。在深入嵌套循环之前确定使用哪一个。在当前的实现中,while为了运行时性能,使用了几个循环。为了可读性和可维护性,我想避免重复自己(重复嵌套循环中的共享操作),并实现类似messagePassingCleanButSlower.

有没有不牺牲运行时性能的方法?

我需要处理两种情况。

  1. 在第一个中,原子例程很小,只涉及 3 个加/减操作,因此我猜它们将被内联。
  2. 在第二个中,原子例程很大(大约 200 行),因此不太可能内联。
#include <vector>

template<typename Uint, typename Real>
class Graph {
public:
  void messagePassing(Uint nit, Uint type);
  void messagePassingCleanButSlower(Uint nit, Uint type);

private:
  struct Vertex {}; // Details are hidden since they are distracting.
  std::vector< Vertex > vertices;

  void atomicMessagePassingType1(Vertex &v);
  void atomicMessagePassingType2(Vertex &v);
  void atomicMessagePassingType3(Vertex &v);
  // ...
  // may have other types
};

template<typename Uint, typename Real>
void
Graph<Uint, Real>::
messagePassing(Uint nit, Uint type)
{
  Uint count = 0;   // round counter
  if (type == 1) {
    while (count < nit) {
      ++count;
      // many operations
      for (auto &v : vertices) {
        // many other operations
        atomicMessagePassingType1(v);
      }
    }
  }
  else if (type == 2) {
    while (count < nit) {
      ++count;
      // many operations
      for (auto &v : vertices) {
        // many other operations
        atomicMessagePassingType2(v);
      }
    }
  }
  else {
    while (count < nit) {
      ++count;
      // many operations
      for (auto &v : vertices) {
        // many other operations
        atomicMessagePassingType3(v);
      }
    }
  }
}

template<typename Uint, typename Real>
void
Graph<Uint, Real>::
messagePassingCleanButSlower(Uint nit, Uint type)
{
  Uint count = 0;   // round counter
  while (count < nit) {
    ++count;
    // many operations
    for (auto &v : vertices) {
      // many other operations
      if (type == 1) {
        atomicMessagePassingType1(v);
      }
      else if (type == 2) {
        atomicMessagePassingType2(v);
      }
      else {
        atomicMessagePassingType3(v);
      }
    }
  }
}

标签: c++performancec++11

解决方案


在此处查看基准:

  1. http://quick-bench.com/rMsSb0Fg4I0WNFX8QbKugCe3hkc

对于 1. 我已经设置了一个测试场景,其中的操作atomicMessagePassingTypeX非常短(只是一个优化障碍)。我大致选择了外部100元素vertices100迭代while。这些条件对于您的实际代码会有所不同,因此我的基准测试结果是否适用于您的情况,您必须通过对自己的代码进行基准测试来验证。

这四个测试用例是:您的两个变体,一个带有其他答案中提到的函数指针,另一个是通过调度 lambda 调用函数指针,如下所示:

template<typename Uint, typename Real>
void
Graph<Uint, Real>::
messagePassingLambda(Uint nit, Uint type)
{
  using ftype = decltype(&Graph::atomicMessagePassingType1);
  auto lambda = [&](ftype what_to_call) {
    Uint count = 0;   // round counter
    while (count < nit) {
      ++count;
      // many operations
      for (auto &v : vertices) {
        // many other operations
        (this->*what_to_call)(v);
      }
    }
  };
  if(type == 1) lambda(&Graph::atomicMessagePassingType1);
  else if(type == 2) lambda(&Graph::atomicMessagePassingType2);
  else lambda(&Graph::atomicMessagePassingType3);
}

尝试 GCC 9.1/Clang 8.0 和 O2/O3 的所有组合。您将看到在 O3 中,两个编译器为您的“慢”变体提供大致相同的性能,在 GCC 的情况下,它实际上是最好的。if编译器确实将/语句从至少内部循环中提升else出来,然后,由于某种我并不完全清楚的原因,GCC 确实对内部循环中的指令进行了不同于第一个变体的重新排序,导致它是甚至稍微快一点。

函数指针变体始终是最慢的。

lambda 变体在性能上实际上等于您的第一个变体。我想很清楚为什么如果内联 lambda,它们本质上是相同的。

如果它没有被内联,那么由于间接调用what_to_call. 这可以通过在以下的每个调用站点强制使用适当的直接调用的不同类型来避免lambda

使用 C++14 或更高版本,您可以制作通用 lambda:

 auto lambda = [&](auto what_to_call) {

将调用表单调整(this->*what_to_call)(v);what_to_call();并使用另一个 lambda 调用它:

lambda([&](){ atomicMessagePassingType1(v); });

这将强制编译器在每次调度时实例化一个函数,并且应该删除任何潜在的间接调用。

使用 C++11,您无法制作通用 lambda 或变量模板,因此您需要编写一个以辅助 lambda 作为参数的实际函数模板。


推荐阅读