2016-05-13 45 views
10

mi sto paragonando le prestazioni dei seguenti metodi di C++ polimorfismo:statico polimorfismo con boost variante singolo visitatore vs più visitatore vs polimorfismo dinamico

Metodo [1]. polimorfismo statico utilizzando le varianti boost con un visitatore separato per ciascun metodo Metodo [2]. polimorfismo statico che utilizza le varianti boost con un singolo visitatore che chiama un metodo diverso usando il metodo overloading Method [3]. Plain Old polimorfismo dinamico

Piattaforma: - moderno processore multi-core Intel x86 a 64 bit di Red Hat, 32 GB di RAM - gcc (GCC) 4.8.1 con -O2 ottimizzazione - Boost 1.6.0

alcuni risultati:

  • Metodo [1] sembra avere Metodi significativamente sovraperformare [2] e [3]
  • Metodo [3] sorpassa Metodo [2] la maggior parte del tempo

La mia domanda è, perché Method [2] dove sto usando un visitatore ma utilizzando il metodo overloading per chiamare il metodo corretto offre prestazioni peggiori rispetto ai metodi virtuali. Mi aspetto che il polimorfismo statico funzioni meglio del polimorfismo dinamico. Capisco che ci sia un certo costo del parametro extra che viene passato nel metodo [2] per capire quale metodo visit() della classe chiamare e possibilmente qualche ramo in più a causa dell'overload dei metodi? Ma shouldn "t questo ancora sovraperformare metodi virtuali

codice è qui sotto:?.

// qcpptest.hpp 

#ifndef INCLUDED_QCPPTEST_H 
#define INCLUDED_QCPPTEST_H 

#include <boost/variant.hpp> 

class IShape { 
public: 
    virtual void rotate() = 0; 
    virtual void spin() = 0; 
}; 

class Square : public IShape { 
public: 
    void rotate() { 
    // std::cout << "Square:I am rotating" << std::endl; 
    } 
    void spin() { 
    // std::cout << "Square:I am spinning" << std::endl; 
    } 
}; 

class Circle : public IShape { 
public: 
    void rotate() { 
    // std::cout << "Circle:I am rotating" << std::endl; 
    } 
    void spin() { 
    // std::cout << "Circle:I am spinning" << std::endl; 
} 
}; 

// template variation 

// enum class M {ADD, DEL}; 
struct ADD {}; 
struct DEL {}; 

class TSquare { 
    int i; 
public: 
    void visit(const ADD& add) { 
     this->i++; 
    // std::cout << "TSquare:I am rotating" << std::endl; 
    } 
    void visit(const DEL& del) { 
     this->i++; 
    // std::cout << "TSquare:I am spinning" << std::endl; 
    } 

    void spin() { 
     this->i++; 
    // std::cout << "TSquare:I am rotating" << std::endl; 
} 
    void rotate() { 
     this->i++; 
    // std::cout << "TSquare:I am spinning" << std::endl; 
} 
}; 

class TCircle { 
    int i; 
public: 
    void visit(const ADD& add) { 
     this->i++; 
    // std::cout << "TCircle:I am rotating" << std::endl; 
    } 
    void visit(const DEL& del) { 
     this->i++; 
    // std::cout << "TCircle:I am spinning" << std::endl; 
    } 
    void spin() { 
     this->i++; 
     // std::cout << "TSquare:I am rotating" << std::endl; 
    } 
    void rotate() { 
    this->i++; 
     // std::cout << "TSquare:I am spinning" << std::endl; 
    } 
}; 

class MultiVisitor : public boost::static_visitor<void> { 
public: 
    template <typename T, typename U> 

    void operator()(T& t, const U& u) { 
    // std::cout << "visit" << std::endl; 
    t.visit(u); 
    } 
}; 

// separate visitors, single dispatch 

class RotateVisitor : public boost::static_visitor<void> { 
public: 
    template <class T> 
    void operator()(T& x) { 
    x.rotate(); 
    } 
}; 

class SpinVisitor : public boost::static_visitor<void> { 
public: 
    template <class T> 
    void operator()(T& x) { 
    x.spin(); 
    } 
}; 

#endif 

// qcpptest.cpp 

#include <iostream> 
#include "qcpptest.hpp" 
#include <vector> 
#include <boost/chrono.hpp> 

using MV = boost::variant<ADD, DEL>; 
// MV const add = M::ADD; 
// MV const del = M::DEL; 
static MV const add = ADD(); 
static MV const del = DEL(); 

void make_virtual_shapes(int iters) { 
    // std::cout << "make_virtual_shapes" << std::endl; 
    std::vector<IShape*> shapes; 
    shapes.push_back(new Square()); 
    shapes.push_back(new Circle()); 

    boost::chrono::high_resolution_clock::time_point start = 
     boost::chrono::high_resolution_clock::now(); 

    for (int i = 0; i < iters; i++) { 
    for (IShape* shape : shapes) { 
     shape->rotate(); 
     shape->spin(); 
    } 
    } 

    boost::chrono::nanoseconds nanos = 
     boost::chrono::high_resolution_clock::now() - start; 
    std::cout << "make_virtual_shapes took " << nanos.count() * 1e-6 
      << " millis\n"; 
} 

void make_template_shapes(int iters) { 
    // std::cout << "make_template_shapes" << std::endl; 
    using TShapes = boost::variant<TSquare, TCircle>; 
    // using MV = boost::variant<M>; 

    // xyz 
    std::vector<TShapes> tshapes; 
    tshapes.push_back(TSquare()); 
    tshapes.push_back(TCircle()); 
    MultiVisitor mv; 

    boost::chrono::high_resolution_clock::time_point start = 
     boost::chrono::high_resolution_clock::now(); 

    for (int i = 0; i < iters; i++) { 
    for (TShapes& shape : tshapes) { 
     boost::apply_visitor(mv, shape, add); 
     boost::apply_visitor(mv, shape, del); 
     // boost::apply_visitor(sv, shape); 
    } 
    } 
    boost::chrono::nanoseconds nanos = 
     boost::chrono::high_resolution_clock::now() - start; 
    std::cout << "make_template_shapes took " << nanos.count() * 1e-6 
      << " millis\n"; 
} 

void make_template_shapes_single(int iters) { 
    // std::cout << "make_template_shapes_single" << std::endl; 
    using TShapes = boost::variant<TSquare, TCircle>; 
    // xyz 
    std::vector<TShapes> tshapes; 
    tshapes.push_back(TSquare()); 
    tshapes.push_back(TCircle()); 
    SpinVisitor sv; 
    RotateVisitor rv; 

    boost::chrono::high_resolution_clock::time_point start = 
     boost::chrono::high_resolution_clock::now(); 

    for (int i = 0; i < iters; i++) { 
    for (TShapes& shape : tshapes) { 
     boost::apply_visitor(rv, shape); 
     boost::apply_visitor(sv, shape); 
    } 
    } 
    boost::chrono::nanoseconds nanos = 
     boost::chrono::high_resolution_clock::now() - start; 
    std::cout << "make_template_shapes_single took " << nanos.count() * 1e-6 
      << " millis\n"; 
} 

int main(int argc, const char* argv[]) { 
    std::cout << "Hello, cmake" << std::endl; 

    int iters = atoi(argv[1]); 

    make_virtual_shapes(iters); 
    make_template_shapes(iters); 
    make_template_shapes_single(iters); 

    return 0; 
} 
+0

Questo programma segifica per me quando è compilato con '-O3'. Sei sicuro che la tua logica sia corretta? –

+1

Solo segfaults se non viene fornito argv [1] :) –

+0

Sì, è necessario fornire un argomento come 10 o 1000 o 1000000. Quante volte verrà eseguito il ciclo. – Sid

risposta

4

Metodo 2 è fondamentalmente reimplementare spedizione dinamica inefficiente Quando si dispone di:

shape->rotate(); 
shape->spin(); 

Ecco comporta cercando il la giusta funzione nel vtable e la chiamata. L'inefficienza da quella ricerca.Ma quando hai:

boost::apply_visitor(mv, shape, add); 

che esplode approssimativamente in (assumendo un modello add<> membro funzione che è solo un reinterpret_cast senza controllare):

if (shape.which() == 0) { 
    if (add.which() == 0) { 
     mv(shape.as<TSquare&>(), add.as<ADD&>()); 
    } 
    else if (add.which() == 1) { 
     mv(shape.as<TSquare&>(), add.as<DEL&>()); 
    } 
    else { 
     // ??? 
    } 
} 
else if (shape.which() == 1) { 
    if (add.which() == 0) { 
     mv(shape.as<TCircle&>(), add.as<ADD&>()); 
    } 
    else if (add.which() == 1) { 
     mv(shape.as<TCircle&>(), add.as<DEL&>()); 
    } 
    else { 
     // ??? 
    } 
} 
else { 
    // ??? 
} 

Qui, abbiamo un'esplosione combinatoria di rami (che non abbiamo avuto a che fare nel metodo 1) ma dobbiamo effettivamente controllare ogni possibile tipo statico di ogni variante per capire cosa dovevamo fare (cosa che non dovevamo fare nel Metodo 3). E quei rami non saranno in grado di essere previsti poiché ne stai prendendo uno diverso ogni volta, quindi non puoi pipeline alcun tipo di codice senza interrompere bruscamente.

Il sovraccarico su mv() è gratuito: è il modo di capire che cosa stiamo chiamando mv con quello no. Si noti anche il tempo delta che sarebbe accaduto sulla base di cambiare uno dei due assi:

+---------------+----------------+----------------+----------+ 
|    | Method 1 | Method 2 | Method 3 | 
+---------------+----------------+----------------+----------+ 
| New Type | More Expensive | More Expensive | Free | 
| New Operation |  Free  | More Expensive | Free* | 
+---------------+----------------+----------------+----------+ 

Metodo 1 diventa più costoso sull'aggiunta di nuovi tipi perché dobbiamo iterare in modo esplicito su tutti i nostri tipi. L'aggiunta di nuove operazioni è gratuita poiché non importa quale sia l'operazione.

Il metodo 3 è libero di aggiungere nuovi tipi e freeish per aggiungere nuove operazioni - l'unica modifica è l'aumento di vtable. Ciò avrà alcuni effetti a causa della dimensione dell'oggetto, ma sarà generalmente inferiore alla maggiore iterazione rispetto ai tipi.

+0

Grazie. Domanda: 1. Non ci sarebbe un check in apply_visitor() anche per il metodo 1, anche se sarebbe un unico livello per verificare con quale forma viene chiamato il visitatore? Il che significa Metodo 2 ha "1 ulteriore controllo": Così Metodo 1: if (shape.which() == 0) {} ... else if (shape.which() == 1) { ...} Metodo 2: - come raffigurato, in modo da 2 controlli invece di 1. così un ulteriore "se" condizione è che costoso? – Sid

+0

Il mio punto è, con più di due tipi, posso capire che potrebbero esserci più confronti, ma nel mio esempio ci sono solo 2 tipi, quindi il Metodo 2 dovrebbe risultare in "1" in più rispetto al Metodo 2. L'impatto sulle prestazioni sembra essere sproporzionatamente grande. – Sid

+0

@Sid Non due tipi, due * varianti *. Non hai un altro confronto, hai un altro confronto annidato. Non è come aggiungere un altro tipo nella variante del metodo 1. – Barry