arthur.bebou

Le site arthur.bebou.netlib.re - retour accueil

git clone git://bebou.netlib.re/arthur.bebou
Log | Files | Refs |

index.sh (9962B)


      1 #! page
      2 title: Advent of code 2024^\(mais c\'est l\'advent of *unix* code\)
      3 author: Arthur Pons
      4 description: L\'advent of code 2024 version Unix
      5 publication: 2024-12-04
      6 
      7 sectionmd: main
      8 
      9 ## Qu'est-ce que c'est ?
     10 
     11 L'[advent of code](https://adeventofcode.com/2024/about) est un évènement
     12 annuel créé par [Eric Wastl](https://was.tl/projects/)[^1] durant lequel
     13 il faut résoudre des exercices algorithmiques. Je documente ici mes
     14 *tentatives* en utilisant les outils traditionnels d'Unix (les commandes
     15 posix, awk, sed, peut-être un peu de GNU coreutils etc).
     16 
     17 Je ne pense pas faire les 25 jours. Premièrement ça demande d'être motivé sur
     18 une longue période ce que j'ai du mal à faire, deuxièmement cette boite à outil
     19 est mal adapté à de nombreux types de problèmes généralement posés.
     20 
     21 Mon but est, pour chaque jour que je vais faire, de mettre en lumière une
     22 fonctionnalité méconnue, problème embêtant, astuce maline etc.
     23 
     24 Malheureusement le jeu nécessite un compte Github, Google, Twitter ou Reddit
     25 pour jouer. Je sacrifie donc mon compte Github pour vous fournir l'intitulé du
     26 problème de chaque journée. L'entrée du problème (et ma solution) sera dans le
     27 [dépôt git aoc2024](http://git.bebou.netlib.re/aoc2024/log.html). Il ce peut
     28 que ce qui se trouve commenté dans les articles ne soit plus synchro avec ce
     29 qui se trouve dans le dépôt.
     30 
     31 ## Jour 1
     32 
     33 [Jour 2](2.html)
     34 
     35 ### Intitulé résumé
     36 
     37 #### Première partie
     38 
     39 	3   4
     40 	4   3
     41 	2   5
     42 	1   3
     43 	3   9
     44 	3   3
     45 
     46 Maybe the lists are only off by a small amount! To find out, pair up the
     47 numbers and measure how far apart they are. Pair up the smallest number in the
     48 left list with the smallest number in the right list, then the second-smallest
     49 left number with the second-smallest right number, and so on.
     50 
     51 Within each pair, figure out how far apart the two numbers are; you'll need to
     52 add up all of those distances. For example, if you pair up a 3 from the left
     53 list with a 7 from the right list, the distance apart is 4; if you pair up a 9
     54 with a 3, the distance apart is 6.
     55 
     56 To find the total distance between the left list and the right list, add up the
     57 distances between all of the pairs you found. In the example above, this is 2 +
     58 1 + 0 + 1 + 2 + 5, a total distance of 11!
     59 
     60 #### Deuxième partie
     61 
     62 This time, you'll need to figure out exactly how often each number from the
     63 left list appears in the right list. Calculate a total similarity score by
     64 adding up each number in the left list after multiplying it by the number of
     65 times that number appears in the right list.
     66 
     67 So, for these example lists, the similarity score at the end of this process is
     68 31 (9 + 4 + 0 + 0 + 9 + 9).
     69 
     70 ### Commentaire de ma solution
     71 
     72 #### Première partie
     73 
     74 On veut tout de suite séparer les deux colonnes pour les trier. Il est
     75 étrangement "compliqué" de faire ça sur un fichier dont les colonnes son
     76 séparées par trois espaces sans dégainer `awk`. En effet, `cut` ne prend qu'un
     77 seul caractère comme argument et si on lui donne un espace le fichier sera en
     78 réalité séparé en cinq colonnes et non deux comme on voudrait. On peut faire
     79 avec mais ce n'est pas agréable. Pour utiliser `cut` on peut masser le fichier
     80 en "squeezant" les espaces successifs avec `tr -s ' '` puis un cut.
     81 
     82 Personnellement plutôt que de multiplier les pipes j'ai tendance à dégaîner `awk`
     83 dès que je rencontre un fichier dont les champs sont séparés de manière un peu
     84 arbitraire avec du blanc. `awk` se charge tout seul de faire le taf et on
     85 peut simplement écrire :
     86 
     87 	< 1.input awk '{print $1}' | sort
     88 	< 1.input awk '{print $2}' | sort
     89 
     90 Sinon pour le coeur de la réponse j'ai eu naturellement recours à la technique
     91 "utiliser sed,paste,tr pour écrire des maths et piper tout ça dans `bc`". C'est
     92 une illustration de se dont parle [Florian Cramer](http://floriancramer.nl/)
     93 dans son super article [$(echo echo) echo
     94 $(echo)](http://reader.lgru.net/texts/echo-echo-echo-echo-command-line-poetics/).
     95 
     96 > Command lines can be constructed that wrangle input data, text, into new
     97 > commands to be executed. Unlike in GUIs, there is recursion in user interfaces:
     98 > commands can process themselves.
     99 
    100 J'utilise également un fifo pour éviter de créer un fichier temporaire qui
    101 risquerait de rester ici et de prendre de la place sur mon disque. Si l'on ne
    102 prend pas le temps de supprimer le fifo il restera également mais au moins il
    103 est vide.
    104 
    105 Au final ça donne :
    106 
    107 	mkfifo left 2> /dev/null
    108 	< 1.input awk '{print $1}' | sort > left&
    109 	< 1.input awk '{print $2}' | sort |
    110 		paste -d '-' left - | grep -Ev '^-$' |
    111 		bc |
    112 		tr -d '-' | paste -s -d'+' |
    113 		bc
    114 
    115 Avec cette idée de manipuler du texte pour créer des formules mathématiques
    116 l'équivalent de `abs(x)` devient `tr -d '-'`.
    117 
    118 #### Deuxième partie
    119 
    120 Ce problème est une bonne opportunité pour évoquer le shell et ses performances.
    121 Récemment on m'a demandé s'il était possible d'implémenter des algos de machine
    122 learning (spécifiquement avec des réseaux de neurones) en shell. La réponse est
    123 techniquement oui mais il serait vain de tenter de le faire, le shell n'est pas
    124 fait pour faire de l'algèbre. Cela a ouvert une discussion plus générale sur
    125 la performance du shell.
    126 
    127 On admet ici qu'il n'est pas question des performances du shell "pure" qui
    128 n'appellerait aucun programme externe. Bien qu'il soit possible de programmer
    129 en shell "pure" il n'existe pas vraiment de bonne raison de le faire.
    130 peut s'installer sur un unix". A l'inverse on ne compterait pas comme "du
    131 shell" un script qui se contenterait d'appeler un autre programme bien que ce
    132 soit un usage légitime. Dans presque tous les cas quand on parle du shell on
    133 entend "un programme shell unixien pouvant appeler à minima les outils posix et
    134 au maxima n'importe quel outil qui peut s'installer sur un Unix et dont le
    135 fonctionnement et le temps d'exécution dépend pour une partie non négligeable
    136 des méchanismes du shell lui même (pipes, boucles, redirections etc)."
    137 
    138 Aujourd'hui je répond à cette question de cette manière :
    139 
    140   1. Le shell "pure" n'est pas un langage très performant
    141   2. *mais* il permet d'orchestrer efficacement des programmes qui peuvent être
    142      très performants
    143   3. la performance d'un programme en shell dépend donc, peut-être plus que
    144      pour n'importe quel autre langage, des outils utilisés **et** de la manière
    145 	 de les agencer.
    146 
    147 La nature des shell unix fait qu'il est très facile de d'utiliser des
    148 mécanismes assez avancés permettant d'accélérer l'exécution (parallélisme avec
    149 le pipe) et tout aussi facile de faire de grosses bêtises (forker
    150 exponentiellement en fonction de la taille de notre problème). L'étendu des
    151 performances possibles en shell pour un problème donné est très grande.
    152 
    153 Illustrons cet exemple avec notre deuxième partie.  Une solution qui pourrait
    154 paraître naturelle, notamment pour une personne ayant l'habitude d'un langage
    155 comme le C, serait :
    156 
    157 	< 1.input awk '{print $2}' | sort | uniq -c > rightcount
    158 	for e in $(< 1.input tr -s ' ' | cut -f1 -d' ');do
    159 		echo "$e * $(grep -E "$e$" rightcount | awk '{print $1}')"
    160 	done | sed 's/* $/*0/' | bc | paste -s -d'+' | bc
    161 
    162 Dans `rightcount` le nombre d'occurences des chiffres dans la seconde colonnes
    163 du type :`3   1234` si `1234` y apparaît 3 fois. Pour chaque élément de la
    164 première colonne on va récupérer ses occurrences dans `rightcount` qu'on multiplie
    165 à sa propre valeur. A la fin de la boucle on fait la somme et hop dans bc.
    166 
    167 C'est rapide à écrire mais c'est leeent. Pourquoi ? Parce que pour chaque
    168 nombre (et y'en a pas mal) on fork pour exécuter notre `grep` et aller chercher
    169 la bonne ligne dans `rightcount`. De manière générale quand on fork beaucoup
    170 (par exemple pleins de captures de commandes) dans une boucle en shell c'est
    171 mauvais signe pour les perfs. Bien sûr si votre problème est petit ce n'est pas
    172 un souci. A vous de voir si la relative lisibilité du code vaut le coup de perf[^2].
    173 
    174 Une solution plus "unixienne", beaucoup plus performante mais aussi plus obscure quand on y est pas habitué serait :
    175 
    176 	< 1.input awk '{print $2}' | sort | uniq -c > rightcount
    177 	< 1.input awk '{print $1}' | sort > leftc
    178 	< rightcount grep -f leftc | awk '{print $1"*"$2}' | paste -s -d'+' | bc
    179 
    180 Toujours nos occurences dans `rightcount`, Dans `leftc` la première colonne
    181 triée.  Ici l'astuce est de laisser `grep` faire le travail en utilisant son
    182 option `-f` qui permet d'utiliser comme pattern à matcher les lignes successives
    183 d'un fichier. Il se trouve que c'est exactement ce que l'on veut faire ! Plus
    184 qu'à créer les formules et hop dans `bc`.
    185 
    186 C'est performant parce que le très gros du travail est fait dans un seul
    187 processus `grep`. Et `grep`[^3], c'est trèèèès rapide.
    188 
    189 Dans l'ensemble le shell est très bon quand :
    190 
    191   1. le gros du travail peut être sous traité à un coreutils
    192   2. il est possible de paralléliser le problème par ligne (le pipe est alors
    193      un gain de perf "gratuit")
    194   3. le problème n'est pas complexe algorithmiquement parlant, il y a peu ou
    195 	 pas d'exceptions, peu de finesse dans le traitement à faire, peu
    196 	 d'embranchements logiques, peu de maths
    197 
    198 Pour plus de contenu sur le shell, son monde et ses perfs voir [un article sur
    199 sed](/substitutions/) ou [cet article assez connu qui compare le shell avec un
    200 cluster
    201 hadoop](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html).
    202 
    203 [^1]: également créateur de vanilla-js.com donc une personne particulièrement drôle.
    204 [^2]: au passage il y a une autre bêtise ici qui est que la formule peut être construite uniquement avec les données qui sortent du `grep` en faisant `awk '{print $1*$2}'\ mais peu importe, ça ne changerait presque rien en terme de perf. De manière générale le fait que beaucoup de choses peuvent être mises dans awk et possiblement pour le meilleur sera, je le prédis, un thème récurrent de l'aoc.
    205 [^3]: en particulier GNU grep