0

我需要帮助理解函子调度程序的两个不同版本,请参见此处:

#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) 在调度程序中创建函子的静态实例,但随后对象的生命周期会增长到程序的生命周期。

话虽这么说,我不知道足够的组装来有意义地比较这两种方法。有没有更优雅/更有效的方法?

4

2 回答 2

1

假设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 编译它,我得到相同的程序集生成。不会发生任何创建或初始化A

于 2021-06-18T21:03:34.207 回答
1

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

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

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

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


对于有问题的代码,优化后的两个代码片段都会产生相同的程序集——这意味着两种方法在实践中的性能应该相同(只要调用特性相同)。但即便如此,基准测试将是验证这一点的唯一真正方法。

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

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

不; static功能是不错的做法。就像 C++ 中的任何实用程序一样,它们肯定会被滥用——但它们本质上并没有什么坏处。

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

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

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

于 2021-06-18T21:04:53.170 回答