【问题标题】:Can be std::function inlined or should I use different approach?可以内联 std::function 还是应该使用不同的方法?
【发布时间】:2017-03-17 11:44:53
【问题描述】:

我正在开发一个复杂的框架,它使用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

【问题讨论】:

  • std::function 确实类型擦除并且从不内联。
  • 恕我直言,在这种情况下,最好使用仿函数而不是 lambda。然后你可以有一个命名函数类型而不是std::function,编译器擅长内联函子。
  • 使用 lambda 代替 std::function 是否工作得更快? IE。 auto fn = [](double a, double b, double c) ....
  • 除了已经说过的其他内容之外:由于它不是 const,因此永远不能内联。您可以在运行时更改指针。因此编译器必须证明它从未被修改过才能内联它,这在某些情况下甚至是不可能的。

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


【解决方案1】:

std::function 不是零运行时成本抽象。它是一个类型擦除的包装器,在调用 operator() 时具有类似 virtual-call 的成本,并且还可能潜在地进行堆分配(这可能意味着每次调用缓存未命中)。 p>

编译器很可能无法内联它

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


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

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

【讨论】:

  • 有趣的事实:这是我在一小时内第三次使用std::function回答一个不必要的问题。我希望更清楚一点,它不是零成本抽象,它被命名为 std::type_erased_function...
  • @Vittorrio:当我研究如何存储参考页面提到的 lambda 时,我认为你可以责怪 cpp 参考站点。事实证明,函数指针要快得多,但是关于 lambdas 的页面并没有提到你可以将 lambda 传递给其中一个。
  • @JasonLang:那是因为你不能。如果它是 captureless lambda,则只能将 lambda 转换为函数指针。参考页面指出。
【解决方案2】:

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

现在我们编写循环器:

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 维循环的整个操作,而不仅仅是尾随维度。

【讨论】:

  • 在我看来,使用模板参数的第二种方法是重构之前的解决方案,即使我不喜欢它,因为我对模板参数中传递的内容的控制较少。我希望没有隐式转换和其他东西。
  • @Radek 我不明白。你的意思是looper_t 一个?在这种情况下,模板参数是一个 lambda,您可以控制它,因为您调用 looper。然后looper_t 转换为std::function,就像代码中的 lambda 转换为std::function 一样。这就是std::function
  • 谢谢你我明白了。几个月前,我只想避免到处写template&lt;class F&gt;。你知道 std::function 更具可读性。
  • @Radek 是的,经常。 std::function 是类型擦除。类型擦除以适中的运行时成本“隐藏”了里面的确切类型。选择类型擦除点,即我们忘记我们实际使用的类型并对其进行一般处理的点,可以将适度的运行时成本降低到几乎为零。 looper 返回的东西不是类型擦除,但可以立即。我本来可以让looper 返回std::function&lt;double(unsigned, unsigned, unsigned)&gt;,但我对过早的类型擦除持防御态度。
猜你喜欢
  • 2012-02-01
  • 2015-07-26
  • 1970-01-01
  • 2014-11-09
  • 2011-10-10
  • 2013-02-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多