首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >C++11 std::功能比虚拟调用慢?

C++11 std::功能比虚拟调用慢?
EN

Stack Overflow用户
提问于 2013-09-04 08:26:26
回答 4查看 11.2K关注 0票数 40

我正在创建一种机制,允许用户使用装饰图案从基本构建块中形成任意复杂的函数。这在功能上很好,但我不喜欢它涉及大量的虚拟调用,特别是当嵌套深度变得很大时。它让我担心,因为复杂的函数可能经常调用(>100.000次)。

为了避免这个问题,我试图在装饰方案完成后将其转换为std::function (cfr )。SSCCE中的to_function() )。在构建std::function期间,所有内部函数调用都是连接的。我认为这将比最初的装饰方案更快,因为在std::function版本中不需要执行虚拟查找。

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

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

SSCCE

代码语言:javascript
复制
// 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;
}

基准测试

使用pipe

代码语言:javascript
复制
g++ -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 60 ms

使用fun

代码语言:javascript
复制
g++ -DUSE_FUNCTION -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 110 ms
EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2013-09-04 12:12:35

正如Sebastian的答案所述,您的虚拟函数的“替代”通过动态绑定函数(根据std::function实现,通过虚拟函数或函数指针)添加了几个间接层,然后它仍然调用虚拟Pipeline::process(double)函数!

通过移除一个std::function间接层并防止对Derived::process的调用是虚拟的,此修改使其速度大大加快:

代码语言:javascript
复制
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;
}

不过,这里比虚拟函数版本做的工作还多。

票数 20
EN

Stack Overflow用户

发布于 2013-09-04 09:07:29

您有std::function的绑定lambdas,它调用std::functions绑定lamdbas,调用std::functions .

看看你的to_function。它创建一个lambda,它调用两个std::function,并返回绑定到另一个std::function的lambda。编译器不会静态地解析其中任何一个。

最后,以与虚拟函数解决方案相同的间接调用结束,这是如果您摆脱绑定的processfun并在lambda中直接调用它。否则你的人数是原来的两倍。

如果您想要加速,就必须以静态解析的方式创建整个管道,这意味着在最终将类型擦除为单个std::function之前,有更多的模板。

票数 25
EN

Stack Overflow用户

发布于 2013-09-04 09:36:10

std::function速度慢得出了名;类型擦除和由此产生的分配也是其中的一部分,而且,对于gcc,调用的内联/优化也很糟糕。出于这个原因,存在着大量的C++“委托”,人们试图用它们来解决这个问题。我将其中一个移植到代码审查中:

https://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11

但是你可以用谷歌找到很多其他人,或者写你自己的。

编辑:

这些天来,这里寻找一个快速的委托。

票数 9
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/18608888

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档