高效的调度员操作员

Efficient functor dispatcher

我需要帮助了解两个不同版本的仿函数调度程序,请参阅此处:

#include <cmath>
#include <complex>
double* psi;
double dx = 0.1;
int range;
struct A
{
    double operator()(int x) const
    {
        return dx* (double)x*x;
    }
};

template <typename T>
void dispatchA()
{
    constexpr T op{};

    for (int i=0; i<range; i++)
        psi[i]+=op.operator()(i);
}

template <typename T>
void dispatchB(T op)
{

    for (int i=0; i<range; i++)
        psi[i]+=op.operator()(i);
}

int main(int argc, char** argv)
{
    range= argc;
    psi = new double[range];
    dispatchA<A>();
    // dispatchB<A>(A{});
}

住在https://godbolt.org/z/93h5T46oq

调度程序将在一个大循环中被调用多次,因此我需要确保我做对了。 这两个版本在我看来都不必要地复杂,因为仿函数的类型在编译时是已知的。 DispatchA,因为它不必要地创建了一个 (constexpr) 对象。 DispatchB,因为它一遍又一遍地传递对象。

当然可以通过 a) 在仿函数中创建一个静态函数来解决这些问题, 但是静态函数是不好的做法,对吧? b) 在调度器中创建一个仿函数的静态实例,但随后对象的生命周期增长到程序的生命周期。

话虽如此,我对汇编的了解还不够多,无法对这两种方法进行有意义的比较。 有没有更elegant/efficient的方法?

假设 A 是无状态的,就像在您的示例中一样,并且没有非静态数据成员,它们是相同的。编译器足够聪明,可以看到对象的构造是一个空操作并忽略它。让我们稍微清理一下您的代码以获得干净的程序集,我们可以轻松地推断出:

struct A {
  double operator()(int) const noexcept;
};

void useDouble(double);
int genInt();

void dispatchA() {
  constexpr A op{};
  auto const range = genInt();
  for (int i = 0; i < range; i++) useDouble(op(genInt()));
}

void dispatchB(A op) {
  auto const range = genInt();
  for (int i = 0; i < range; i++) useDouble(op(genInt()));
}

这里,输入从哪里来,输出到哪里,都被抽象掉了。生成的程序集只能因 op 对象的创建方式而异。用 GCC 11.1 编译它,我得到 identical assembly generation。没有创建或初始化 A

这可能不是您正在寻找的答案,但您将从几乎所有经验丰富的开发人员那里得到的一般建议是以 natural/understandable 方式编写代码,并且仅在以下情况下进行优化你需要。

这听起来像是没有答案,但实际上是个好建议。

大多数时候,您 可能 (如果有的话)因像这样的小决定而付出的代价总体上是无关紧要的。一般来说,优化 一个算法 比优化 几个指令 的收益更多。这条规则确实有例外——但通常这样的优化是 紧密循环 的一部分——这是您可以通过分析和基准测试追溯查看的类型。

最好以将来可以维护的方式编写代码,并且只有在证明这是一个问题时才真正优化它。


对于有问题的代码,优化后的两个代码片段 identical assembly -- meaning that both approach should perform equally as well in practice (provided the calling characteristics are the same). But even then, benchmarking 将是验证这一点的唯一真实方法。

由于调度程序是函数 template 定义,它们是隐式的 inline,并且它们的定义在调用之前始终可见。通常,这足以让优化器自省和内联此类代码(如果它认为这比不这样做更好)。

... static functions are bad practice, right?

否; static 函数不是坏习惯。与 C++ 中的任何实用程序一样,它们肯定会被滥用——但它们本身并没有什么坏处。

DispatchA, ... unnecessarily creates an (constexpr) object

constexpr 对象是在编译时构建的——因此除了可能在堆栈上保留更多 space 之外,您不会看到任何实际成本。这个成本真的很小。

如果你真的想避免这种情况,你也可以改为 static constexpr。尽管从逻辑上讲,正如您提到的那样,“对象的生命周期增长到程序的生命周期”,constexpr 对象在 C++ 中不能具有退出时行为,因此成本几乎不存在。