Ruby esegue l'ottimizzazione delle chiamate di coda?


92

I linguaggi funzionali portano all'uso della ricorsione per risolvere molti problemi e quindi molti di essi eseguono l'ottimizzazione delle chiamate di coda (TCO). Il TCO fa sì che le chiamate a una funzione da un'altra funzione (o da se stessa, nel qual caso questa funzione è anche nota come Tail Recursion Elimination, che è un sottoinsieme del TCO), come ultimo passaggio di quella funzione, non necessiti di un nuovo stack frame, che riduce l'overhead e l'utilizzo della memoria.

Ruby ovviamente ha "preso in prestito" una serie di concetti dai linguaggi funzionali (lambda, funzioni come map e così via, ecc.), Il che mi incuriosisce: Ruby esegue l'ottimizzazione delle chiamate di coda?

Risposte:


127

No, Ruby non esegue il TCO. Tuttavia, inoltre, non esegue il TCO.

La specifica del linguaggio Ruby non dice nulla sul TCO. Non dice che devi farlo, ma non dice nemmeno che non puoi farlo. Non puoi fare affidamento su di esso.

Questo è diverso da Scheme, in cui la specifica della lingua richiede che tutte le implementazioni debbano rispettare il TCO. Ma è anche diverso da Python, dove Guido van Rossum ha chiarito in più occasioni (l'ultima volta solo un paio di giorni fa) che le implementazioni Python non dovrebbero eseguire il TCO.

Yukihiro Matsumoto è favorevole al TCO, semplicemente non vuole costringere tutte le implementazioni a supportarlo. Sfortunatamente, questo significa che non puoi fare affidamento sul TCO o, se lo fai, il tuo codice non sarà più trasferibile su altre implementazioni Ruby.

Quindi, alcune implementazioni di Ruby eseguono il TCO, ma la maggior parte non lo fa. YARV, ad esempio, supporta il TCO, sebbene (per il momento) sia necessario decommentare esplicitamente una riga nel codice sorgente e ricompilare la VM, per attivare il TCO - nelle versioni future sarà attivo per impostazione predefinita, dopo che l'implementazione avrà dimostrato stabile. La Parrot Virtual Machine supporta nativamente il TCO, quindi anche Cardinal potrebbe supportarlo abbastanza facilmente. Il CLR ha un certo supporto per TCO, il che significa che IronRuby e Ruby.NET potrebbero probabilmente farlo. Probabilmente potrebbe farlo anche Rubinius.

Ma JRuby e XRuby non supportano il TCO e probabilmente non lo faranno, a meno che la stessa JVM non ottenga il supporto per il TCO. Il problema è questo: se vuoi avere un'implementazione veloce e un'integrazione veloce e senza soluzione di continuità con Java, allora dovresti essere compatibile con lo stack con Java e utilizzare lo stack della JVM il più possibile. Puoi implementare abbastanza facilmente il TCO con trampolini o uno stile di continuazione esplicito, ma poi non stai più utilizzando lo stack JVM, il che significa che ogni volta che vuoi chiamare Java o chiamare da Java a Ruby, devi eseguire una sorta di conversione, che è lenta. Quindi, XRuby e JRuby hanno scelto di andare con la velocità e l'integrazione di Java su TCO e continuazioni (che fondamentalmente hanno lo stesso problema).

Questo vale per tutte le implementazioni di Ruby che vogliono integrarsi strettamente con qualche piattaforma host che non supporta nativamente il TCO. Ad esempio, immagino che MacRuby avrà lo stesso problema.


2
Potrei sbagliarmi (per favore illuminami se è così), ma dubito che il TCO abbia senso nei veri linguaggi OO, poiché la chiamata di coda deve essere in grado di riutilizzare lo stack frame del chiamante. Poiché con l'associazione tardiva, non è noto in fase di compilazione quale metodo verrà invocato dall'invio di un messaggio, sembra difficile garantire che (magari con un JIT di feedback del tipo, o forzando tutti gli implementatori di un messaggio a utilizzare gli stack frame della stessa dimensione, o limitando il TCO all'autoinvio dello stesso messaggio ...).
Damien Pollet,

2
È un'ottima risposta. Quelle informazioni non sono facilmente reperibili tramite Google. È interessante che Yarv lo supporti.
Charlie Flowers,

15
Damien, risulta che il TCO è effettivamente richiesto per i veri linguaggi OO: vedi projectfortress.sun.com/Projects/Community/blog/… . Non preoccuparti troppo delle cose dello stack frame: è perfettamente possibile progettare gli stack frame in modo sensato in modo che funzionino bene con il TCO.
Tony Garnock-Jones

2
tonyg ha salvato il post di riferimento di GLS dall'estinzione, rispecchiandolo qui: ottanta-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar

Sto svolgendo un compito a casa che mi richiede di smontare una serie di array annidati di profondità arbitraria. Il modo ovvio per farlo è ricorsivamente, e casi d'uso simili online (che posso trovare) usano la ricorsione. È molto improbabile che il mio problema particolare esploda, anche senza TCO, ma il fatto che non riesca a scrivere una soluzione completamente generale senza passare all'iterazione mi dà fastidio.
Isaac Rabinovitch

42

Aggiornamento: ecco una bella spiegazione del TCO in Ruby: http://nithinbekal.com/posts/ruby-tco/

Aggiornamento: potresti anche controllare la gemma tco_method : http://blog.tdg5.com/introducing-the-tco_method-gem/

In Ruby MRI (1.9, 2.0 e 2.1) puoi attivare il TCO con:

RubyVM::InstructionSequence.compile_option = {
  :tailcall_optimization => true,
  :trace_instruction => false
}

C'era una proposta per attivare il TCO per impostazione predefinita in Ruby 2.0. Spiega anche alcuni problemi che ne derivano: Ottimizzazione delle chiamate di coda: abilitare per impostazione predefinita ?.

Breve estratto dal link:

Generalmente, l'ottimizzazione della ricorsione in coda include un'altra tecnica di ottimizzazione: la "chiamata" alla traduzione "salta". A mio parere, è difficile applicare questa ottimizzazione perché riconoscere la "ricorsione" è difficile nel mondo di Ruby.

Prossimo esempio. L'invocazione del metodo fact () nella clausola "else" non è una "chiamata di coda".

def fact(n) 
  if n < 2
    1 
 else
   n * fact(n-1) 
 end 
end

Se si desidera utilizzare l'ottimizzazione della chiamata di coda sul metodo fact (), è necessario modificare il metodo fact () come segue (stile di passaggio di continuazione).

def fact(n, r) 
  if n < 2 
    r
  else
    fact(n-1, n*r)
  end
end

12

Può avere, ma non è garantito:

https://bugs.ruby-lang.org/issues/1256


Il collegamento è morto per ora.
karatedog

@karatedog: grazie, aggiornato. Anche se ad essere onesti il ​​riferimento è probabilmente obsoleto, poiché il bug ha ormai 5 anni e da allora c'è stata attività sullo stesso argomento.
Steve Jessop

Sì :-) Ho appena letto dell'argomento e ho visto che in Ruby 2.0 può essere abilitato dal codice sorgente (niente più modifiche al sorgente C e ricompilazione).
karatedog


2

Questo si basa sulle risposte di Jörg ed Ernest. Fondamentalmente dipende dall'implementazione.

Non sono riuscito a far funzionare la risposta di Ernest sulla risonanza magnetica, ma è fattibile. Ho trovato questo esempio che funziona per MRI da 1.9 a 2.1. Questo dovrebbe stampare un numero molto grande. Se non imposti l'opzione TCO su true, dovresti ricevere l'errore "stack too deep".

source = <<-SOURCE
def fact n, acc = 1
  if n.zero?
    acc
  else
    fact n - 1, acc * n
  end
end

fact 10000
SOURCE

i_seq = RubyVM::InstructionSequence.new source, nil, nil, nil,
  tailcall_optimization: true, trace_instruction: false

#puts i_seq.disasm

begin
  value = i_seq.eval

  p value
rescue SystemStackError => e
  p e
end
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.