Programmation Fonctionnelle Avancée

Cours 9

kn@lri.fr
http://www.lri.fr/~kn

Résumé de l'épisode précédent

On a vu le trie, un structure de donnée permettant d'implémenter des dictionnaires :

Accès Données triées Persistante
Trie O(k) (?)
p r o g r a ┌━━━━━━┴━━━━━━┐ d m ┃ ┃ a m ┃ ┃ t a ┌━━━━━━━┼━━━━━━━━┐ i b i n ┃ ┃ ┌━━┼━━┐ o l e s t t ┃ ┃ ┃ n e n ┃ ┃ s t

Corrigé commenté du TP

Corrigé commenté du TP 8, EXO 1

On fait un corrigé commenté du TP (~15 à 20 minutes)

On continuera la feuille dans le TP 9 (quelques questions ajoutées)

Trie (suite)

Trier les mots d'un texte

On considère un texte T = { s1, …, sn} constitué de n mots. Le nombre de caractères dans le texte est

|T| =
i=1..n
|si|

On considère l'algorithme suivant :

On a donc trié l'ensemble des mots du texte. Quelle est la complexité ?

O(|T|) 🤔

On a pu trier un texte en moins que O(|T|log(|T|)), où est l'arnaque ?

Complexité des tris

Il faut être très précis dans les énoncés.

Un tri utilisant une fonction de comparaison binaire (qui compare 2 éléments entre eux) doit effectuer O(N log(N)) comparaisons pour une collection de taille N.

Intuition: étant donné une collection de N éléments, il y a N! permutations possibles. En faisant les comparaisons 2 à 2, on peut « trouver » la bonne permutation au mieux en O(log(N!)) = O(N log(N))

Complexité des tris

Mais ici on n'utilise pas une comparaison 2 à 2. On a une structure de données auxiliaire qui sait donner rapidement un ensemble de clés plus grandes.

On peut donc trier en temps linéaire, mais pour un ordre bien particulier, l'ordre lexicographique. On ne peut pas utiliser un trie pour un ordre arbitraire.

Exemple, si on a une liste de vecteurs (x,y) que l'on veut trier par taille croissante (√(x2+y2)), on ne peut pas utiliser un trie.

Ensembles

Dans toute la suite, on suppose que les tries qu'on manipule représentent des ensembles et non des dictionnaires. On stocke juste dans les nœuds un booléen qui dit si le nœud est terminal ou pas:

type trie = Node of bool * (char * trie list) (* pas de 'a, car on ne stocke pas de valeur *)

Unicité de la représentation

Pour un ensemble de clés données, le trie représentant cet ensemble est unique.

Ce n'est pas le cas d'autres structures. Exemple avec les tables de hachage:

Le tableau interne peut avoir été agrandi, mais c'est le même ensemble.

Unicité de la représentation : utilité

Si deux objets (immuables) égaux sont structurellement égaux, alors on peut partager leur représentation en mémoire. Cela permet d'accélérer des opérations et d'économiser de la mémoire. Avant de voir cette technique, on doit s'intéresser à la représentation en mémoire des valeurs OCaml.

Modèle mémoire d'OCaml

Modèle mémoire ?

Le modèle mémoire d'un langage de programmation est la façon dont ce dernier représente ses valeurs en mémoire, i.e. comme des suites d'octets.

De ce point de vue, l'impact le plus grand pour langage est la présence d'un GC ou garbage collector.

GC

Le garbage collector (ou GC) est un "sous-programme" dont le rôle est de désallouer automatiquement la mémoire allouée. Exemple :

Pourquoi le GC a-t'il un impact ?

Comment le GC détecte-t'il qu'un objet peut être désalloué ?

Il doit stocker avec chaque objet de l'information supplémentaire. La nature de cette information à un impact sur le modèle mémoire.

Représentation des valeurs en OCaml

OCaml distingue 2 sortes de valeurs à l'exécution

Ainsi, toutes les valeurs OCaml sont représentées de façon uniforme, un mot mémoire de 64 bits qui est soit un entier soit un pointeur.

Le GC

Comment désallouer ?

Exemple, let x = (41, 42). La variable x contient un pointeur vers un bloc de taille 3 * 64 bits = 3* 8 octets = 24 octets.

Le GC

Le GC parcours l'ensemble des valeurs pendant l'exécution du programme (en « parallèle »).

Il commence son parcours depuis les variables globales et la pile d'appel courante (variable locale visible)

Si le GC rencontre un pointeur, il va voir le bloc correspondant, et met un 1 dans les 2 bits réservés. Puis il parcours les blocs suivants et suis les pointeurs ≡ parcours du graphe des pointeurs.

Après le parcours, on re-parcourt une nouvelle fois. Toutes les valeurs marquées sont gardées (et la marque remise à 0), les autres sont désallouées.

Cette technique est appelée Mark and Sweep.

Pointeur vs. Entier ?

Problème, quand le GC voit la valeur « 42 » dans un bloc, est-ce qu'il s'agit de l'entier 42 (on ne fait rien) ou est-ce qu'il s'agit du pointeur vers l'adresse 42 (et il faut suivre ce pointeur).

Solution: Les entiers sont marqués en décalant à gauche de 1 bit mettant leur bit de poids faible à 1.

Ça marche car les adresses mémoires vers des blocs de 8 octets sont toujours des multiples de 8 donc pairs.

Donc: si on voit un nombre pair, c'est un pointeur on le suit
Si on voit un nombre impair, on le divise par 2 et on obtient la valeur avec laquelle on peut calculer.

Démos

Conclusion

On voit donc la différence qu'il y a entre « être structurellement égal » et être identique (pointer vers le même bloc)

Les trie on la propriété que si deux sous-arbres sont structurellement égaux, on peut les remplacer par le même pointeur.

On verra la prochaine fois comment utiliser cela pour rendre du code plus efficace