高效的调度员操作员
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++ 中不能具有退出时行为,因此成本几乎不存在。
我需要帮助了解两个不同版本的仿函数调度程序,请参阅此处:
#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++ 中不能具有退出时行为,因此成本几乎不存在。