Le site arthur.bebou.netlib.re - retour accueil
git clone git://bebou.netlib.re/arthur.bebou
Log | Files | Refs |
index.sh (19986B)
1 #! page 2 title: Sed, substitutions et performances 3 author: Arthur Pons 4 description: Explorons les perfs de sed sur des gros fichiers 5 publication: 2023-09-08 6 sectionmd: main 7 8 9 ## Le problème 10 11 Il y a peu une ingénieure de l'Université de Strasbourg est venue me voir avec 12 un problème. Elle travaille avec une équipe de recherche sur un projet d'édition numérique du manuscrit d'un certain George Flohr, 13 alsacien au XVIIIe enrolé dans la guerre d'indépendance des Etats-Unis. A son retour, 14 il rédige tout un journal de bord de son aventure, des lieux qu'il a traversé, des personnes qu'il a rencontré. 15 Il s'agit de ce manuscrit que ce projet d'édition numérique souhaite publier en ligne. Pour cela, les fichiers 16 de transcription et de traduction sont encodés en [TEI](https://fr.wikipedia.org/wiki/Text_Encoding_Initiative). Les 17 nom de lieux sont référencés via des balises qui comportent des identifiants. 18 Par exemple pour Strasbourg : 19 20 Geschehen zu <placeName corresp="https://sws.geonames.org/2973783/about.rdf">Straßburg</placeName> den <date when="1787-06-05">5 J[uni]<lb/> 21 22 Ici `2973783` est l'identifiant [geonames](https://www.geonames.org/) du lieux. 23 L'ingénieure a créé un référentiel pour chacun des lieux. Ce référentiel fait 24 correspondre un identifiant geonames à un identifiant maison. Elle souhaitait 25 donc modifier la valeur des balises `corresp` en substituant l'url geonames à 26 l'identifiant maison correspondant. Nous avons d'abord préparé une table de 27 correspondance qui a, pour Strasbourg par exemple, cette tête : 28 29 flohr_place_00074 30 2973783 31 32 On veut modifier le texte de la pièce pour qu'apparaisse : 33 34 Geschehen zu <placeName corresp="flohr_place_00074">Straßburg</placeName> den <date when="1787-06-05">5 J[uni]<lb/> 35 36 Le texte d'origine est [ici (544Ko)](./text.xml) et le fichier de correspondance 37 [là (8Ko)](./corres). 38 39 ## La solution naïve 40 41 Qui dit substitution dit `sed`. une commande qui ferait l'affaire serait 42 43 sed -i "s;https://sws.geonames.org/2973783/about.rdf;flohr_place_00074;g" text.xml 44 45 > Petit tips, souvenez vous que l'on peut choisir le caractère que l'on veut 46 > comme délimiteur de la commande `s` de sed. Pratique quand on bosse avec des 47 > urls et que l'on ne peut pas utiliser `/`. 48 49 Pour exécuter une commande de ce type pour toutes les paires "ancien id/nouvel id" on 50 peut utiliser xargs. En imaginant que la table de correspondance se trouve dans 51 un fichier nommé `corres` : 52 53 xargs -a corres -d'\n' -n2 sh -c 'sed -i "s;https://sws.geonames.org/$2/about.rdf;$1;g" text.xml' -- 54 55 Ce script exécute à la suite toutes les commandes `sed` nécessaires pour faire 56 toutes les substitutions. Il prend environ 1,3s à finir. L'ingénieure s'est 57 ensuite demandée si ce script était plus ou moins rapide et/ou énergivore (deux 58 choses différentes) que celui qu'elle avait l'intention d'écrire. Sans aucune 59 mesure j'ai estimé qu'il était probable qu'il soit très peu optimisé sur ces 60 deux métriques notamment parce qu'il ouvre et ferme le fichier à modifier à 61 chaque substitution, ce qui est notoirement coûteux en temps. Or il y en a 213 62 en tout, il est donc raisonnable de penser qu'il existe d'autres techniques 63 pour aller beaucoup plus vite. 64 65 ## Optimisations en temps 66 67 ### Attention ! 68 69 Je tiens à prévenir et que je n'y connais rien en programmation parallèle et je 70 me cantonne volontairement au shell. J'imagine qu'il existe d'autres 71 techniques, plus ou moins coûteuses en code et en temps, bien plus efficaces 72 que ce que je vais développer ci dessous. Aussi, mon manque de compétences dans 73 ce domaine m'empêche de bien comprendre pourquoi telle technique fonctionne 74 mieux ou moins bien qu'une autre. J'ai entre autre du mal à identifier les 75 "bottlenecks". Si vous faite fréquemment du calcul intensif la lecture de ce 76 qui suit risque de vous être désagréable. 77 78 Les mesures de temps on été faites avec `time`, généralement deux trois fois 79 sans particulièrement faire attention à ce que le reste de l'environnement 80 soit équivalent d'une commande à l'autre. A prendre donc avec quelques gros 81 grain de sel. 82 83 J'ai connaissance de GNU parralel sans pour autant l'utiliser. Il est possible 84 que ce soit un outil bien plus approprié qu'xargs pour faire ce qui suit. Je me 85 demande entre autre s'il est possible non seulement de parraléliser les 86 commandes mais aussi de découper l'exécution sur différents morceaux du 87 fichier. 88 89 ### Quelques métriques 90 91 Le texte d'origine en TEI fait 12307 lignes pour 54095 mots. Il y a 213 92 substitutions à faire. Il y a en tout 377 occurrences de ces 213 substitutions 93 puisque certains lieux apparaissent plusieurs fois dans le texte. 94 95 Pour que les optimisations soient plus flagrantes j'ai créé un nouveau fichier 96 qui est 100 fois le texte d'origine. Il y a donc 1 million 230 mille lignes, 97 5 millions 409 milles mots et les 213 commandes de substitutions vont en tout 98 modifier 377 mille occurrences. Pour comparaison, Guerre et Paix c'est 587 99 mille mots. 100 101 ### La parallélisation 102 103 La première optimisation très peu coûteuse à coder en partant de notre script 104 est la parallélisation. Pour le moment le script exécute `sed -i ...`, attend 105 que cela termine puis passe au suivant de manière séquentielle. Forcément, 106 avec un très gros fichier cela prend un très gros bout de temps. En 107 l'occurence plus de 2m10s sur notre gros fichier. 108 109 Les processeurs modernes disposent de plusieurs coeurs. Ils peuvent donc mener 110 plusieurs processus en simultané. A l'aide de l'option `-Pn` de la commande 111 `xargs` nous pouvons dire au script de lancer jusqu'à `n` processus en 112 simultané. Par exemple avec `-P4` c'est quatre substitutions qui se feront en 113 même temps. 114 115 D'expérience je ne sais jamais vraiment combien de processus lancer, j'expérimente 116 pour trouver le "sweet spot". Avec 10 le script passe de 2m10 à 1m42. Sans donner 117 de limite c'est 1m54. On voit que le gain de temps est modéré. Le PC a tendance 118 à légèrement freeze sans pour autant toujours utiliser les processus à 100%. 119 J'imagine que paralléliser beaucoup d'accès mémoire n'est pas particulièrement 120 efficace. 121 122 ### Économiser des accès au fichier 123 124 Pour tester l'hypothèse selon laquelle une bonne partie du temps est passé 125 à ouvrir `text.xml` et à l'écrire, faisons un seul appel à `sed`. Pour cela 126 je manipule le fichier `corres` de façon à en faire un script sed contenant 127 toutes les substitutions : 128 129 s,https://sws.geonames.org/3411865/about.rdf,flohr_place_00010,g; 130 s,https://sws.geonames.org/5106834/about.rdf,flohr_place_00035,g; 131 s,https://sws.geonames.org/3038230/about.rdf,flohr_place_00106,g; 132 ... 133 134 > Pour l'obtenir j'ai fait 135 > `cat corres | paste - - | sed -E 136 > 's+(.*).(.*)+s,https//sws.geonames.org/\2/about.rdf,\1,g;+'`. 137 > Alternativement, et sûrement plus proprement : 138 > `xargs -a corres printf 's;https://sws.geonames.org/%s/about.rdf;%s;g\n'` 139 140 Ainsi on appelle sed une seule fois 141 142 sed -i -f subs.sed text.xml 143 144 Cette technique permet de faire tomber le temps d'exécution à 30s soit 145 une division par 3 ou 4. Notre hypothèse se vérifie bien. Aucun scoop 146 je crois que c'est vraiment le b-a-ba de l'optimisation de calcul. 147 148 ### Combiner les deux 149 150 Dans l'exemple précédent nous utilisons un seul processus. L'un de nos coeurs 151 sera possiblement à 100% mais nous n'utilisons pas la puissance des trois autres. 152 Pour améliorer encore nos performances nous pouvons séparer les commandes de 153 substitutions en quelques gros groupes et les exécuter via quelques appels 154 simultanés à sed. Je ne sais pas bien comment estimer la quantité optimale 155 de processus à lancer ici, j'ai choisi quatre. 156 157 sed -e 'les_50_premières_sub_de_subs.sed' text.xml | 158 sed -e 'les_50_suivantes' | 159 ... 160 ... > res.xml 161 162 Avec cette technique nous tombons à environ 10s. Pour le fichier d'origine on 163 passe de 1,3s avec la première technique à 0,15s avec celle-ci. 164 165 > Dans une ancienne version de ce script j'avais proposé le script suivant 166 > 167 > `xargs -a subs.sed -d'\n' -P4 -n54 sh -c 'sed -i "$*" big.xml' --` 168 > 169 > Il se révèle, pour tout œil un peu entrainé, qu'il n'accomplit pas du tout 170 > la tâche. L'accès concurrentiel de quatre processus à un même fichier pour 171 > y faire des substitutions inplace donne à la fin un fichier dans lequel n'a 172 > été substitué que ce que le dernier processus se terminant aura fait. C'est 173 > lui qui aura écrit le fichier en dernier. 174 175 Si vous avez énormément de substitutions et/ou un gigantesque fichier il peut 176 être intéressant d'avoir un script qui génère automatiquement ce script. Je 177 propose : 178 179 set 50 text.xml 180 sed "1~$1 s/^/sed '/; $1 s/$/' $2 |/; $(($1*2))~$1 s/$/' |/; $ s/$/'/" subs.sed 181 | sh > res.xml 182 183 Attention, l'adressage sed `n~m` n'est pas POSIX. Dans `$1` la valeur de la taille des groupes de commandes de substitutions, dans `$2` le chemin du fichier résultat. 184 185 On me dit que ce serait mieux avec `split`, c'est sûrement vrai. Exemple : 186 187 split -l50 corres CHANGES 188 echo '< big.xml' $( printf 'sed -f%s| ' CHANGES* ) 'cat > new.xml' | sh 189 190 ### Synthèse 191 192 | Méthode | texte d'origine | très gros texte | 193 |---------|-----------------|-----------------| 194 | Un accès fichier par sub | 1,3s | 2m10s | 195 | Parallélisation d'un accès fichier par sub | 0,5s | 1m42s | 196 | Un seul accès fichier | 0,35s | 30s | 197 | Parallélisation pour 4 accès fichier | 0,15s | 10s | 198 199 ## Optimisation en énergie 200 201 ### Méthodologie^(très nulle^©™) 202 203 Pour mesurer à la louche la consommation énergétique des commandes j'utilise un 204 programme nommé `energy`, disponible chez Bitreich[^1]. Du propre aveu de la 205 personne l'ayant écrit, c'est peu fiable[^4]. En plus de n'être pas fiable energy 206 ne fait pas la différence entre les processus et renvoie donc l'énergie totale 207 consommée lors de l'exécution de la commande. J'ai donc écrit un wrapper qui 208 fait la différence entre l'énergie totale et celle habituellement consommée par 209 l'ordinateur au repos. Evidemment cela dépend fortement du matériel. 210 211 Mon script est le suivant : 212 213 res=$( (time -f %e energy $*) 2>&1 1>/dev/null) 214 timeres=$(echo "$res" | tail -n1) 215 idleenergy=$(echo "11.03 * $timeres" | bc -l) 216 totenergy=$(echo "$res" | sed -nE '1,2 s/.* ([0-9]+\.[0-9]+).*/\1/ p' | paste -s -d'+' | bc -l) 217 diffenergy=$(echo "$totenergy - $idleenergy" | bc -l) 218 echo "$timeres : command exec time" 219 echo "$idleenergy : estimated J consumption for $timeres of idle time" 220 echo "$totenergy : total energy consumption" 221 echo "$diffenergy : added consumption of command" 222 223 Pas bien joli mais ça fonctionne. La constante `11.03` est la conso moyenne en 224 joule de mon pc durant une seconde avec uniquement l'environnement de bureau et 225 une console ouverte. 226 227 Le script récupère la durée d'exécution de la commande ainsi que la totalité de 228 l'énergie consommée durant puis fait la différence entre ce total et 11.03 fois 229 la durée d'exécution en seconde. 230 231 Évidemment la moyenne de 11.03 n'est pas tout à fait juste, mon pc fait plein 232 de choses en arrière plan[^2] et je n'ai pas la patience de faire plusieurs mesures 233 pour faire des moyennes. Je m'astreins donc à ne tirer des conclusions 234 définitives que de grosses différences de mesure. 235 236 Il faudrait refaire toutes les mesures avec un logiciel et une méthode plus sérieuse. 237 238 Mon est un Dell Latitude 5480 avec un Intel i5-7200U @ 2.50 GHz. 239 240 ### Les mesures 241 242 Pour la toute première solution sur le petit fichier : 243 244 1.167 : command exec time 245 12.87201 : estimated J consumption for 1.167 of idle time 246 29.96 : total energy consumption 247 17.08799 : added consumption of command 248 249 L'exécution de cette commande aurait donc plus que doublé la quantité d'énergie 250 consommée. A vérifier parce que ça me semble beaucoup. 251 252 Cette même solution mais sur le gros fichier 253 254 133.03 : command exec time 255 1467.3209 : estimated J consumption for 133.03 of idle time 256 2608.30 : total energy consumption 257 1140.9791 : added consumption of command 258 259 Ici la commande ajoute proportionnellement moins d'énergie. A noter 260 que le temps d'exécution est tellement long que l'écart inévitable 261 entre la moyenne de consommation au repos et la réelle valeur risque 262 d'être très grande. Par exemple, juste après ce test j'ai lancé un 263 264 energy sleep 133.03 265 266 et j'ai obtenu 1061.39 et donc une consommation supplémentaire de 1549.91 J 267 soit 400 J de plus qu'annoncé ! L'une des valeurs n'est pas plus vraie que 268 l'autre, simplement avec cette technique plus le temps d'exécution est long 269 plus le résultat est peu fiable. 270 271 Toutes les substitutions en un seul appel à sed : 272 273 29.90 : command exec time 274 329.7970 : estimated J consumption for 29.90 of idle time 275 696.41 : total energy consumption 276 366.6130 : added consumption of command 277 278 et la technique la plus rapide : 279 280 9.717 : command exec time 281 107.17851 : estimated J consumption for 9.717 of idle time 282 400.66 : total energy consumption 283 293.48149 : added consumption of command 284 285 Avec les fortes incertitudes de notre méthode je pense que l'on peut conclure 286 que ces deux techniques se valent en terme de consommation d'énergie. 287 288 ### Synthèse 289 290 291 | Méthode | texte d'origine | très gros texte | 292 |---------|-----------------|-----------------| 293 | Un accès fichier par sub | 17J | 1140J | 294 | Parallélisation d'un accès fichier par sub | pas mesuré | pas mesuré | 295 | Un seul accès fichier | pas mesuré | 366.6J | 296 | Parallélisation pour 4 accès fichier | pas mesuré | 293J | 297 298 ## Remarques 299 300 Il faut reconnaître que ce travail d'optimisation n'est qu'à but expérimental. 301 Le texte d'origine et la taille du corpus dont il vient ne sont pas d'ampleur 302 suffisante pour que cela vaille nécessairement le coup. Cela dit passer du 303 premier script au dernier a été assez rapide, les outils `sed` et `xargs` 304 permettant d'écrire du code très expressif et d'itérer très rapidement. 305 306 Donnons momentanément du crédit à notre méthode de calcul de consommation 307 d'énergie. On apprend que notre première solution, en plus d'être lente n'est 308 pas efficace. Je suppose que le temps perdu en accès mémoire, à relancer sed à 309 chaque fois etc se traduit aussi par de l'énergie perdue à les exécuter. 310 Autrement dit, on met une quantité non négligeable d'énergie à faire d'autres 311 choses que des substitutions. Un peu comme lorsque l'on pédale sur un mauvais 312 vélo. On y met de l'énergie mais seulement une fraction sert réellement à nous 313 faire avancer. 314 315 L'appel unique à sed est manifestement beaucoup plus efficace. Il est 316 raisonnable de penser qu'en s'économisant tout ce travail superflu il va plus 317 vite en consommant moins. 318 319 La technique la plus rapide a une consommation d'énergie similaire à la 320 précédente. Je suppose que les 3 cycles supplémentaires de lancement de sed 321 d'ouverture de fichier etc sont trop peu nombreux pour avoir une incidence 322 significative sur la consommation d'énergie. Ici la parallélisation a 323 considérablement accéléré la tâche[^3] (divisé par ~3) ce qui a suffit pour 324 compenser la charge presque quatre fois plus grande imposée au CPU. On constate 325 donc une accélération "gratuite" en terme de consommation d'énergie. Le temps 326 d'exécution de la tâche s'est plus ou moins linéairement réduite avec la 327 quantité de puissance mise dedans. D'ailleurs, et c'est certainement bien connu 328 des personnes bossant dans le domaine, j'ai le sentiment que l'on a des 329 rendements décroissants avec l'augmentation du nombre de processus parallélisé 330 et qu'ici quatre était le chiffre magique. Du moins c'est ce que je constate 331 souvent, j'imagine que ça dépend du matériel et du type de processus. Sur des 332 CPU avec de très nombreux cœurs on peut peut-être continuer à constater des 333 rendements linéaires pour des quantités de processus plus élevés. Est-ce que la 334 meilleure quantité de processus a un rapport avec la quantité de cœurs de son 335 CPU ? Si vous avez la réponse n'hésitez pas à m'éduquer. D'ailleurs si vous 336 avez une quelconque expertise en calcul intensif je veux bien un retour sur cet 337 article. J'ai l'impression de redécouvrir la roue mais en commençant avec un 338 prototype carré. 339 340 ## Aller plus loin en consommant moins 341 342 Le périmètre initiale de notre problème était celui de `sed`. Nous sommes 343 probablement allez aussi loin que possible sans avoir à y passer une quantité 344 de temps déraisonnable. Il est cependant possible de résoudre ce problème de 345 façon bien plus efficace à l'aide de langages de programmation plus complexes. 346 347 Marc Chantreux et Pierre Gerhard en font la démonstration avec respectivement 348 un [script perl](./sub.pl) et un [script python](./sub.py). Sur mon pc les 349 perfs sont : 350 351 | Méthode | texte d'origine | très gros texte | 352 |---------|-----------------|-----------------| 353 | script perl | 0,006s | 0,4s | 354 | script python | 0,005s | 0,7s | 355 356 Les deux scripts ont des performances énergétiques drastiquement meilleures que 357 tous nos scripts sed. Elles ajoutent une consommation énergétique d'environ 9J 358 pour le gros fichier soit deux ordres de grandeur en dessous de notre meilleur 359 résultat sed. 360 361 Pourquoi ? Parce que ces deux scripts n'ont qu'une commande de substitution. 362 Pour chaque ligne une seule tentative de substitution est faite contre 213 363 dans les scripts sed. Pour que cela puisse fonctionner quand bien même il y 364 a bien des chaînes différentes à substituer dans chacune des paires dans 365 `corres` ils utilisent une astuce avec un tableau associatif. En se reposant 366 sur le principe que toutes les substitutions vont commencer par la même chaine 367 `https://...` ont peut créer un "template" de substitution type : 368 369 s;https://sws.geonames.org/ID/about.rdf;NEWID;g 370 371 Où l'on récupère la clef ID pour chercher la valeur correspondante NEWID dans 372 le tableau associatif. Ainsi on construit une seule substitution que l'on 373 modifie à la volée lorsque l'on a matché le début de notre chaîne. Pour saisir 374 pourquoi c'est beaucoup plus efficace imaginons une ligne sur laquelle il n'y a 375 rien à substituer. Nos scripts sed vont tester une première fois de trouver un 376 h, puis un t, puis un t sans succès puis passer à une autre substitution pour 377 tenter de retrouver un h puis un t puis un t alors même que la première 378 substitution n'a pas matché et que l'on sait donc qu'aucune autre ne matchera ! 379 Nos scripts perl et python vont tenter de trouver un h, puis un t, puis un t 380 mais une fois arrivé au bout de la ligne sans match ils passeront à la ligne 381 suivante. La complexité en sed croît avec le produit de `nb_sub*nb_lignes` 382 quand nos scripts perl/python croient en `nb_lignes` uniquement. 383 384 Il semblerait donc que pour un problème de substitution de ce type et pour de 385 très gros volumes de donnée la complexité supplémentaire d'embarquer 386 perl/python et la relative verbosité des scripts associés puissent valoir le 387 coup temporellement et énergétiquement. C'est un cas intéressant d'arbitrage 388 coût/bénéfice, selon moi ici plutôt à l'avantage de perl/python. C'est aussi 389 une belle démonstration que la compétence des développeurs et l'adéquation des 390 outils peut faire des miracles pour les performances. Parfois bien plus que du 391 matériel plus puissant. C'est cette idée là qui me fait penser que la meilleure 392 chose que l'on puisse faire pour réduire l'impact environnemental d'un centre 393 de calcul est d'embaucher des ingénieur·e·s calcul supplémentaires pour 394 accompagner les chercheureuses. Bien évidemment les problèmes scientifiques 395 auxquels iels sont confronté·e·s sont extrêmement divers et ne peuvent pas tous 396 être aussi bien optimisés mais je parie que l'on serait surpris. 397 398 [^1]: git clone git://bitreich.org/energy 399 [^2]: #ubuntu 400 [^3]: contrairement à la première tentative de parallélisation de notr solution naïve 401 [^4]: Pour avoir comparé les valeurs avec un wattmètre branché sur une prise les valeurs sont étonamment justes. En tout cas les ordres de grandeur sont bons, ça nous suffit.