C++ 11 std::function 比虚拟调用慢? [英] C++11 std::function slower than virtual calls?

查看:56
本文介绍了C++ 11 std::function 比虚拟调用慢?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一种机制,允许用户使用 装饰模式.这在功能上很好,但我不喜欢它涉及大量虚拟调用的事实,特别是当嵌套深度变大时.这让我很担心,因为复杂的函数可能会经常调用(> 100.000 次).

I am creating a mechanism which allows users to form arbitrary complex functions from basic building blocks using the decorator pattern. This works fine functionality wise, but I don't like the fact that it involves a lot of virtual calls, particularly when the nesting depth becomes large. It worries me because the complex function may called often (>100.000 times).

为了避免这个问题,我尝试在完成后将装饰器方案转换为 std::function(参见 SSCCE 中的 to_function()).所有内部函数调用都在 std::function 的构造过程中进行连接.我认为这比原始装饰器方案的评估速度更快,因为在 std::function 版本中不需要执行虚拟查找.

To avoid this problem, I tried to turn the decorator scheme into a std::function once it was finished (cfr. to_function() in the SSCCE). All internal function calls are wired during construction of the std::function. I figured this would be faster to evaluate than the original decorator scheme because no virtual lookups need to be performed in the std::function version.

唉,基准测试证明我错了:装饰器方案实际上比我用它构建的 std::function 快.所以现在我想知道为什么.也许我的测试设置有问题,因为我只使用了两个微不足道的基本函数,这意味着 vtable 查找可能会被缓存?

Alas, benchmarks prove me wrong: the decorator scheme is in fact faster than the std::function I built from it. So now I am left wondering why. Maybe my test setup is faulty since I only use two trivial basic functions, which means the vtable lookups may be cached?

我使用的代码包含在下面,不幸的是它很长.

The code I used is included below, unfortunately it is quite long.

// sscce.cpp
#include <iostream>
#include <vector>
#include <memory>
#include <functional>
#include <random>

/**
 * Base class for Pipeline scheme (implemented via decorators)
 */
class Pipeline {
protected:
    std::unique_ptr<Pipeline> wrappee;
    Pipeline(std::unique_ptr<Pipeline> wrap)
    :wrappee(std::move(wrap)){}
    Pipeline():wrappee(nullptr){}

public:
    typedef std::function<double(double)> FnSig;
    double operator()(double input) const{
        if(wrappee.get()) input=wrappee->operator()(input);
        return process(input);
    }

    virtual double process(double input) const=0;
    virtual ~Pipeline(){}

    // Returns a std::function which contains the entire Pipeline stack.
    virtual FnSig to_function() const=0;
};

/**
 * CRTP for to_function().
 */
template <class Derived>
class Pipeline_CRTP : public Pipeline{
protected:
    Pipeline_CRTP(const Pipeline_CRTP<Derived> &o):Pipeline(o){}
    Pipeline_CRTP(std::unique_ptr<Pipeline> wrappee)
    :Pipeline(std::move(wrappee)){}
    Pipeline_CRTP():Pipeline(){};
public:
    typedef typename Pipeline::FnSig FnSig;

    FnSig to_function() const override{
        if(Pipeline::wrappee.get()!=nullptr){

            FnSig wrapfun = Pipeline::wrappee->to_function();
            FnSig processfun = std::bind(&Derived::process,
                static_cast<const Derived*>(this),
                std::placeholders::_1);
            FnSig fun = [=](double input){
                return processfun(wrapfun(input));
            };
            return std::move(fun);

        }else{

            FnSig processfun = std::bind(&Derived::process,
                static_cast<const Derived*>(this),
                std::placeholders::_1);
            FnSig fun = [=](double input){
                return processfun(input);
            };
            return std::move(fun);
        }

    }

    virtual ~Pipeline_CRTP(){}
};

/**
 * First concrete derived class: simple scaling.
 */
class Scale: public Pipeline_CRTP<Scale>{
private:
    double scale_;
public:
    Scale(std::unique_ptr<Pipeline> wrap, double scale) // todo move
:Pipeline_CRTP<Scale>(std::move(wrap)),scale_(scale){}
    Scale(double scale):Pipeline_CRTP<Scale>(),scale_(scale){}

    double process(double input) const override{
        return input*scale_;
    }
};

/**
 * Second concrete derived class: offset.
 */
class Offset: public Pipeline_CRTP<Offset>{
private:
    double offset_;
public:
    Offset(std::unique_ptr<Pipeline> wrap, double offset) // todo move
:Pipeline_CRTP<Offset>(std::move(wrap)),offset_(offset){}
    Offset(double offset):Pipeline_CRTP<Offset>(),offset_(offset){}

    double process(double input) const override{
        return input+offset_;
    }
};

int main(){

    // used to make a random function / arguments
    // to prevent gcc from being overly clever
    std::default_random_engine generator;
    auto randint = std::bind(std::uniform_int_distribution<int>(0,1),std::ref(generator));
    auto randdouble = std::bind(std::normal_distribution<double>(0.0,1.0),std::ref(generator));

    // make a complex Pipeline
    std::unique_ptr<Pipeline> pipe(new Scale(randdouble()));
    for(unsigned i=0;i<100;++i){
        if(randint()) pipe=std::move(std::unique_ptr<Pipeline>(new Scale(std::move(pipe),randdouble())));
        else pipe=std::move(std::unique_ptr<Pipeline>(new Offset(std::move(pipe),randdouble())));
    }

    // make a std::function from pipe
    Pipeline::FnSig fun(pipe->to_function());   

    double bla=0.0;
    for(unsigned i=0; i<100000; ++i){
#ifdef USE_FUNCTION
        // takes 110 ms on average
        bla+=fun(bla);
#else
        // takes 60 ms on average
        bla+=pipe->operator()(bla);
#endif
    }   
    std::cout << bla << std::endl;
}

<小时>

基准

使用管道:

g++ -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 60 ms

使用乐趣:

g++ -DUSE_FUNCTION -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 110 ms

推荐答案

正如 Sebastian Redl 的回答所说,虚拟函数的替代"通过动态绑定函数(虚拟或通过函数指针,取决于std::function 实现),然后它仍然调用虚拟的 Pipeline::process(double) 函数!

As Sebastian Redl's answer says, your "alternative" to virtual functions adds several layers of indirection through dynamically bound functions (either virtual, or through function pointers, depending on the std::function implementation) and then it still calls the virtual Pipeline::process(double) function anyway!

此修改通过移除一层 std::function 间接并防止对 Derived::process 的调用是虚拟的,从而显着加快了速度:

This modification makes it significantly faster, by removing one layer of std::function indirection and preventing the call to Derived::process being virtual:

FnSig to_function() const override {
    FnSig fun;
    auto derived_this = static_cast<const Derived*>(this);
    if (Pipeline::wrappee) {
        FnSig wrapfun = Pipeline::wrappee->to_function();
        fun = [=](double input){
            return derived_this->Derived::process(wrapfun(input));
        };
    } else {
        fun = [=](double input){
            return derived_this->Derived::process(input);
        };
    }
    return fun;
}

这里还有比虚函数版本更多的工作要做.

There's still more work being done here than in the virtual function version though.

这篇关于C++ 11 std::function 比虚拟调用慢?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆