可以内联 std::函数还是应该使用不同的方法?
Can be std::function inlined or should I use different approach?
我正在研究一个复杂的框架,它使用std::function<>
作为许多函数的参数。通过分析,我发现了以下性能问题之一。
有人可以解释为什么 Loop3a 这么慢吗?我预计会使用内联,时间会是一样的。程序集也是如此。有什么方法可以提高性能或不同的方法吗?C++17 会以这种方式做出任何改变吗?
#include <iostream>
#include <functional>
#include <chrono>
#include <cmath>
static const unsigned N = 300;
struct Loop3a
{
void impl()
{
sum = 0.0;
for (unsigned i = 1; i <= N; ++i) {
for (unsigned j = 1; j <= N; ++j) {
for (unsigned k = 1; k <= N; ++k) {
sum += fn(i, j, k);
}
}
}
}
std::function<double(double, double, double)> fn = [](double a, double b, double c) {
const auto subFn = [](double x, double y) { return x / (y+1); };
return sin(a) + log(subFn(b, c));
};
double sum;
};
struct Loop3b
{
void impl()
{
sum = 0.0;
for (unsigned i = 1; i <= N; ++i) {
for (unsigned j = 1; j <= N; ++j) {
for (unsigned k = 1; k <= N; ++k) {
sum += sin((double)i) + log((double)j / (k+1));
}
}
}
}
double sum;
};
int main()
{
using Clock = std::chrono::high_resolution_clock;
using TimePoint = std::chrono::time_point<Clock>;
TimePoint start, stop;
Loop3a a;
Loop3b b;
start = Clock::now();
a.impl();
stop = Clock::now();
std::cout << "A: " << std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
std::cout << "msn";
start = Clock::now();
b.impl();
stop = Clock::now();
std::cout << "B: " << std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
std::cout << "msn";
return a.sum == b.sum;
}
使用 g++5.4 和 "-O2 -std=c++14" 的示例输出:
A: 1794ms
B: 906ms
在探查器中,我可以看到许多内部结构:
double&& std::forward<double>(std::remove_reference<double>::type&)
std::_Function_handler<double (double, double, double), Loop3a::fn::{lambda(double, double, double)#1}>::_M_invoke(std::_Any_data const&, double, double, double)
Loop3a::fn::{lambda(double, double, double)#1}* const& std::_Any_data::_M_access<Loop3a::fn::{lambda(double, double, double)#1}*>() const
std::function
不是零运行时成本的抽象。它是一个类型擦除的包装器,在调用operator()
时具有类似virtual
调用的成本,并且还可能进行堆分配(这可能意味着每次调用的缓存未命中)。
编译器很可能无法内联它。
如果要以不引入额外开销并允许编译器内联的方式存储函数对象,则应使用模板参数。这并不总是可行的,但可能适合您的用例。
我写了一篇与该主题相关的文章:">
将函数传递给函数">
它包含一些基准,显示与模板参数和其他解决方案相比,为std::function
生成了多少程序集。
std::function
有一个大致的虚拟呼叫开销。 这很小,但如果您的操作更小,它可能会很大。
在你的例子中,你正在大量循环std::function
,用一组可预测的值调用它,并且可能在其中几乎什么都不做。
我们可以解决这个问题。
template<class F>
std::function<double(double, double, double, unsigned)>
repeated_sum( F&& f ) {
return
[f=std::forward<F>(f)]
(double a, double b, double c, unsigned count)
{
double sum = 0.0;
for (unsigned i = 0; i < count; ++i)
sum += f(a,b,c+i);
return sum;
};
}
然后
std::function<double(double, double, double, unsigned)> fn =
repeated_sum
(
[](double a, double b, double c) {
const auto subFn = [](double x, double y) { return x / (y+1); };
return sin(a) + log(subFn(b, c));
}
);
现在repeating_function
采用一个double, double, double
函数并返回一个double, double, double, unsigned
。 这个新函数重复调用前一个函数,每次最后一个坐标增加 1。
然后,我们按如下方式替换impl
:
void impl()
{
sum = 0.0;
for (unsigned i = 1; i <= N; ++i) {
for (unsigned j = 1; j <= N; ++j) {
fn(i,j,0,N);
}
}
}
我们用对重复函数的单个调用替换"最低级别的循环"。
这将使虚拟呼叫开销减少 300 倍,这基本上使其消失。 基本上,50% 的时间/300 = 0.15% 的时间(实际上是 0.3%,因为我们将时间减少了 2 倍,使贡献翻了一番,但谁在计算十分之一的百分比?
现在在实际情况中,您可能不会使用 300 个相邻值来调用它。 但通常有一些模式。
我们上面所做的是移动一些控制fn
如何在fn
中调用的逻辑。 如果可以做到这一点,则可以从考虑中消除虚拟呼叫开销。
std::function
开销大多可以忽略,除非您想以每秒数十亿次的速度调用它,我称之为"每像素"操作。 将此类操作替换为"每扫描行"(每行相邻像素),开销就不再是一个问题。
这可能需要公开一些关于如何在"标头"中使用函数对象的逻辑。 根据我的经验,仔细选择您公开的逻辑可以使它相对通用。
最后,请注意,可以内联std::function
并且编译器越来越擅长。 但它是艰难的,也是脆弱的。 在这一点上依靠它是不明智的。
还有另一种方法。
template<class F>
struct looper_t {
F fn;
double operator()( unsigned a, unsigned b, unsigned c ) const {
double sum = 0;
for (unsigned i = 0; i < a; ++i)
for (unsigned j = 0; j < b; ++j)
for (unsigned k = 0; k < c; ++k)
sum += fn(i,j,k);
return sum;
}
};
template<class F>
looper_t<F> looper( F f ) {
return {std::move(f)};
}
现在我们编写我们的 looper:
struct Loop3c {
std::function<double(unsigned, unsigned, unsigned)> fn = looper(
[](double a, double b, double c) {
const auto subFn = [](double x, double y) { return x / (y+1); };
return sin(a) + log(subFn(b, c));
}
);
double sum = 0;
void impl() {
sum=fn(N,N,N);
}
};
它删除了三维循环的整个操作,而不仅仅是尾随维度。
- 使用std::函数映射对象方法
- 初始化具有非默认构造函数的std::数组项的更好方法
- std::atomic和std::condition_variable wait,notify_*方法之间的区别
- 绑定派生类方法C++从实例范围之外的分隔 std::function 变量调用
- 为什么 std::span 缺少 cbegin 和 cend 方法?
- 使 std::vector 分配对齐内存的现代方法
- std::find,返回所有找到的值的替代方法,而不仅仅是存在重复的向量的第一个值
- C++ STD 函数运算符:有没有一种方法可以通过函数将一个向量映射到另一个向量上?
- 如何访问存储在 std::variant 中的类的方法
- C++ assigment std::list:<typename>:itrator 在 main 中工作,但在方法中它不起作用
- 我无法使用C++指针指向类方法返回的 std::vector
- 在自定义 std::vector-like 容器中处理指针和非指针模板类型的最佳方法是什么?
- 静态 std::map instatiation 在类的方法中调用构造函数吗?
- C++:std::ofstream 方法 open() 在第二次迭代时擦除打开的 ifstream 文件
- 在 std::vector<无符号字符中存储任意数据的方法>
- 将 std:set<int32_t> 复制到 std::set <uint32_t>的好方法
- std::bind,无法让具有单个参数的方法工作
- 连接和压缩标准::vector<std::字符串的最佳方法>
- C++中的多维数据集:从 std::vector 的 2D 数据到 std::vector 的 2D 网格的最干净方法?
- 向后迭代 std::array 或 std::vector 的正确方法是什么?