高效的函子调度器

问题描述

我需要帮助理解两个不同版本的 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) 在调度程序中创建函子的静态实例,但随后对象的生命周期增长到程序的生命周期。

话虽如此,我不知道足够的程序集来有意义地比较这两种方法。 有没有更优雅/更有效的方法

解决方法

假设 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

,

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

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

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

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


对于有问题的代码,优化后的两个代码片段都会产生 identical assembly——这意味着这两种方法应该在实践中表现同样出色(前提是调用特征相同) .但即便如此,benchmarking 也将是验证这一点的唯一真正方法。

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

...静态函数是不好的做法,对吧?

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

DispatchA,... 不必要地创建了一个 (constexpr) 对象

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

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