Alors bon .. comment dire ?
Je rejoins ceux qui ne voient pas trop l'intérêt de cet article.
Comparer Python 3.10 à Python 3.11, pourquoi pas.
Cela permet de vérifier que l'on ne nous vend pas un nième épisode de
la grande saga "Les prochaines versions seront plus rapides".
(Je vois passer ces histoires depuis 20 ans à propos de Java, et s'il y a effectivement
des améliorations, elle passent toujours par la même porte:
un petit peu moins d'interprétation et un peu plus de compilation).
Au final, rien ne change fondamentalement.
Ce qui m'a intéressé ici est la version C++ de ce benchmark.
Oulà ! On peut dire que ça pique les yeux. Je me demande quel développeur à
écrit cela.
Je passe sur l'intérêt d'utiliser l'héritage ici.
Techniquement, il ne sert à rien dans ce cas précis, comme cela a déjà été dit.
Mais dans le cadre d'une simulation plus importante, pourquoi pas.
Mais dans ce cas, il faudra aussi du polymorphisme.
Pour ce qui est de l'écriture du code, on sent clairement que c'est une transcription de la version Python.
- Encapsulation ? Y'en a pas.
- Construction propre ? Y'en a pas non plus. Les objets sont construits, puis modifiés (par chance, tout est public, c'est commode comme du Python)
- RAII pour la fermeture automatique du flux ? Ah bon, ça existe ?
- Les calculs sont menés en dépit du bon sens :
ex: tirage d'un nombre aléatoire entre [0.0 et 1.0] : (rand() / (RAND_MAX + 1.0)
ben non, désolé, ce n'est pas ça. On obtient ici un nombre entre [0 et quelque chose qui est inférieur à 1]
Je sais ici ce qui s'est passé : la division entière à donné un résultat inattendu, et le "programmeur" s'est rendu compte que ça "marchait" en ajoutant 1.0.
Autre exemple de code tordu : { return (a->is_alive == false) ? true : false; }
ah bon ? { return !a->is_alive ; } ça ne serait pas un peu plus clair ?
Un autre petit pour la route : double squared_dist = pow((x - target->x), 2) + pow((y - target->y), 2);
Tu m'étonnes que sans l'option -O2 la programme n'aille pas vite !
La fonction pow est tout sauf simple (souvent log et exp sont de la partie) alors pour calculer un carré
par pitié, utilisez un produit ! Il se trouve que le compilateur est plus compétent que le programmeur: avec -O2
l'exponentiation est remplacée par un produit. J'ai vérifié.
- Et il y a aussi des erreurs de logique dans le code.
- Mais le pire du pire est la gestion de la mémoire. Comme certains l'on dit (mais il semble que ça n'intéresse personne)
ce programme est un gigatesque memory leak. Voici ce que dit valgrind :
==13641== HEAP SUMMARY:
==13641== in use at exit:
1,567,552 bytes in 22,579 blocks
==13641== total heap usage: 45,155 allocs, 22,576 frees, 2,190,672 bytes allocated
==13641==
==13641== LEAK SUMMARY:
==13641== definitely lost: 1,421,696 bytes in 22,214 blocks
==13641== indirectly lost: 22,976 bytes in 359 blocks
==13641== possibly lost: 0 bytes in 0 blocks
==13641== still reachable: 122,880 bytes in 6 blocks
==13641== suppressed: 0 bytes in 0 blocks
Mazette ! 1,567,552 bytes, ce n'est pas du goutte à goutte.
Ceci montre à quel point il y a des programmeurs qui ne possèdent pas les bases
les plus élémentaires. Et je pense que plus Python prend de l'importance (enseignement et industrie)
plus le nombre de ces programmeurs augmente.
A première vue, il suffit de désallouer les acteurs pointés par les différents pointeurs contenus dans les listes,
à la fin du programme, et ceux qui sont retirés des listes en cours de simulation.
En fait, c'est un peu plus compliqué, car chaque prédateur comporte un pointeur target vers sa proie.
Il est donc fréquent qu'un acteur soit "mort", mais encore référencé pour au moins un cycle de simulation,
par un ou plusieurs prédateurs.
il n'est pas simple de le desallouer au bon moment.
A moins de faire de grosses acrobaties, un compteur
de références s'impose. J'ai remplacé tous les pointeurs nus par des shared_ptr (5 minutes), et tout se passe bien.
L'empreinte mémoire est aussi plus faible à l'instant t.
==14063==
==14063== HEAP SUMMARY:
==14063== in use at exit: 0 bytes in 0 blocks
==14063== total heap usage: 67,606 allocs, 67,606 frees, 2,966,744 bytes allocated
==14063==
==14063== All heap blocks were freed -- no leaks are possible
==14063==
Si shared_ptr est trop luxueux (pas besoin d'être thread safe) un pointeur intelligent maison plus rustique marche aussi très bien
et consomme moins de mémoire.
==14316==
==14316== HEAP SUMMARY:
==14316== in use at exit: 0 bytes in 0 blocks
==14316== total heap usage: 45,220 allocs, 45,220 frees, 2,252,760 bytes allocated
==14316==
==14316== All heap blocks were freed -- no leaks are possible
==14316==
Vous vous doutez que j'ai vérifié les performances (rapidement, sur 10 20 ou 30 exec)
Effectivement Python 3.11 est plus rapide que 3.10 sur cet exemple. Les chiffres avancés
me semblent en accord avec ce que j'ai constaté.
En ce qui concerne Python 3.11 vs C++ (compilé en C++20, -O2), j'ai un ratio de 10 à 18 selon les séries,
mais souvent plus proche de 10. Je précise ce ces chiffres concerne la version sans fuite de mémoire.
Un phénomène m'intrigue : les résultats en C++ sont assez stables (ils varient dans un rapport de 1 à 2 environ)
par contre en Python 3.10 on a des pics aléatoires d'un facteur 5 (de 15 à 75 sec par exemple !). Cela se produit aussi en Python 3.11
mais c'est moins fréquent (de 10 à 50 par exemple). Si un connaisseur de Python a une explication, je suis preneur.
11 |
0 |