Je suis en train de créer un mécanisme qui permet aux utilisateurs de former des fonctions complexes arbitraires à partir de blocs de construction de base en utilisant l'outil de gestion de l'information de l'entreprise. motif décoratif . Cela fonctionne bien du point de vue fonctionnel, mais je n'aime pas le fait que cela implique beaucoup d'appels virtuels, en particulier lorsque la profondeur d'imbrication devient importante. Cela m'inquiète car la fonction complexe peut être appelée souvent (>100 000 fois).
Pour éviter ce problème, j'ai essayé de transformer le schéma du décorateur en une std::function
une fois qu'il était terminé (cfr. to_function()
dans le SSCCE). Tous les appels de fonctions internes sont câblés lors de la construction de la std::function
. Je me suis dit que cette méthode serait plus rapide à évaluer que le schéma original du décorateur, car aucune recherche virtuelle ne doit être effectuée dans le fichier std::function
version.
Hélas, les bancs d'essai prouvent que j'avais tort : le schéma du décorateur est en fait plus rapide que le schéma de l'utilisateur. std::function
J'ai construit à partir de ça. Je me demande maintenant pourquoi. Peut-être que ma configuration de test est défectueuse puisque je n'utilise que deux fonctions de base triviales, ce qui signifie que les recherches dans les vtable peuvent être mises en cache ?
Le code que j'ai utilisé est inclus ci-dessous, malheureusement il est assez long.
SSCCE
// 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;
}
Point de repère
Utilisation de pipe
:
g++ -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 60 ms
Utilisation de fun
:
g++ -DUSE_FUNCTION -std=gnu++11 sscce.cpp -march=native -O3
sudo nice -3 /usr/bin/time ./a.out
-> 110 ms