2011-01-09 24 views
5

Ho il seguente codice Clojure per calcolare un numero con una certa proprietà "factorable". (quello che fa esattamente il codice è secondario).Non dovrebbe anche una funzione ricorsiva in coda essere più veloce?

(defn factor-9 
    ([] 
    (let [digits (take 9 (iterate #(inc %) 1)) 
      nums (map (fn [x] ,(Integer. (apply str x))) (permutations digits))] 
     (some (fn [x] (and (factor-9 x) x)) nums))) 
    ([n] 
     (or 
     (= 1 (count (str n))) 
     (and (divisible-by-length n) (factor-9 (quot n 10)))))) 

ora, sono in TCO e si rendono conto che Clojure può fornire solo coda ricorsione se esplicitamente detto in modo da utilizzare la parola chiave recur. Così ho riscritto il codice per farlo (sostituendo il fattore-9 con ripresentarsi essendo l'unica differenza):

(defn factor-9 
    ([] 
    (let [digits (take 9 (iterate #(inc %) 1)) 
      nums (map (fn [x] ,(Integer. (apply str x))) (permutations digits))] 
     (some (fn [x] (and (factor-9 x) x)) nums))) 
    ([n] 
     (or 
     (= 1 (count (str n))) 
     (and (divisible-by-length n) (recur (quot n 10)))))) 

A mia conoscenza, TCO ha un doppio vantaggio. Il primo è che non usa lo stack pesantemente come una chiamata non ricorsiva alla coda e quindi non lo fa saltare su ricursioni più grandi. Il secondo, penso sia che di conseguenza è più veloce poiché può essere convertito in loop.

Ora, ho fatto un benchmark molto approssimativo e non ho visto alcuna differenza tra le due implementazioni. Ho sbagliato nella mia seconda ipotesi o ha qualcosa a che fare con l'esecuzione sulla JVM (che non ha TCO automatico) e recur usando un trucco per raggiungerlo?

Grazie.

risposta

6

L'uso di ripresentarsi non accelerare le cose, ma solo di circa 3 nanosecondi (veramente) oltre una chiamata ricorsiva. Quando le cose diventano così piccole possono essere nascoste nel rumore del resto del test. Ho scritto quattro test (link sotto) che sono in grado di illustrare la differenza di prestazioni.

Vorrei anche suggerire di utilizzare qualcosa come criterium durante il benchmarking. (Stack Overflow non mi consente di pubblicare più di 1 link poiché non ho alcuna reputazione di cui parlare, quindi dovrai cercarlo su google, forse "criterium clojure")

Per motivi di formattazione, I ' abbiamo messo i test e i risultati in questo gist.

In breve, per confrontare relativamente, se il test ricorsivo è 1, allora il test di looping è di circa 0,45, e il TCO test di circa 0,87 e la differenza assoluta tra i test ricorsivi e TCO sono di circa 3ns.

Ovviamente, all si applicano le avvertenze relative al benchmarking.

+0

Questo è interessante, ho impostato criterium per misurare il fattore-9 e ho ottenuto che la versione TCO è in realtà un po 'più lenta rispetto alla versione non ottimizzata. Vedi https://gist.github.com/773060. –

+0

Sorprendentemente (almeno per me :)) c'è un mondo di differenza quando si confrontano le due versioni per un semplice calcolo di Fibonacci: https://gist.github.com/773068 –

+0

Questo è piuttosto drammatico, non è vero? Gioie di punti di riferimento. Almeno hai una tesi convincente che il TCO fa una grande differenza :-) – hutch

-1

Dopo aver valutato factor-9 (quot n 10) un and e un or deve essere valutato prima che la funzione possa tornare. Quindi non è coda-ricorsiva.

+0

A meno che 'and' e' o' si comportino diversamente in clojure che in altre lisps (che dubito), non è vero. '(e x y)' dovrebbe essere equivalente a '(se x y false)' e '(o x y)' a '(se x vero y)', quindi 'y' è in posizione di coda in entrambi i casi. – sepp2k

+0

@ sepp2k Ciò significa che il programma può essere convertito in uno equivalente coda-ricorsivo. Ma questo fa sì che il programma stesso ricorra alla coda? Se sì, come posso votare la mia risposta? – Oswald

+0

Poiché la macro-espansione avviene in fase di compilazione e dopo la macro-espansione si ha un programma ricorsivo in coda, sì rende il programma ricorsivo in coda. Inoltre: se usi 'recur' in una posizione che non è la coda, ottieni effettivamente un errore. Non puoi downvotare la tua risposta, ma puoi eliminarla facendo clic sul link "elimina" che si trova tra il corpo della tua risposta e i commenti. – sepp2k

2

Quando si ottimizza qualsiasi codice, è opportuno iniziare da colli di bottiglia potenziali o effettivi e ottimizzarlo prima.

Mi sembra che questo particolare pezzo di codice sta mangiando la maggior parte del tempo di CPU:

(map (fn [x] ,(Integer. (apply str x))) (permutations digits)) 

E questo non dipende da TCO in alcun modo - viene eseguita in stesso modo. Quindi, la chiamata di coda in questo particolare esempio ti consentirà di non utilizzare tutto lo stack, ma per ottenere prestazioni migliori, prova a ottimizzarlo.

+0

Non penso che la risposta sia corretta. Innanzitutto, le permutazioni danno un pigro seq in modo che la valutazione di (alcuni ...) attiverà il calcolo della permutazione solo se necessario. In secondo luogo, a seconda del comportamento di runtime del fattore-9 questo potrebbe essere rilevante o meno. Dall'altro lato, i consigli generali sull'analisi * di cosa * ottimizzare sono corretti. Se il fattore di funzione-9 è il collo di bottiglia, la domanda su TCO o meno dipende solo dalle dimensioni dello stack, né dalle prestazioni di runtime. – ordnungswidrig

+0

@ordnungswidrig: abbastanza possibile. Non essendo in grado di eseguire il codice, ho scelto il pezzo più sospetto. Aspetterò il feedback da Balint e poi lo modificherò se necessario. Il consiglio sull'identificazione dei colli di bottiglia è applicabile per * qualsiasi * problema di ottimizzazione del codice. –

+0

Goran, penso che ordnungswidrig sia corretto, le permutazioni verranno recuperate secondo necessità, ma sai di un profiler che clojure posso verificare questa ipotesi con? –

1

solo un promemoria gentile che non ha clojure TCO

+0

La JVM su cui gira Clojure non ha un TCO automatico ma può essere ingannato nel farlo per auto-ricorsione e quando è esplicito al riguardo con 'recur' –

+0

no ha un costrutto di ciclo chiamato recur. sintatticamente somiglia a una coda, anche se non è perché può essere utilizzata per entrambi i cicli e le funzioni. –

+0

+1 per essere l'unica persona a farlo notare. Aggiungo anche che le discussioni qui sono specifiche per il sottogruppo di chiamate tail che "recur" di Clojure possono gestire e non sono applicabili all'ottimizzazione delle chiamate tail in generale. –

Problemi correlati