Prestazioni: ricorsione contro iterazione in Javascript


24

Ho letto di recente alcuni articoli (ad esempio http://dailyjs.com/2012/09/14/functional-programming/ ) sugli aspetti funzionali di Javascript e la relazione tra Scheme e Javascript (quest'ultimo è stato influenzato dal primo, che è un linguaggio funzionale, mentre gli aspetti OO sono ereditati dal Sé, che è un linguaggio basato sulla prototipazione).

Tuttavia, la mia domanda è più specifica: mi chiedevo se ci sono metriche sull'esecuzione della ricorsione rispetto all'iterazione in Javascript.

So che in alcune lingue (dove l'iterazione per progettazione funziona meglio) la differenza è minima perché l'interprete / compilatore converte la ricorsione in iterazione, tuttavia suppongo che probabilmente non sia il caso di Javascript poiché è, almeno parzialmente, funzionale linguaggio.


3
Fai il tuo test e scoprilo
TehShrike,

con la generosità e una risposta che menziona TCO. Sembra che ES6 specifichi il TCO ma finora nessuno lo implementa se crediamo kangax.github.io/compat-table/es6 Mi sto perdendo qualcosa?
Matthias Kauer,

Risposte:


28

JavaScript non esegue l'ottimizzazione della ricorsione della coda , quindi se la ricorsione è troppo profonda, è possibile che si verifichi un overflow dello stack di chiamate. L'iterazione non presenta tali problemi. Se pensi di dover ricorrere troppo e hai davvero bisogno di ricorsione (ad esempio, per riempire le inondazioni), sostituisci la ricorsione con il tuo stack.

Le prestazioni di ricorsione sono probabilmente peggiori delle prestazioni di iterazione, poiché le chiamate e i ritorni di funzioni richiedono la conservazione e il ripristino dello stato, mentre l'iterazione passa semplicemente a un altro punto di una funzione.


Mi chiedo solo ... Ho visto un po 'di codice in cui viene creato un array vuoto e il sito della funzione ricorsiva viene assegnato a una posizione nell'array e quindi viene restituito il valore memorizzato nell'array. È questo che intendi con "sostituisci la ricorsione con il tuo stack"? Ad esempio: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
mastazi il

@mastazi: No, questo renderà un inutile stack di chiamate insieme a quello interno. Intendevo qualcosa di simile al flusso di alluvione basato sulla coda di Wikipedia .
Triang3l

Vale la pena notare che una lingua non esegue il TCO, ma un'implementazione potrebbe. Il modo in cui le persone ottimizzano JS significa che forse il TCO potrebbe apparire in alcune implementazioni
Daniel Gratzer,

1
@mastazi Sostituisci elsein quella funzione con else if (stack[n-1]) { return stack[n-1]; } elsee hai memoization . Chiunque abbia scritto quel codice fattoriale aveva un'implementazione incompleta (e probabilmente avrebbe dovuto usarlo stack[n]ovunque invece che stack[n-1]).
Izkata,

Grazie @Izkata, faccio spesso quel tipo di ottimizzazione ma fino ad oggi non conoscevo il suo nome. Avrei dovuto studiare CS invece di IT ;-)
mastazi

20

Aggiornamento : da ES2015, JavaScript ha il TCO , quindi parte dell'argomento seguente non è più valido .


Sebbene Javascript non abbia l'ottimizzazione delle chiamate di coda, la ricorsione è spesso il modo migliore di procedere. E sinceramente, tranne nei casi limite, non otterrai overflow dello stack di chiamate.

Le prestazioni sono qualcosa da tenere a mente, ma anche l'ottimizzazione prematura. Se pensi che la ricorsione sia più elegante dell'iterazione, allora provaci. Se si scopre che questo è il tuo collo di bottiglia (che potrebbe non essere mai), puoi sostituirlo con qualche brutta iterazione. Ma la maggior parte delle volte, il collo di bottiglia sta nelle manipolazioni del DOM o più in generale nell'I / O, non nel codice stesso.

La ricorsione è sempre più elegante 1 .

1 : opinione personale.


3
Concordo sul fatto che la ricorsione sia più elegante e che l'eleganza sia importante in quanto è leggibilità e manutenibilità (questo è soggettivo, ma a mio avviso la ricorsione è molto facile da leggere, quindi mantenibile). Tuttavia, a volte le prestazioni contano; puoi sostenere l'affermazione secondo cui la ricorsione è il modo migliore di procedere, anche dal punto di vista delle prestazioni?
Mastazi,

3
@mastazi, come detto nella mia risposta, dubito che la ricorsione sarà il tuo collo di bottiglia. Il più delle volte, è la manipolazione del DOM, o più in generale l'I / O. E non dimenticare che l'ottimizzazione prematura è la radice di tutti i mali;)
Florian Margaine,

+1 per la manipolazione del DOM essendo un collo di bottiglia il più delle volte! Ricordo un'intervista molto interessante a Yehuda Katz (Ember.js) a riguardo.
Mastazi,

1
@mike La definizione di "prematuro" è "matura o matura prima del momento opportuno". Se sai che fare ricorsivamente qualcosa causerà uno stackoverflow, non è prematuro. Tuttavia, se si assume per capriccio (senza dati reali), allora è prematuro.
Zirak,

2
Con Javascript non hai la quantità di stack disponibile per il programma. Potresti avere un piccolo stack in IE6 o un grande stack in FireFox. Gli algoritmi ricorsivi raramente hanno una profondità fissa a meno che non si stia eseguendo un ciclo ricorsivo in stile Schema. Non sembra proprio che la ricorsione non basata su loop si adatti ad evitare ottimizzazioni premature.
mike30,

7

Ero piuttosto curioso di questa prestazione anche in javascript, quindi ho fatto alcuni esperimenti (anche se su una versione precedente di nodo). Ho scritto una calcolatrice fattoriale in modo ricorsivo rispetto alle iterazioni e l'ho eseguita alcune volte a livello locale. Il risultato è sembrato piuttosto pesantemente distorto verso la ricorsione con una tassa (prevista).

Codice: https://github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22

Puoi provarlo con "use strict";e vedere se fa la differenza. (Produrrà jumps invece della sequenza di chiamate standard)
Bardana

1
Su una versione recente del nodo (6.9.1) ho ottenuto risultati estremamente simili. C'è un po 'di una tassa sulla ricorsione, ma la considero un caso limite: una differenza di 400 ms per 1.000.000 di loop è di 0,0025 ms per loop. Se stai facendo 1.000.000 di loop è qualcosa da tenere a mente.
Kelz,

6

Come da richiesta dell'OP, farò un chip (senza farmi ingannare, spero: P)

Penso che siamo tutti d'accordo sul fatto che la ricorsione sia solo un modo più elegante di codifica. Se fatto bene può rendere il codice più gestibile, che è IMHO altrettanto importante (se non di più) che radersi 0,0001 ms.

Per quanto riguarda l'argomento secondo il quale JS non esegue l'ottimizzazione della coda, non è più del tutto vero, l' uso della modalità rigorosa di ECMA5 abilita il TCO. Era qualcosa di cui non ero molto felice qualche tempo fa, ma almeno ora so perché arguments.calleegenera errori in modalità rigorosa. Conosco il link sopra riportato a una segnalazione di bug, ma il bug è impostato su WONTFIX. Inoltre, sta arrivando il TCO standard: ECMA6 (dicembre 2013).

Istintivamente, e attenendosi alla natura funzionale di JS, direi che la ricorsione è lo stile di codifica più efficiente il 99,99% delle volte. Tuttavia, Florian Margaine ha ragione quando afferma che è probabile che il collo di bottiglia si trovi altrove. Se stai manipolando il DOM, probabilmente ti stai concentrando sulla scrittura del codice nel modo più gestibile possibile. L'API DOM è quella che è: lenta.

Penso che sia quasi impossibile offrire una risposta definitiva su quale sia l'opzione più veloce. Ultimamente, molti jspref che ho visto mostrano che il motore V8 di Chrome è incredibilmente veloce in alcune attività, che funzionano 4 volte più lentamente su SpiderMonkey di FF e viceversa. I moderni motori JS hanno ogni sorta di asso nella manica per ottimizzare il tuo codice. Non sono un esperto, ma credo che V8, ad esempio, sia altamente ottimizzato per le chiusure (e la ricorsione), mentre il motore JScript di MS non lo è. SpiderMonkey si comporta spesso meglio quando è coinvolto il DOM ...

In breve: direi quale tecnica più performante è, come sempre in JS, quasi impossibile da prevedere.


3

Senza modalità rigorosa, le prestazioni di iterazione sono in genere leggermente più veloci della ricorsione ( oltre a far lavorare di più JIT ). L'ottimizzazione della ricorsione della coda elimina sostanzialmente qualsiasi differenza evidente perché trasforma l'intera sequenza di chiamate in un salto.

Esempio: Jsperf

Suggerirei di preoccuparmi molto di più della chiarezza e della semplicità del codice quando si tratta di scegliere tra ricorsione e iterazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.