I compilatori possono e possono convertire la logica ricorsiva in logica non ricorsiva equivalente?


15

Ho imparato F # e sta iniziando a influenzare il modo in cui penso quando sto programmando C #. A tal fine, ho usato la ricorsione quando sento che il risultato migliora la leggibilità e non riesco a immaginarlo che si esaurisca in un overflow dello stack.

Questo mi porta a chiedere se i compilatori potrebbero o meno convertire automaticamente le funzioni ricorsive in una forma equivalente non ricorsiva?


l'ottimizzazione delle chiamate di coda è un buon esempio di base, ma funziona solo se hai return recursecall(args);per la ricorsione, le cose più complesse sono possibili creando uno stack esplicito e liquidandolo, ma dubito che lo faranno
maniaco del cricco

@ratchet maniaco: ricorsione non significa "calcolo che utilizza uno stack".
Giorgio,

1
@Giorgio Lo so, ma uno stack è il modo più semplice per convertire la ricorsione in un loop
maniaco del cricchetto

Risposte:


21

Sì, alcune lingue e compilatori convertiranno la logica ricorsiva in logica non ricorsiva. Questo è noto come ottimizzazione delle chiamate di coda - notare che non tutte le chiamate ricorsive sono ottimizzabili per le chiamate di coda. In questa situazione, il compilatore riconosce una funzione del modulo:

int foo(n) {
  ...
  return bar(n);
}

Qui, la lingua è in grado di riconoscere che il risultato che viene restituito è il risultato di un'altra funzione e cambiare una chiamata di funzione con un nuovo frame dello stack in un salto.

Realizza che il classico metodo fattoriale:

int factorial(n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return n * factorial(n - 1);
}

non è ottimizzabile per le chiamate di coda a causa dell'ispezione necessaria al ritorno.

Per rendere ottimizzabile questa chiamata in coda,

int _fact(int n, int acc) {
    if(n == 1) return acc;
    return _fact(n - 1, acc * n);
}

int factorial(int n) {
    if(n == 0) return 1;
    return _fact(n, 1);
}

Compilando questo codice con gcc -O2 -S fact.c(il -O2 è necessario per abilitare l'ottimizzazione nel compilatore, ma con più ottimizzazioni del -O3 diventa difficile da leggere per un essere umano ...)

_fact:
.LFB0:
        .cfi_startproc
        cmpl    $1, %edi
        movl    %esi, %eax
        je      .L2
        .p2align 4,,10
        .p2align 3
.L4:
        imull   %edi, %eax
        subl    $1, %edi
        cmpl    $1, %edi
        jne     .L4
.L2:
        rep
        ret
        .cfi_endproc

Si può vedere nel segmento .L4, jnepiuttosto che un call(che fa una chiamata di subroutine con un nuovo frame dello stack).

Si noti che questo è stato fatto con C. L'ottimizzazione delle chiamate di coda in Java è difficile e dipende dall'implementazione di JVM: coda-ricorsione + java e coda-ricorsione + ottimizzazione sono buoni set di tag da sfogliare. Si possono trovare altre lingue JVM sono in grado di ottimizzare la ricorsione in coda migliore (prova clojure (che richiede il ripresentarsi alla chiamata di coda ottimizzare), o di scala).


1
Non sono sicuro che questo sia ciò che l'OP sta chiedendo. Solo perché il runtime consuma o non consuma spazio nello stack in un certo modo, non significa che la funzione non sia ricorsiva.

1
@MattFenwick Come intendi? "Questo mi porta a chiedere se i compilatori possano o meno convertire automaticamente le funzioni ricorsive in una forma non ricorsiva equivalente" - la risposta è "sì in determinate condizioni". Le condizioni sono dimostrate e ci sono alcuni gotcha in alcune altre lingue popolari con ottimizzazioni di coda che ho citato.

9

Percorri attentamente.

La risposta è sì, ma non sempre, e non tutti. Questa è una tecnica che prende alcuni nomi diversi, ma puoi trovare alcune informazioni piuttosto definitive qui e su Wikipedia .

Preferisco il nome "Tail Call Optimization", ma ce ne sono altri e alcune persone confonderanno il termine.

Detto questo, ci sono un paio di cose importanti da realizzare:

  • Per ottimizzare una chiamata di coda, la chiamata di coda richiede parametri noti al momento della chiamata. Ciò significa che se uno dei parametri è una chiamata alla funzione stessa, non può essere convertito in un ciclo, poiché ciò richiederebbe un annidamento arbitrario di detto ciclo che non può essere espanso al momento della compilazione.

  • C # fa non in modo affidabile le chiamate di coda ottimizzare. L'IL ha le istruzioni per fare ciò che il compilatore F # emetterà, ma il compilatore C # lo emetterà in modo incoerente e, a seconda della situazione JIT, il JIT può o meno farlo. Tutte le indicazioni sono che non dovresti fare affidamento sull'ottimizzazione delle chiamate in coda in C #, il rischio di overflow nel farlo è significativo e reale


1
Sei sicuro che questo sia ciò che l'OP sta chiedendo? Come ho pubblicato sotto l'altra risposta, solo perché il runtime consuma o non consuma spazio nello stack in un certo modo, non significa che la funzione non sia ricorsiva.

1
@MattFenwick che in realtà è un ottimo punto, in termini reali dipende, il compilatore F # che emette le istruzioni di chiamata di coda sta mantenendo completamente la logica ricorsiva, sta solo istruendo la JIT ad eseguirla in un modo di sostituzione dello spazio dello stack piuttosto che dello spazio dello spazio- in crescita, tuttavia altri compilatori possono letteralmente compilarsi in un ciclo. (Tecnicamente la JIT si sta compilando in un loop o forse anche in modalità loopless se il loop è totalmente in primo piano)
Jimmy Hoffa,
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.