微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

为什么 std::function 太慢是 CPU 不能利用指令重新排序?

如何解决为什么 std::function 太慢是 CPU 不能利用指令重新排序?

当我开发我的项目时,我发现 std::function 真的很慢。
所以我想知道为什么它真的很慢。
但我找不到明显的原因。

我认为 cpu 不能利用指令重新排序优化和 cpu 流水线,因为它不知道调用哪个函数性能不佳的原因。
它使内存停滞和性能下降....

我说得对吗???

解决方法

包装到 std::function 中的代码总是比将代码直接内联到调用位置慢。特别是如果你的代码很短,比如 3-5 个 CPU 指令。

如果您的函数代码很大,有数百条指令,那么使用 std::function 或其他调用/包装代码的机制没有区别。

std::function 代码未内联。使用 std::function 包装器与在类中使用虚拟方法具有几乎相同的速度开销。不仅如此,std::function 机制看起来非常像虚拟调用机制,在这两种情况下,代码都没有内联,而是使用指向代码的指针通过汇编程序的 call 指令调用它。

如果你真的需要速度,那么使用 lambdas 并将它们作为模板参数传递,如下所示。如果可能,Lambda 总是内联的(如果编译器决定它会提高速度)。

Try it online!

#include <functional>

template <typename F>
void __attribute__((noinline)) use_lambda(F const & f) {
    auto volatile a = f(13); // call f
    // ....
    auto volatile b = f(7); // call f again
}

void __attribute__((noinline)) use_func(
        std::function<int(int)> const & f) {
    auto volatile a = f(11); // call f
    // ....
    auto volatile b = f(17); // call f again
}

int main() {
    int x = 123;
    auto f = [&](int y){ return x + y; };
    use_lambda(f); // Pass lambda
    use_func(f); // Pass function
}

如果您查看上述示例的汇编代码(单击上面的在线试用链接),您会发现 lambda 代码已内联,而 std::function 代码未内联。

模板参数总是比其他解决方案更快,您应该始终在需要多态性同时具有高性能的任何地方使用模板。

,

std::function 会导致几种开销。

首先,编译器很难理解。如果你有一个原始函数指针,一些编译器能够比使用 std 函数更容易“撤消”间接。但是,在这些情况下,原始函数指针和 std 函数的使用通常首先是错误的。

其次,std 函数的实现方式通常涉及一个虚函数表,它导致最多 2 个间接引用,而不是一个函数指针。当虚拟函数表脱离 CPU 缓存时,此命中最大。

第三,C++ 编译器非常擅长内联,以及通过 std 函数块间接调用。


现在,根据我的经验,当您进行缓冲区处理(例如逐像素操作)时,这种开销会变得最严重。

在这种情况下,您可以处理数百万或数十亿像素。每个像素完成的工作很小,与实际完成的工作相比,对每个操作进行 std 函数调用的开销最终会很大。

解决这个(及相关)问题的最简单方法是保存一个缓冲区处理函数,而不是像这样的每个元素的函数。

using Pixel = std::uint32_t;
using Scanline = std::span<Pixel>;
using ScanlineOp = std::function<void(Scanline)>;

template<class PixelOp>
ScanlineOp MakeScanlineOp( PixelOp op ) {
  return [op=std::move(op)](Scanline line) {
    for (Pixel& p : line)
      op(p);
  };
}

这里我采用逐像素操作,并将它与迭代代码一起保存到 std::function 中。

现在,在处理 4000 x 4000 像素的图像时,我没有遇到 std::function 1600 万次的开销,而是遇到了 4000 次。这将开销成本降低了 99.975%。

将某些东西的速度提高 4000 倍,您就不再关心它的成本了。

现在,std::span 是一种不在 中的类型。这是一个玩具版本:

template<class It>
struct range {
    It b,e;
    using reference = typename std::iterator_traits<It>::reference;
    using value_type = typename std::iterator_traits<It>::value_type;

    range( It s,It f ):b(s),e(f) {}
    It begin() const { return b; }
    It end() const { return e; }
    bool empty() const { return begin()==end(); }
    reference front() const { return *begin(); }
};
template<class It>
struct random_range:range<It> {
    using range<It>::range;
    using reference = typename range<It>::reference;

    reference back() const { return *std::prev(this->end()); }
    std::size_t size() const { return this->end()-this->begin(); }
    reference operator[](std::size_t i) const{ return this->begin()[i]; }
};

template<class T>
struct array_view:random_range<T*> {
    array_view( T* start,T* finish ):random_range<T*>(start,finish) {}
    array_view( T* start,std::size_t length ):array_view(start,start+length) {}
    array_view():array_view(nullptr,nullptr) {}

    template<class C>
    using data_type = typename std::remove_pointer< decltype( std::declval<C>().data() )>::type;
    template<class U>
    static constexpr bool pointer_compatible() {
        return 
            std::is_same<
                typename std::decay<U>::type,typename std::decay<T>::type
            >::value
            && std::is_convertible<U*,T*>::value;
    }
    // accept any container whose 
    template<class C,typename std::enable_if< pointer_compatible<data_type<C>>(),bool >::type = true
    >
    array_view( C&& c ):array_view(c.data(),c.size()) {}
};

复杂的部分是我接受 vectorarray 的地方,因为它的 .data() 字段存在,返回一个兼容的指针。


您将转换如下代码:

void foreachPixel( PixelOp op,Image img ) {
  for (int i = 0; i < img.height(); ++i)
    for (int j = 0; j < img.width(); ++j)
      op(img[i][j]);
}

void foreachPixel( ScanlineOp op,Image img ) {
  for (int i = 0; i < img.height(); ++i)
    op(img.Scanline(i));
}

现在,我所展示的是针对一个具体案例。总体思路是,您可以将一些低级控制流注入您的 std::function 并在更高一级进行操作,从而消除几乎所有 std::function 开销。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。