| gasche | # · le 01/04/2012 à 20:52:16 · Répondre · Source |
|
|
Certains d'entre-vous en ont entendu parler vendredi, je me suis décidé ce week-end à implémenter l'algorithme HashLife. C'est un algorithme subtil et intéressant pour calculer de nombreuses générations de l'automate cellulaire dit "jeu de la vie" (Conway's game of life). J'en avais déjà parlé sur progmod comme sujet d'atelier potentiel, mais vous aviez trouvé (sans doute à raison, je m'en rends compte maintenant) que c'est peut-être trop compliqué. Ensuite, et indépendamment, j'ai aussi essayé d'aider des gens implémentant cet algorithme et qui posaient des questions de performance, mais ce n'est pas agréable de triturer du code sans le comprendre car on ne connaît pas le principe d'ensemble. J'ai donc écrit ce week-end une implémentation de HashLife en OCaml, en espérant la rendre claire et en faire, en même temps, une explication de l'algorithme. C'est une forme de literate programming en un sens, car le code est truffé de commentaire et est pensé pour se lire de haut en bas, sans avoir besoin de sauter d'un endroit à un autre comme souvent dans les programmes informatiques. J'ai mis le code sur gitorious, et vous pouvez trouver en particulier l'unique fichier source ici: https://gitorious.org/hashlife-in-literate-ocaml/hashlife-in-literate-ocaml/blobs/master/hashlife.ml La coloration syntaxique est un peu foireuse (j'ai soumis un rapport de bug à ce sujet, on verra), et le code est assez long (un bon millier de lignes) car il y a pas mal de choses à expliquer. J'ai bien aimé cet exercice de style, et j'espère avoir des retours. En particulier, je me demande s'il est vraiment possible de comprendre l'algorithme en lisant le code, ou si c'est une lubie qui paraît réaliste seulement à la personne qui écrit le code après avoir compris (ou pendant la compréhension). |
| Cygal | # · le 01/04/2012 à 22:50:38 · Répondre · Source |
|
|
J'ai joué le jeu : je connais le jeu de la vie, je l'ai implémenté plusieurs fois naïvement, mais je n'avais jamais entendu parler de HashLife. J'écris mes remarques au fur et à mesure que je lis le code, sans avoir peur d'avoir l'air bête. Du coup c'est pas forcément que c'est pas clair, juste que je me suis posé la question. Disons du literate code reading. Je me suis posé pas mal de questions bêtes au début :
Damn, les choses sérieuses commencent.
Du coup c'est un peu chiant, je dois relire les explications en essayanat cette nouvelle possibilité. Bonne excuse pour aller au lit. |
| Cacophrene | # · le 02/04/2012 à 05:45:20 · Répondre · Source |
|
|
Mon premier message ici, pour commenter une implémentation sympa d'un algorithme sympa. D'abord, les explications qui accompagnent le texte et les choix de présentation du code : c'est une aide précieuse pour comprendre ce qui se passe... mais les non-initiés risquent de négliger des détails qui se révèlent importantes par la suite. Ça, tu n'y peux rien.
Pourquoi avoir gardé des booléens pour les feuilles ? Pourquoi pas un seul entier et des opérations bit à bit ? Est-ce que ça aurait un impact sur les perfs (et/ou sur le GC) ? Faudrait que je bidouille ça dans un coin. En tout cas merci pour le partage et ce code qui donne à réfléchir. |
| gasche | # · le 02/04/2012 à 06:11:18 · Répondre · Source |
|
|
Cygal > Merci beaucoup pour tes remarques. Il reste quelques incohérences (et de nombreuses fautes d'anglais) dans mon explication et il faut bien des gens pour débugguer :-' Je suis ravi d'apprendre tout ce que vous trouvez délicat, subtil, "fourbe" etc. C'est clairement des endroits où il faut rajouter des commentaires ou clarifier le code. Pour ton deuxième bloc de questions, l'incompréhension vient d'une confusion entre le côté du carré, de taille 2^n, et sa surface 2^(2n): un carré de dimension 1 a un côté de taille 2^1 = 2, mais donc contient bien 2*2 = 4 cellules individuelles. (Je trouve que le nom "Cell" comme constructeur d'un arbre de dimension 1 (qui contient... 4 cellules) est un peu pourri mais je n'avais pas trouvé mieux. En fait "Base" serait peut-être mieux, je vais sans doute changer quand je serai convaincu que c'est mieux.) La bonne nouvelle pour toi, c'est que tu n'es qu'au début, et donc tu peux te permettre de relire l'ensemble :-' |
| gasche | # · le 02/04/2012 à 06:20:38 · Répondre · Source |
|
|
Cacophrène > merci pour tes remarques aussi.
Ça doit avoir un impact, mais j'ai commencé à écrire du code naïf et lisible (je veux dire dont l'algorithmique soit correcte mais dont le code soit clair au risque d'avoir un fort facteur multiplicatif).
Pourrais-tu être un peu plus explicite sur ce qui te dérange ? (Globalement le code est assez "piéton", avec un usage intensif de l'indentation comme suggestive de position spatiale, et moins de factorisation qu'on pourrait le souhaiter. Ici on pourrait utiliser une liste par exemple et faire un fold, mais du coup on perdrait l'intuition spatiale, donc ça me tente moins.)
Ça ne casserait pas la vérification tant que je teste seulement sur des "foo" de la bonne taille. Je n'ai pas fait cette complémentation à la puissance de 2 la plus proche parce que je n'en avais pas besoin à ce endroit (en fait j'arrivais de la direction inverse: j'ai du code subtil sur les trees que je veux tester, donc je les transforme en map où je fais tourner un algo plus clairement correct). C'est implémenté plus bas dans la partie "exemples" parce que ça devient utile sur des exemples concrets (chiant de se forcer à complémenter soi-même). Je vais rajouter un commentaire pour prévenir. |
| Cacophrene | # · le 02/04/2012 à 10:00:03 · Répondre · Source |
|
|
D'accord pour le gain en temps qui est compensé par la mise en cache des résultats. Il restera malgré tout un gain en mémoire si l'on remplace un entier par un n-uplet.
Bon, c'est juste de la présentation. Dans cette fonction
Exact, je n'avais pas fait attention. |
| gasche | # · le 02/04/2012 à 13:43:18 · Répondre · Source |
|
|
En fait je me suis basé sur la description du sujet de Jean-Cristophe Filliâtre:
Pour les quadruplets de booléens, ils ne sont pas stockés en mémoire dans la version mémoisée, qui associe un résultat à l'identifiant de l'arbre correspondant.
Non, ce n'est pas une question d'inattention: je voudrais qu'on puisse lire le code de haut en bas, donc il ne faut pas que le lecteur soit étonné par quelque chose qui s'explique ensuite. J'ai donc rajouté un commentaire à ce sujet dans le code au moment où tu t'es posé la question. |
| mob | # · le 02/04/2012 à 21:13:23 · Répondre · Source |
|
Je me suis prêté au jeu également, et bien que j'ai eu à revenir en arrière pour clarifier certaines choses par moments, l'algorithme est bien expliqué et le code assez clair dans l'ensemble (je précise que je n'avais jamais entendu parler de l'algorithme avant). La plupart des confusions qu'on peut avoir sont clarifiées en relisant attentivement. Concernant le code, les fonctions
Ce n'est pas plutôt "into two computations of 2^(n-1) steps" ? Aussi il serait bien de préciser "steps in the future". À ma première lecture, par manque de concentration peut-être, en lisant computation et steps, j'ai cru que tu faisais allusion à la complexité algorithmique :-° Concernant la séparation du nombre de steps ahead en a + b, les explications gagneraient à être un peu étendues et clarifiées. Lorsqu'on lit les explications sur le cas général, on a l'impression que le 2^(n-1) est une sorte de nombre spécial et qu'on doit forcément calculer la 2^(n-1)-ième génération des (n+1)-trees avant de passer à l'étape suivante de l'algorithme, qui calcule 2^(n-1) générations dans le futur aussi. Alors qu'en fait il s'agit simplement d'avoir une somme permettant d'atteindre la 2^n-ième génération, qui peut être choisie autrement (si j'ai compris correctement). Je ne sais pas ce que je dis est clair, mais je pense que comprendre ça plus tôt permettrait de mieux comprendre le cas s ≤ 2^n. Sur une note plus positive, je trouve que l'organisation spatiale que tu donnes au code aide beaucoup à la compréhension, et que globalement c'est très clair. PS : Je n'ai pas encore vu la partie sur la mémoization, j'ajouterais des remarques dessus si j'en ai par la suite. |
| gasche | # · le 02/04/2012 à 22:00:35 · Répondre · Source |
|
|
Tu as tout à fait raison pour "two computations of 2^(n+1) steps", c'est bien 2^(n-1) qu'il faut lire ici. C'est une erreur d'inattention de ma part, j'ai corrigé. Pour ton second paragraphe sur le rôle privilégié de la somme 2^(n-1)+2^(n-1), je pense qu'on peut effectivement dire qu'elle joue un rôle privilégié. En effet, vu la structure de données on est forcé, de toute façon, de renvoyer un arbre de taille 2^n, donc 2^(n-1) est le futur "maximal" qu'on peut calculer sur un arbre de cette taille là. Si on demande de calculer un nombre plus petit d'itérations, on ne renvoie pas pour autant un arbre plus grand (alors qu'on a assez d'information pour), donc d'une certaine manière on perd de l'information. Ça se voit si je demande le calcul sur 10 itérations: la technique naïve de calcul sur les matrices renverra une matrice avec seulement 10 cases en moins sur les bords, alors que les algorithmes par quadtree renverront un arbre de côté moitié dans tous les cas. Pour éviter cet effet qui privilégie 2^(n-1) il faudrait une structure plus flexible qui peut contenir non pas 4 enfants, mais autant qu'il est nécessaire, et à la granularité nécessaire, pour couvrir la zone totale pouvant être calculée. Ça ne m'a pas l'air facile. Plus généralement cette partie sur le calcul d'un nombre d'étapes quelconques je ne l'ai pas lue quelque part, le sujet de Filliâtre dont je suis parti demande essentiellement de deviner ça tout seul. Du coup je ne sais pas si c'est la meilleure solution: il y a peut-être des façons de faire ça qui n'ont pas cet inconvénient. Dans tous les cas, je ne suis pas convaincu que parler de ce problème plus tôt aide à la compréhension. Je trouve que la première version de la fonction "result", avec les sous-arbres qui dansent et tout, est déjà suffisamment difficile à comprendre et qu'ajouter des considérations sur le nombre d'étape à calculer risquerait d'augmenter encore la difficulté. C'est pour ça que j'ai préféré faire les choses en deux temps. |
| mob | # · le 03/04/2012 à 20:15:31 · Répondre · Source |
|
Citation de gasche
Effectivement je ne le voyais pas comme ça, c'est plus clair maintenant. :) |
Hébergé sur awesom.eu · Bug tracker · Code du site · Flux RSS : Dépêches · Tutoriels · Forum