2012-06-03 24 views
7

Ecco una semplice classe per iterare su un intervallo numerico multidimensionale:Perché la versione ricorsiva di questa funzione è più veloce?

#include <array> 
#include <limits> 

template <int N> 
class NumericRange 
{ 
public: 
    // typedef std::vector<double>::const_iterator const_iterator; 
    NumericRange() { 
    _lower.fill(std::numeric_limits<double>::quiet_NaN()); 
    _upper.fill(std::numeric_limits<double>::quiet_NaN()); 
    _delta.fill(std::numeric_limits<double>::quiet_NaN()); 
    } 
    NumericRange(const std::array<double, N> & lower, const std::array<double, N> & upper, const std::array<double, N> & delta): 
    _lower(lower), _upper(upper), _delta(delta) { 
    _state.fill(std::numeric_limits<double>::quiet_NaN()); 
    _next_index_to_advance = 0; 
    } 

    const std::array<double, N> & get_state() const { 
    return _state; 
    } 

    void start() { 
    _state = _lower; 
    } 

    bool in_range(int index_to_advance = N-1) const { 
    return (_state[ index_to_advance ] - _upper[ index_to_advance ]) < _delta[ index_to_advance ]; 
    } 

    void advance(int index_to_advance = 0) { 
    _state[ index_to_advance ] += _delta[ index_to_advance ]; 
    if (! in_range(index_to_advance)) { 
     if (index_to_advance < N-1) { 
    // restart index_to_advance 
    _state[index_to_advance] = _lower[index_to_advance]; 

    // carry 
    index_to_advance; 
    advance(index_to_advance+1); 
     } 
    } 
    } 

private: 
    std::array<double, N> _lower, _upper, _delta, _state; 
    int _next_index_to_advance; 
}; 

int main() { 
    std::array<double, 7> lower{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; 
    std::array<double, 7> upper{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; 
    std::array<double, 7> delta{0.03, 0.06, 0.03, 0.06, 0.03, 0.06, 0.03}; 

    NumericRange<7> nr(lower, upper, delta); 
    int c = 0; 
    for (nr.start(); nr.in_range(); nr.advance()) { 
    const std::array<double, 7> & st = nr.get_state(); 
    ++c; 
    } 
    std::cout << "took " << c << " steps" << std::endl; 

    return 0; 
} 

Quando si sostituisce la funzione advance con una variante non ricorsiva, il runtime aumenti:

void advance(int index_to_advance = 0) { 
    bool carry; 
    do { 
    carry = false; 
    _state[ index_to_advance ] += _delta[ index_to_advance ]; 
    if (! in_range(index_to_advance)) { 
     if (index_to_advance < N-1) { 
    // restart index_to_advance 
    _state[index_to_advance] = _lower[index_to_advance]; 

    // carry 
    ++index_to_advance; 
    carry = true; 
    // advance(index_to_advance); 
     } 
    } 
    } while (carry); 
} 

Runtimes erano preso usando l'ora utente di Unix tramite il comando time. Il codice è stato compilato usando gcc-4.7 con le opzioni -std=c++11 -O3 (ma penso che dovrebbe funzionare con c++0x su gcc-4.6). La versione ricorsiva ha preso 13s e la versione iterativa ha richiesto 30 secondi. Entrambi richiedono lo stesso numero di chiamate advance per terminare (e se si stampa l'array for(ns.start()...) all'interno del ciclo , entrambi eseguono la stessa operazione).

Questo è un divertente indovinello! Aiutami a capire perché ricorsivo sarebbe più efficiente/più ottimizzabile.

+2

Prova profilo. 'valgrind --callgrind' è un buon profiler. –

+0

Personalmente mi piace gdb. Break + backtrace –

+0

Le prestazioni che sto vedendo sono incoerenti. Per la versione iterativa, ottengo 30 o 100 secondi su diverse esecuzioni. Forse c'è un problema di memorizzazione nella cache sottile. –

risposta

13

La versione ricorsiva è un esempio di coda ricorsione che significa che il compilatore può trasformare la ricorsione in iterazione. Ora, una volta che viene eseguita la trasformazione, la funzione ricorsiva sarebbe simile a questa:

void advance(int index_to_advance = 0) { 
    _state[ index_to_advance ] += _delta[ index_to_advance ]; 
    while (!in_range(index_to_advance) && index_to_advance < N-1) { 
     // restart index_to_advance 
     _state[index_to_advance] = _lower[index_to_advance]; 

     // carry 
     ++index_to_advance; 
     _state[ index_to_advance ] += _delta[ index_to_advance ]; 
    } 
    } 

Come vedi la tua versione contiene un test in più e la variabile di condizione. Il ciclo, se si guarda da vicino è equivalente a

for(; index_to_advance < N-1 && !in_range(index_to_advance);++index_to_advance) 

(rimuovendo il ++index_to_advance alla fine), e l'ottimizzatore potrebbe avere una migliore possibilità di svolgimento questo.

Detto questo, non penso che questo spieghi l'enorme differenza di tempo, anche se spiega perché la versione ricorsiva non è molto più lenta di quella iterativa. Controlla l'assemblaggio generato per vedere cosa ha effettivamente fatto il compilatore.

+0

Sei sicuro che la versione 'TCO' sia accurata? Non ha mai completato per me (~ 30 minuti). Non ho esaminato il problema anche se – sehe

+0

@sehe: hai ragione, la trasformazione non è corretta, la prima riga deve essere all'interno del ciclo (vale a dire deve essere applicata a ciascuna iterazione) ... –

+0

+1 Ottima spiegazione. – user

3

Giusto per aggiungere ulteriori dettagli a ciò che ha detto David Rodriguez:

Con l'ottimizzazione ricorsione in coda, la funzione diventa:

void advance(int index_to_advance = 0) { 
    top: 
    _state[ index_to_advance ] += _delta[ index_to_advance ]; 
    if (! in_range(index_to_advance)) { 
    if (index_to_advance < N-1) { 
     // restart index_to_advance 
     _state[index_to_advance] = _lower[index_to_advance]; 

     // carry 
     ++index_to_advance; 
     goto top; 
    } 
    } 
} 

e questo effettivamente avere le stesse prestazioni della versione ricorsiva sul mio sistema (g ++ 4.6.3 -std = C++ 0x)

+0

+1 È così strano per me che un 'etichetta ... goto' sarebbe più efficiente (puoi fare cose con 'label ... goto' che si muove tra scope e fare il punt del compilatore), ma posso capire perché in questo caso. Grazie! – user

Problemi correlati