Fahrplan Praktische Informatik 3: Einführung in die Funktionale Programmierung Vorlesung vom 19.01.2011: Effizienzaspekte I Teil II: Funktionale Programmierung im Großen I Teil III: Funktionale Programmierung im richtigen Leben I Effizient Funktional Programmieren Universität Bremen I Fallstudie: Kombinatoren Wintersemester 2010/11 I Eine Einführung in Scala I Rückblich & Ausblick I Zeitbedarf: Endrekursion — while in Haskell I Platzbedarf: Speicherlecks I “Unendliche” Datenstrukturen I Verschiedene andere Performancefallen: 2 [33] 1 [33] Inhalt I Teil I: Funktionale Programmierung im Kleinen Christoph Lüth & Dennis Walter Rev. –revision– I I Effizienzaspekte I Überladene Funktionen, Listen Erste Lösung: bessere Algorithmen I Zweite Lösung: Büchereien nutzen I Analyse der Auswertungsstrategie I . . . und des Speichermanagement I Der ewige Konflikt: Geschwindigkeit vs. Platz I Effizenzverbesserungen durch “Usual Disclaimers Apply”: I Zur Verbesserung der Effizienz: I I Endrekursion: Iteration in funktionalen Sprachen I Striktheit: Speicherlecks vermeiden (bei verzögerter Auswertung) Vorteil: Effizienz muss nicht im Vordergrund stehen 3 [33] Endrekursion 4 [33] Beispiel: Fakultät I Definition (Endrekursion) Eine Funktion ist endrekursiv, wenn fac1 nicht endrekursiv: f a c 1 :: I n t e g e r → I n t e g e r f a c 1 n = i f n == 0 then 1 e l s e n ∗ f a c 1 ( n−1) (i) es genau einen rekursiven Aufruf gibt, (ii) der nicht innerhalb eines geschachtelten Ausdrucks steht. I I D.h. darüber nur Fallunterscheidungen: case oder if I Entspricht goto oder while in imperativen Sprachen. I Wird in Sprung oder Schleife übersetzt. I Braucht keinen Platz auf dem Stack. fac2 endrekursiv: f a c 2 :: I n t e g e r → I n t e g e r fac2 n = f a c ’ n 1 where f a c ’ :: I n t e g e r → I n t e g e r → I n t e g e r f a c ’ n a c c = i f n == 0 then a c c e l s e f a c ’ ( n−1) ( n∗ a c c ) I fac1 verbraucht Stack, fac2 nicht. 5 [33] Beispiel: Listen umdrehen I 6 [33] Überführung in Endrekursion Liste umdrehen, nicht endrekursiv: I r e v ’ :: [ a ] → [ a ] rev ’ [ ] = [] r e v ’ ( x : x s ) = r e v ’ x s ++ [ x ] I I I Gegeben Funktion f0 :S →T f 0 x = if B x then H x else φ (f 0 (K x )) (E x ) I Hängt auch noch hinten an — O(n2 )! Mit K : S → S, φ : T → T → T , E : S → T , H : S → T . Liste umdrehen, endrekursiv und O(n): I Voraussetzung: φ assoziativ, e : T neutrales Element r e v :: [ a ] → [ a ] r e v x s = r e v 0 x s [ ] where rev0 [ ] ys = ys rev0 ( x : xs ) ys = rev0 xs ( x : ys ) I Dann ist endrekursive Form: f :S→T f x = g x e where g x y = if B x then φ (H x ) y else g (K x ) (φ (E x ) y ) Beispiel: last (rev [1..10000]) 7 [33] 8 [33] Beispiel I Beispiel Länge einer Liste (nicht-endrekursiv) I l e n g t h ’ :: [ a ] → I n t l e n g t h ’ x s = i f n u l l x s then 0 e l s e 1+ l e n g t h ’ ( t a i l x s ) I I Zuordnung der Variablen: K (x ) 7→ tail x E (x ) 7→ 1 φ(x , y ) 7→ x + y Es gilt: φ(x , e) = x + 0 = x B(x ) → 7 H(x ) → 7 e → 7 Damit endrekursive Variante: l e n g t h :: [ a ] → I n t l e n g t h x s = l e n x s 0 where l e n x s y = i f n u l l x s then y −− was: y+ 0 e l s e l e n ( t a i l x s ) ( 1+ y ) null x 0 0 I (0 neutrales Element) Allgemeines Muster: I Monoid (φ, e): φ assoziativ, e neutrales Element. I Zusätzlicher Parameter akkumuliert Resultat. 10 [33] 9 [33] Endrekursive Aktionen I Fortgeschrittene Endrekursion Nicht endrekursiv: I g e t L i n e s ’ :: IO S t r i n g g e t L i n e s ’ = do s t r ← g e t L i n e i f n u l l s t r then r e t u r n " " e l s e do r e s t ← g e t L i n e s ’ r e t u r n ( s t r ++ r e s t ) I Akkumulation von Ergebniswerten durch closures I I closure: partiell applizierte Funktion Beispiel: die Klasse Show I Nur Methode show wäre zu langsam (O(n2 )): c l a s s Show a where show :: a → S t r i n g Endrekursiv: g e t L i n e s :: IO S t r i n g g e t L i n e s = g e t i t " " where g e t i t r e s = do s t r ← g e t L i n e i f n u l l s t r then r e t u r n r e s e l s e g e t i t ( r e s ++ s t r ) I Deshalb zusätzlich s h o w s P r e c :: I n t → a → S t r i n g → S t r i n g show x = s h o w s P r e c 0 x " " I String wird erst aufgebaut, wenn er ausgewertet wird (O(n)). 11 [33] Beispiel: Mengen als Listen 12 [33] Effizienz durch “unendliche” Datenstrukturen I data S e t a = S e t [ a ] Listen müssen nicht endlich repräsentierbar sein: I Beispiel: “unendliche” Liste [2,2,2, . . . ] I Liste der natürlichen Zahlen: I Syntaktischer Zucker: Zu langsam wäre twos = 2 : twos i n s t a n c e Show a ⇒ Show ( S e t a ) where show ( S e t e l e m s ) = " { " ++ i n t e r c a l a t e " , " ( map show e l e m s ) ++ " } " n a t = n a t s 0 where n a t s n = n : n a t s ( n+ 1 ) Deshalb besser nat = [0 . . ] i n s t a n c e Show a ⇒ Show ( S e t a ) where s h o w s P r e c i ( S e t e l e m s ) = showElems e l e m s where showElems [ ] = ( " {} " ++ ) showElems ( x : x s ) = ( ’ { ’ : ) ◦ shows x ◦ s h o w l x s where s h o w l [ ] = ( ’} ’ : ) s h o w l ( x : x s ) = ( ’ , ’ : ) ◦ shows x ◦ s h o w l x s I Bildung von unendlichen Listen: c y c l e :: [ a ] → [ a ] c y c l e x s = x s ++ c y c l e x s r e p e a t :: a → [ a ] repeat x = x : repeat x I Nützlich für Listen mit unbekannter Länge I Obacht: Induktion nur für endliche Listen gültig. 13 [33] Berechnung der ersten n Primzahlen I Eratosthenes — aber bis wo sieben? I Lösung: Berechnung aller Primzahlen, davon die ersten n. Fibonacci-Zahlen s i e v e :: [ I n t e g e r ] → [ I n t e g e r ] s i e v e ( p : ps ) = p : ( s i e v e ( f i l t e r ( \n → n ‘ mod ‘ p /= 0 ) p s ) ) I 14 [33] I Aus der Kaninchenzucht. I Sollte jeder Informatiker kennen. f i b :: I n t e g e r → I n t e g e r fib 0 = 1 fib 1 = 1 f i b n = f i b ( n−1)+ f i b ( n−2) Keine Rekursionsverankerung ( sieve [ ]) p r i m e s :: [ I n t e g e r ] primes = s i e v e [2 . . ] I Von allen Primzahlen die ersten n: I Problem: baumartige Rekursion, exponentieller Aufwand. f i r s t p r i m e s :: I n t → [ I n t e g e r ] f i r s t p r i m e s n = take n primes 15 [33] 16 [33] Fibonacci-Zahlen als Strom Unendliche Datenstrukturen I Lösung: zuvor berechnete Teilergebnisse wiederverwenden. I Endliche Repräsentierbarkeit für beliebige Datenstrukturen I Sei fibs :: [ Integer ] Strom aller Fibonaccizahlen: I E.g. Bäume: fibs tail fibs tail ( tail fibs ) I 1 1 2 1 2 3 2 3 5 3 5 8 13 21 34 55 5 8 13 21 34 55 8 13 21 34 55 data Tree a = N u l l | Node ( Tree a ) a ( Tree a ) d e r i v i n g Show twoTree = Node twoTree 2 twoTree Damit ergibt sich: r i g h t S p l i n e n = Node N u l l n ( r i g h t S p l i n e ( n+1 ) ) f i b s :: [ I n t e g e r ] f i b s = 1 : 1 : z i p W i t h (+) f i b s ( t a i l fibs ) I I n-te Fibonaccizahl mit fibs !! n I I Aufwand: linear, da fibs nur einmal ausgewertet wird. I twoTree, twos mit Zeigern darstellbar (e.g. Java, C) rightSpline 0, nat nicht mit darstellbar Damit beispielsweise auch Graphen modellierbar 17 [33] Implementation und Repräsentation von Datenstrukturen 18 [33] Speicherlecks I I I Datenstrukturen werden intern durch Objekte in einem Heap repräsentiert Garbage collection gibt unbenutzten Speicher wieder frei. I I Bezeichner werden an Referenzen in diesen Heap gebunden Verzögerte Auswertung effizient, weil nur bei Bedarf ausgewertet wird I I Unendliche Datenstrukturen haben zyklische Verweise I I Kopf wird nur einmal ausgewertet. c y c l e ( t r a c e " Foo ! " [ 5 ] ) I Anmerkung: unendlich Datenstrukturen nur sinnvoll für nicht-strikte Funktionen Aber Obacht: Speicherlecks! Eine Funktion hat ein Speicherleck, wenn Speicher unnötig lange im Zugriff bleibt. I I Unbenutzt: Bezeichner nicht mehr im erreichbar “Echte” Speicherlecks wie in C/C++ nicht möglich. Beispiel: getLines, fac2 I Zwischenergebnisse werden nicht auswertet. I Insbesondere ärgerlich bei nicht-terminierenden Funktionen. 19 [33] Striktheit I I Speicherprofil: fac1 50000, nicht optimiert Strikte Argumente erlauben Auswertung vor Aufruf Dadurch konstanter Platz bei Endrekursion. Erzwungene Striktheit: seq :: α→ β→ β fac ⊥ ‘seq‘ b = ⊥ a ‘seq‘ b = b I I 6,418,898 bytes x seconds Wed Jan 19 11:24 2011 bytes I 20 [33] 1,200k seq vordefiniert (nicht in Haskell definierbar) ($!) :: (a→ b)→ a→ b strikte Funktionsanwendung TSO 1,000k 800k f $ ! x = x ‘ seq ‘ f x BLACKHOLE I I ghc macht Striktheitsanalyse 600k Fakultät in konstantem Platzaufwand 400k ARR_WORDS f a c 3 :: I n t e g e r → I n t e g e r f a c 3 n = f a c ’ n 1 where f a c ’ n a c c = s e q a c c $ i f n == 0 then a c c e l s e f a c ’ ( n−1) ( n∗ a c c ) 200k 0k 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 seconds 21 [33] Speicherprofil: fac2 50000, nicht optimiert Wed Jan 19 11:25 2011 fac 1,481,240 bytes x seconds bytes 7,213,527 bytes x seconds Speicherprofil: fac3 50000, nicht optimiert bytes fac 22 [33] 1,200k 500k Wed Jan 19 11:25 2011 450k TSO 1,000k TSO 400k 350k 800k 300k BLACKHOLE BLACKHOLE 250k 600k 200k 400k 150k ARR_WORDS ARR_WORDS 100k 200k 50k 0k 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 0k 0.0 seconds 23 [33] 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 seconds 24 [33] Speicherprofil: fac1 50000, optimiert Wed Jan 19 11:25 2011 fac 226,043 bytes x seconds Wed Jan 19 11:25 2011 bytes 3,568,845 bytes x seconds bytes fac Speicherprofil: fac2 50000, optimiert 80k ARR_WORDS TSO 600k 60k TSO integer-gmp:GHC.Integer.Type.S# 400k 40k MUT_ARR_PTRS_CLEAN ARR_WORDS 200k 20k ghc-prim:GHC.Types.: 0k 0.0 0k 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 seconds 0.0 0.5 1.0 1.5 2.0 2.5 3.0 seconds 25 [33] Speicherprofil: fac3 50000, optimiert 225,010 bytes x seconds Fazit Speicherprofile Wed Jan 19 11:25 2011 bytes fac 26 [33] 80k I Geschwindigkeitsgewinn durch Endrekursion nur mit Striktheit I Optimierung des ghc meist ausreichend für Striktheitsanalyse, aber nicht für Endrekursion ARR_WORDS 60k TSO 40k MUT_ARR_PTRS_CLEAN 20k ghc-prim:GHC.Types.: 0k 0.0 0.5 1.0 1.5 2.0 2.5 3.0 seconds 27 [33] foldr vs. foldl I Wann welches fold? foldr ist nicht endrekursiv: I f o l d r :: ( a → b → b ) → b → [ a ] → b foldr f z [] = z f o l d r f z ( x : xs ) = f x ( f o l d r f z xs ) I 28 [33] I foldl ist endrekursiv: I Strikte Funktionen mit foldl’ falten: r e v 2 :: [ a ] → [ a ] rev2 = f o l d l ’ ( f l i p foldl’ ist strikt und endrekursiv: foldl foldl foldl let foldl’ ferner strikt und konstanter Platzaufwand Wann welches fold? I f o l d l :: ( a → b → a ) → a → [ b ] → a foldl f z [] = z f o l d l f z ( x : xs ) = f o l d l f ( f z x ) xs I foldl endrekursiv, aber traversiert immer die ganze Liste. I I ’ :: ( a → b → a ) → a → [ b ] → a ’ f a [] = a ’ f a ( x : xs ) = a ’ = f a x i n a ’ ‘ seq ‘ f o l d l ’ f a ’ x s (:)) [] Wenn nicht die ganze Liste benötigt wird, mit foldr falten: a l l :: ( a → Bool ) → [ a ] → Bool a l l p = f o l d r ((&&) ◦ p ) True I Potenziell unendliche Listen immer mit foldr falten. Für Monoid (φ, e) gilt: foldr φ e l = foldl (flip φ) e l 29 [33] Überladene Funktionen sind langsam. 30 [33] Listen als Performance-Falle I I Typklassen sind elegant aber langsam. I I I Listen: Implementierung von Typklassen: Verzeichnis (dictionary) von Klassenfunktionen. I Überladung wird zur Laufzeit aufgelöst I I I I Listen sind keine Felder oder endliche Abbildungen Bei kritischen Funktionen: Spezialisierung erzwingen durch Angabe der Signatur Felder Array ix a (Modul Array aus der Standardbücherei ) I I I NB: Zahlen (numerische Literale) sind in Haskell überladen! I I I Bsp: facs hat den Typ Num a=> a-> a I I 31 [33] Feste Größe (Untermenge von ix ) Zugriff auf n-tes Element in konstanter Zeit. Abstrakt: Abbildung Index auf Daten Endliche Abbildung Map k v (Modul Data.Map) I f a c s n = i f n == 0 then 1 e l s e n∗ f a c s ( n−1) Beliebig lang Zugriff auf n-tes Element in linearer Zeit. Abstrakt: frei erzeugter Datentyp aus Kopf und Rest Beliebige Größe Zugriff auf n-tes Element in sublinearer Zeit. Abstrakt: Abbildung Schlüsselbereich k auf Wertebereich v 32 [33] Zusammenfassung I Endrekursion: while für Haskell. I Überführung in Endrekursion meist möglich. I Noch besser sind strikte Funktionen. I Speicherlecks vermeiden: Striktheit und Endrekursion I Compileroptimierung nutzen I Datenstrukturen müssen nicht endlich repräsentierbar sein I Überladene Funktionen sind langsam. I Listen sind keine Felder oder endliche Abbildungen. 33 [33]