可以是std :: function inlined或者我应该使用不同的方法吗?

时间:2017-03-17 11:44:54

标签: c++ c++11 c++14 c++17 std-function

我正在研究一个复杂的框架,它使用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 << "ms\n";

    start = Clock::now();
    b.impl();
    stop = Clock::now();
    std::cout << "B: " << std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
    std::cout << "ms\n";

    return a.sum == b.sum;
}

使用带有“-O2 -std = c ++ 14”的g ++ 5.4的示例输出:

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

2 个答案:

答案 0 :(得分:7)

std::function 是零运行时成本抽象。它是一个类型擦除的包装器,具有virtual - 在调用operator()时调用成本,并且还可能堆积分配(这可能意味着每次调用都会出现缓存错误)

编译器很可能无法内联

如果要以不引入额外开销并允许编译器内联的方式存储函数对象,则应使用模板参数。这并不总是可行,但可能适合您的用例。

我写了一篇与该主题相关的文章:
"Passing functions to functions"

它包含一些基准测试,显示与模板参数和其他解决方案相比,为std::function生成了多少程序集。

答案 1 :(得分:4)

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个相邻值调用它。但通常会有一些模式。

我们上面所做的是移动一些控制fnfn内被调用的逻辑。如果您可以做到这一点,可以考虑删除虚拟呼叫开销。

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);
  }
};

它会删除3维循环的整个操作,而不仅仅是尾随维度。