Au sujet de la gestion automatique de la mémoire, je dois dire que je n'y connais pas grand chose. Tout comme sur le fonctionnement interne des CPU et de leur cache.
En revanche, j'ai reréfléchi à ton implémentation de update et je doute qu'elle soit optimale pour l'usage de cette structure. [...]
Il est vrai qu'une suite de set vas créer une cascade de Diff qu'il faudra résoudre lors du premier get. En gros, le premier get est en O(n) avec n le nombre de set fait pendant ce temps. Mais en analyse amortie, je pense que c'est équivalent, puisque le temps "gagné" à ne pas faire le reroot lors de chaque set sera "utilisé" lors du premier get.
Par contre, cela change pas mal le comportement en mémoire. Dans mon cas, le garbage collector ne vas pas supprimer les n Diff intermédiaires, tant qu'il n'y aura pas de reroot, même si les vecteurs précédents ne sont pas utilisés.
Quoi qu'il arrive, cette structure se comporte très mal quand tu utilises les anciennes copies. cela coûte très chère si tu essayes d’accéder consécutivement à deux version assez éloignées, car il faut reroot en permanence entre les deux, avec un O(n), n étant le nombre d'étape séparant les deux.
En Haskell, dans mon implémentation paresseuse, c'est encore plus drôle si tu met à jour des cases avec le contenu d'un vecteur précédant (voir futur si tu as envie, c'est la beauté d'Haskell). Imagine le code suivant :
vec=...-- on a un vecteur initialisévec2=updatevec0(indexvec0+1)-- incremente la case 0vec3=updatevec20(indexvec20+1)-- incrémente encore la case 0printvec3
chaque opération est paresseuse, donc en fait, vec2 et vec3 ne sont que des opérations paresseuses. Quand tu vas vouloir afficher vec3, il vas d'abord construire le Diff sur vec2 qui est un Diff sur vec. Puis, pour afficher la case 0, il va reroot sur vec3 jusqu'à réaliser que la case 0 c'est index vec2 0 + 1, donc il va reroot sur vec2, pour réaliser que c'est une fonction de vec, donc il vas reroot sur vec. Fin de l'évaluation paresseuse de la case 0. Pour passer à la case 1, il doit de nouveau reroot sur vec3, et là c'est bon. On peut bien évidemment contrôler ce comportement en forçant l'évaluation paresseuse judicieusement, mais quoi qu'il arrive il y aura des comportements tordus.
Par contre, j'ai benché mon code en ajoutant des lectures, puisque sinon la seule chose que on faisait c'est empiler des création de Diff, et il est clair qu'il y a une différence non négligeable de performance car le reroot doit être effectué, et en fonction du pattern d'utilisation, les performances peuvent être catastrophiques.
En gros, nous avons une structure persistante qui remplace un vecteur, mais dont les performances se cassent la gueule en fonction du nombre de versions partagées, et dont la place mémoire dépend du nombre de versions partagées et du nombre d'étapes entre les versions partagées. Bref, j'en arrive à conclure que c'est une structure amusante, mais pas si performante que cela. Je pense que on peut faire beaucoup mieux avec un HAMT qui conserve des propriétés de modification / insertion en O(log n) avec une très grosse base pour le log. La librairie unordered-containers utilise une base 16 pour le trie.
Après, j'utiliserais sans doute un vecteur mutable, ou, quand cela sortira, des vecteurs en type "linéaires".
[^] # Re: Structures non mutables performantes
Posté par Guillaum (site web personnel) . En réponse à la dépêche Sortie de GHC 8.2.1. Évalué à 3.
What every programmer should know about memory est une lecture vivement conseillée dans ce cas.
Il est vrai qu'une suite de
setvas créer une cascade deDiffqu'il faudra résoudre lors du premierget. En gros, le premiergetest enO(n)avecnle nombre desetfait pendant ce temps. Mais en analyse amortie, je pense que c'est équivalent, puisque le temps "gagné" à ne pas faire lererootlors de chaquesetsera "utilisé" lors du premierget.Par contre, cela change pas mal le comportement en mémoire. Dans mon cas, le garbage collector ne vas pas supprimer les n
Diffintermédiaires, tant qu'il n'y aura pas de reroot, même si les vecteurs précédents ne sont pas utilisés.Quoi qu'il arrive, cette structure se comporte très mal quand tu utilises les anciennes copies. cela coûte très chère si tu essayes d’accéder consécutivement à deux version assez éloignées, car il faut
rerooten permanence entre les deux, avec unO(n),nétant le nombre d'étape séparant les deux.En Haskell, dans mon implémentation paresseuse, c'est encore plus drôle si tu met à jour des cases avec le contenu d'un vecteur précédant (voir futur si tu as envie, c'est la beauté d'Haskell). Imagine le code suivant :
chaque opération est paresseuse, donc en fait,
vec2etvec3ne sont que des opérations paresseuses. Quand tu vas vouloir affichervec3, il vas d'abord construire leDiffsurvec2qui est unDiffsur vec. Puis, pour afficher la case 0, il varerootsurvec3jusqu'à réaliser que la case 0 c'estindex vec2 0 + 1, donc il va reroot survec2, pour réaliser que c'est une fonction devec, donc il vasrerootsurvec. Fin de l'évaluation paresseuse de la case 0. Pour passer à la case 1, il doit de nouveaurerootsurvec3, et là c'est bon. On peut bien évidemment contrôler ce comportement en forçant l'évaluation paresseuse judicieusement, mais quoi qu'il arrive il y aura des comportements tordus.Par contre, j'ai benché mon code en ajoutant des lectures, puisque sinon la seule chose que on faisait c'est empiler des création de
Diff, et il est clair qu'il y a une différence non négligeable de performance car lererootdoit être effectué, et en fonction du pattern d'utilisation, les performances peuvent être catastrophiques.En gros, nous avons une structure persistante qui remplace un vecteur, mais dont les performances se cassent la gueule en fonction du nombre de versions partagées, et dont la place mémoire dépend du nombre de versions partagées et du nombre d'étapes entre les versions partagées. Bref, j'en arrive à conclure que c'est une structure amusante, mais pas si performante que cela. Je pense que on peut faire beaucoup mieux avec un HAMT qui conserve des propriétés de modification / insertion en
O(log n)avec une très grosse base pour le log. La librairie unordered-containers utilise une base 16 pour le trie.Après, j'utiliserais sans doute un vecteur mutable, ou, quand cela sortira, des vecteurs en type "linéaires".