2014-11-14 14 views
6

Di seguito sono descritte due funzioni per trovare il valore massimo di un elenco di numeri.Istanza di funzione poco ricorsiva non a coda

mx :: (Ord a) => [a] -> a 
mx [] = error "Empty list" 
mx [x] = x 
mx (x:xs) 
    | x > (mx xs) = x 
    | otherwise = (mx xs) 

mx' (x:xs) = findMax x xs 
    where 
    findMax cmx [] = cmx 
    findMax cmx (x:xs) | x > cmx = findMax x xs 
       | otherwise = findMax cmx xs 

main = do 
    print $ mx [1..30] 

Timing il codice precedente, prima per mx'(tail-ricorsiva) e successivo per mx (non-tail-ricorsiva), abbiamo le seguenti temporizzazioni.

Lenovo-IdeaPad-Y510P:/tmp$ time ./t 
30 

real 0m0.002s 
user 0m0.000s 
sys 0m0.001s 
Lenovo-IdeaPad-Y510P:/tmp$ ghc -O2 t.hs 
[1 of 1] Compiling Main    (t.hs, t.o) 
Linking t ... 
Lenovo-IdeaPad-Y510P:/tmp$ time ./t 
30 

real 0m6.272s 
user 0m6.274s 
sys 0m0.000s 

Qualcuno può spiegare perché c'è una così grande differenza di prestazioni per un elenco di soli 30 elementi?

risposta

11

Come altri hanno sottolineato, GHC non fa eliminazione sottoespressione comune (CSE), causando il vostro primo frammento per l'esecuzione in tempo esponenziale.

Per capire perché, si consideri ad es.

test1 = length [1..1000] + sum [1..1000] 
test2 = let l = [1..1000] in length l + sum l 

I due esempi sono semanticamente equivalenti, ma test1 eseguito in spazio costante mentre test2 in spazio lineare (l'intero 1000 cellule vengono allocati). Fondamentalmente, in questo caso CSE nega i vantaggi della pigrizia.

Poiché CSE può portare a peggiore prestazione, GHC è piuttosto conservativo nell'applicarlo.

spiegazione più GHC FAQs:

https://www.haskell.org/haskellwiki/GHC/FAQ#Does_GHC_do_common_subexpression_elimination.3F

3

Calling mx [1..3] risultati in seguito ad inviti:

mx [1..3] 

    mx [2..3] -- x > (mx xs) in mx [1..3] 
     mx [3] -- x > (mx xs) in mx [1..2] 
     mx [3] -- otherwise = (mx xs) in mx [1..2] 

    mx [2..3] -- otherwise = (mx xs) in mx [1..3] 
     mx [3] -- x > (mx xs) in mx [1..2] 
     mx [3] -- otherwise = (mx xs) in mx [1..2] 

Il numero di chiamate mx per la ricerca di massima di [1..n] è O(2^n): 2^n - 1, esattamente.

mx' rende O(n) chiamate: n + 1, esattamente.

mx' [1..3] 
    findMax 1 [2, 3] 
     findMax 2 [3] 
      findMax 3 [] 

Per n = 30, come nel test, mx rende 1073741823 chiamate, e mx' solo 29.

5

Il tuo secondo algoritmo è lineare, finirà per fare un singolo passaggio nella tua lista. Il tuo primo algoritmo ha un runtime esponenziale in questo caso (che si verifica nel caso peggiore). Si finisce essenzialmente per controllare tutto nella lista per determinare che il primo elemento, 1, non è il massimo. Quindi consideri il secondo elemento, 2, e guardi l'intero resto dell'elenco per sapere che anche questo non è il massimo.

Se si esegue il programma utilizzando mx e valori come 22, 23, ..., 30 vedrete una crescita esponenziale chiaramente in fase di esecuzione.

In particolare, questo non è semplicemente un problema di ricorsione di coda o no, si tratta di un algoritmo ricorsivo inefficiente rispetto a uno efficiente. È possibile implementarli in una lingua senza ricorsione in coda e vedere ancora le prestazioni più rapide di mx' su mx.

+4

Penso che il problema di fondo è che (se ho capito bene) GHC non fa eliminazione sottoespressione comune per le chiamate ricorsive. – dfeuer

+0

@dfeuer Che suona un po 'strano. C'è un problema aperto per questo? –

+3

@BartekBanachewicz CSE può anche portare a prestazioni peggiori, quindi GHC è prudente nel farlo. per esempio. 'length [1..1000] + length [1..1000]' può essere eseguito in uno spazio costante, 'let l = [1..1000] in length l + length l' alloca l'intero 1000 elementi. Penso che CSE sia in qualche modo garantito in ViewPatterns, dato che non c'è modo di aggirarlo. – chi

9

Il problema non è tanto la coda-ricorsività, è il fatto che nel mx nel caso generale si calcola mx xs due volte: una volta per confrontarlo con x, e poi una seconda volta per restituirlo. Ognuna di queste chiamate chiama due volte lo mx xs, che quindi fa lo stesso, ecc., Risultando in un tempo di esecuzione esponenziale.

È possibile rimuovere questo problema semplicemente salvando il risultato della prima chiamata di usarlo per la seconda volta:

mx :: (Ord a) => [a] -> a 
mx [] = error "Empty list" 
mx [x] = x 
mx (x:xs) = 
    let mxxs = mx xs in 
    if x > mxxs then x else mxxs 
+0

Considerando che mx è una funzione "pura" (cioè restituisce sempre lo stesso risultato per lo stesso input), sembra che ghc debba ottimizzare la chiamata extra alla stessa funzione. Chiaramente questo non sta accadendo nemmeno con -O2. – donatello

+0

@donatello 'mx' potrebbe essere una funzione pura, ma' mx xs' verrà memorizzato solo se associato a un nome. – Jubobs

+2

@donatello: GHC generalmente non esegue questo tipo di ottimizzazione perché, in pratica, è molto difficile prevedere quando è effettivamente utile e aumenta in modo significativo l'utilizzo della memoria. –

Problemi correlati