IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Python 3.11 et C++ : quelles sont les performances de ces deux langages en matière de simulation ?
Comparaison de vitesse à l'aide d'une simulation scientifique

Le , par Stéphane le calme

249PARTAGES

44  1 
Python 3.11, la dernière version langage, serait significativement plus rapide que les versions précédentes. Python 3.11 présenterait des améliorations de performance spectaculaires par rapport à Python 3.10 et aux versions antérieures. Après un test réalisé récemment, Bram Wasti, ingénieur système chez Meta, affirme que Python 3.11 est 3 fois plus rapide que la version 3.8. Wasti pense tout de même qu'il y a encore du chemin à faire en matière d'améliorations des performances.

Si Python a gagné en popularité, en particulier dans le domaine de la science des données, que représente ce gain de vitesse face à des langages comme C++ dans le même domaine ? C'est la question à laquelle a tenté de répondre un développeur en proposant un benchmark spécialement conçu pour cette discipline (science des données).


Un langage qui gagne en popularité, en particulier dans le domaine de la science des données

Depuis un an déjà, Python occupe la première place de l'index TIOBE. Lors de son arrivée en pole position, Paul Jansen, PDG de Tiobe, notait déjà que c'était une première en 20 ans :

« Pour la première fois depuis plus de 20 ans, nous avons un nouveau chef de file : le langage de programmation Python. L'hégémonie de longue date de Java et C est terminée. Python, qui a commencé comme un simple langage de script, comme alternative à Perl, est devenu mature. Sa facilité d'apprentissage, son énorme quantité de bibliothèques et son utilisation répandue dans toutes sortes de domaines en ont fait le langage de programmation le plus populaire d'aujourd'hui. Félicitations Guido van Rossum ! Proficiat ! »

Python est un langage de programmation interprété, multi-paradigme et multi-plateformes. Il favorise la programmation impérative structurée, fonctionnelle et orientée objet. Il est doté d'un typage dynamique fort, d'une gestion automatique de la mémoire par récupérateur de mémoire et d'un système de gestion d'exceptions ; il ressemble ainsi à Perl, Ruby, Scheme, Smalltalk et Tcl.

Python gagne en popularité ces temps-ci, en partie à cause de l'essor de la science des données et de son écosystème de bibliothèques logicielles d'apprentissage automatique comme NumPy, Pandas, TensorFlow de Google et PyTorch de Facebook.

En effet, Python continuerait d'être la norme et la compétence la plus recherchée dans le domaine de la science des données, dépassant de loin les autres technologies et outils, comme R, SAS, Hadoop et Java. C'est ce que suggère une analyse réalisée par Terence Shin, un spécialiste des données, qui a indiqué que l'adoption de Python pour la science des données continue de croître alors même que le langage R, plus spécialisé, est en déclin. Bien entendu, cela ne veut pas dire que les spécialistes des données vont abandonner R de sitôt. L'on continuera probablement à voir Python et R utilisés pour leurs forces respectives.

Entre Python 3.11, Cython et C++, qui est le plus rapide ?

Multi-Agent AI s'est amusé à comparer les performances de Python 3.11, Cython vs C++ pour les simulations, indiquant que son approche est adaptée aux scientifiques des données et aux personnes ayant des connaissances dans le domaine qui souhaitent créer une simulation ou quelque chose de similaire : « il devrait être clair à l'avance que C++ est toujours plus rapide que Python. La question est de savoir de combien ? »

Pour mémoire, Cython est un langage de programmation et un compilateur qui simplifient l'écriture d'extensions compilées pour Python. La syntaxe du langage est très similaire à Python mais il supporte en plus un sous-ensemble du langage C/C++. Le premier intérêt de Cython est qu'il produit du code nettement plus performant.

Comme exemple de simulation scientifique, voici les résultats d'une simulation de Multi-Agent AI :


Un système de simulation à grande échelle ne doit pas être un programme monolithique où tout se fait au même endroit. Il est logique de séparer par exemple la simulation de base à partir de la visualisation ou de l'analyse des données. Pour cet exercice, Multi-Agent AI s'est intéressé uniquement aux performances de la simulation, et la visualisation est donc déplacée vers un autre programme qui n'est pas traité ici.

De toute évidence, un système de simulation doit communiquer avec la visualisation, soit hors ligne à l'aide de fichiers de données de sortie, soit en ligne à l'aide de flux réseau, par exemple. Ces deux méthodes sortent du cadre de cet exercice. En fait, toutes les sorties sont simplement désactivées à cet effet.

Python 3.10 et Python 3.11

La simulation est une simple simulation spatiale de la relation prédateur-proie. Tous les animaux, prédateurs et proies, sont modélisés comme des agents. La programmation orientée objet est un choix naturel. Chaque agent est un objet, et nous avons différents types d'agents. Multi-Agent AI définit le comportement générique d'un agent dans la classe Agent(). C'est la responsabilité de la méthode update() de calculer la nouvelle position d'un agent. L'ensemble de la simulation est assez simple, mais elle montre un comportement assez complexe.

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import random 
from datetime import datetime 
random.seed(datetime.now().timestamp()) 
  
class Agent(): 
    def __init__(self, x=None, y=None): 
        # here: agent initialization 
    def update(self, food=()): 
        self.age = self.age + 1 
        # here: agent position update logic 
  
class Predator(Agent): 
    def __init__(self, x=None, y=None): 
        super().__init__() 
        self.vmax = 2.5 
  
class Prey(Agent): 
    def __init__(self, x=None, y=None): 
        super().__init__() 
        self.vmax = 2.0 
  
class Plant(Agent): 
    def __init__(self, x=None, y=None): 
        super().__init__() 
        self.vmax = 0 
  
def main(): 
    # create initial agents 
    preys = [Prey() for i in range(10)] 
    predators = [Predator() for i in range(10)] 
    plants = [Plant() for i in range(100)] 
    timestep = 0 
  
    while timestep < 10000: 
        # update all agents 
        [a.update(food=plants) for a in preys] 
        [a.update(food=preys) for a in predators] 
  
        # here: handle eaten plants and create new plants 
        # here: handle eaten prey and create new prey 
        # here: handle old predators and create new predators 
  
        timestep = timestep + 1 
  
if __name__ == "__main__": 
    main()

Comment mesurer le temps

Une fois que nous avons décidé de mesurer la rapidité d'un programme, nous devons définir exactement comment nous allons le faire. Au début, cela semble être une tâche triviale. Exécutez simplement le programme et mesurez le temps qu'il a fallu.

La simulation test est initialement peuplée de prédateurs, de proies et de plantes. Ils ont chacun une position aléatoire. Or, il est connu d'ensemencer le générateur de nombres aléatoires à l'aide d'une valeur fixe. Cela garantit que si vous exécutez le même programme deux fois, le résultat sera le même.

Si vous écrivez un programme dans deux langages différents, il peut y avoir de subtiles différences de comportement. Dans le cas de simulations comme celle que nous utilisons ici, par exemple, les erreurs d'arrondi peuvent faire la différence. Un langage peut utiliser une méthode différente pour faire de l'arithmétique ou utiliser une notation interne et une précision différentes pour les nombres à virgule flottante.

Même avec exactement le même point de départ, le résultat peut être complètement différent, et donc aussi le temps d'exécution.

Une approche possible consiste à amorcer le générateur de nombres aléatoires en utilisant une valeur aléatoire, comme l'heure actuelle. Exécutez ensuite l'expérience plusieurs fois et utilisez un temps d'exécution moyen.

Code Python : Sélectionner tout
1
2
3
4
5
6
7
8
9
# Seeding the RNG in Python 
import random 
from datetime import datetime 
random.seed(datetime.now().timestamp()) 
  
// Seeding the RNG in C++ 
#include <random> 
#include <ctime> 
srand (time(NULL));

Il existe une commande UNIX appelée time qui mesure le temps d'exécution d'un autre programme. C'est très pratique pour des estimations rapides. Les valeurs qu'il rapporte sont :
  • real : Temps réel écoulé (horloge murale) ;
  • user : Temps total utilisé directement par le processus (en mode utilisateur) ;
  • sys: temps total utilisé par le système pour le compte du processus (en mode noyau).

Pour notre propos, c'est le temps réel qui nous intéresse. L'exécution du programme a pris 8,33 secondes dans l'exemple ci-dessous :


Voici le code pour les plus curieux :...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de floyer
Membre éclairé https://www.developpez.com
Le 27/12/2022 à 23:39
On peut aussi avoir un programme Python dont le temps d’exécution est principalement passé dans les bibliothèques (Numpy, Panda, Tensorflow…), et du coup, le temps passé dans l’interpréteur Python apparaît moins important. Et le caractère pratique de la bibliothèque est prédominant.
12  1 
Avatar de amorgos
Membre du Club https://www.developpez.com
Le 31/12/2022 à 22:46
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 
Avatar de redcurve
Inactif https://www.developpez.com
Le 28/12/2022 à 4:18
Citation Envoyé par floyer Voir le message
Le on peut aussi avoir un programme Python dont le temps d’exécution est principalement passé dans les bibliothèques (Numpy, Panda, Tensorflow…), et du coup, le temps passé dans l’interpréteur Python apparaît moins important. Et le caractère pratique de la bibliothèque est prédominant.
Elles sont toutes écrites en C ou C++ pratiquement aucun code python n'entre en jeu en utilisant ces bibliothèques
7  1 
Avatar de Pierre Louis Chevalier
Expert éminent sénior https://www.developpez.com
Le 29/12/2022 à 14:59
Citation Envoyé par pebaroiller Voir le message
Pourquoi comparer du code "interprété" avec du code compilé ? ...
Pour avoir des données précises, et au vu des informations prendre les décisions adéquates en fonction du type de projet.
5  0 
Avatar de dourouc05
Responsable Qt & Livres https://www.developpez.com
Le 27/12/2022 à 23:08
Citation Envoyé par grunk Voir le message
Python est un des langaes interprété les plus lent , C++ un des langages compilé les plus rapide. Tout le monde le sait.
Tout le monde le sait, mais c'est faux ? Certes, C++ est toujours dans le haut du classement en termes de performance (sauf code mal écrit), mais Python fait d'énormes progrès en termes de performance. Par exemple, la version 3.11 est prévue pour être 10 à 60 % plus rapide que la 3.10. Stéphane en parle bien dans son message. Certes, on sera toujours loin de C++, mais sur cet exemple Python passe de 12 fois plus lent à 7 fois, c'est un beau progrès, ça limite le besoin de passer à du code compilé.
10  7 
Avatar de pebaroiller
Membre à l'essai https://www.developpez.com
Le 29/12/2022 à 11:33
Pourquoi comparer du code "interprété" avec du code compilé ? ...

Que ce soit du code généré en pyton, js, php ou n'improte quel autre langage qui utilise un preprocesseur pour "pré-compilier" puis exécuter le code , celui-ci sera toujours plus lent qu'un code compilé ..
8  5 
Avatar de floyer
Membre éclairé https://www.developpez.com
Le 29/12/2022 à 15:37
De plus, avec Cython, on a du code compilé. Il
Est donc intéressant de situer ce compilateur.

Et même s’il est évident qu’un language semi-compilé (compilé en byte code) sera moins performant que le langage C compilé, avoir des ordres de grandeur de la différence peut être utile. Est-ce un facteur 2 ? 3 ? 10 ? …
3  0 
Avatar de grunk
Modérateur https://www.developpez.com
Le 27/12/2022 à 22:04
Je vois pas bien l'intérêt de comparer Python et C++ en terme de perf.
Python est un des langaes interprété les plus lent , C++ un des langages compilé les plus rapide. Tout le monde le sait.

Python est un super outil car il permet de mettre en place des choses rapidement , mais dès que les performances sont importante on le remplace
9  7 
Avatar de eric44000
Membre averti https://www.developpez.com
Le 29/12/2022 à 4:19
Citation Envoyé par Stéphane le calme Voir le message

Lequel de ces langages utilisez-vous ?
Les 2 mon capitaine (et Rust en plus).

Il n'y a pas que la vitesse d’exécution qui compte sinon c'est le langage assembleur qui serait le seul utilisé. Le C a été inventé pour s'affranchir de son bas niveau d'abstraction: le compilateur a traduit le langage C en assembleur. Mais la compilation prend du temps d'où l'intérêt de Python, sans compter que Python est encore de plus haut niveau (proche du langage naturel).

Tant que Python n'est pas rédhibitoire en terme d’exécution (tourne même sur un Raspberry, si, si), il est préférable tant au niveau de l'écriture du code (le plus rapide) que de sa maintenance (le plus concis et lisible).

Dans l'autre cas, rien n'empêche, comme il est dit dans l'article, d'écrire les briques en C/C++/Rust et le ciment en Python. Ou même d'imaginer un tandem avec un programme d'interface écrit en Python s’exécutant un Raspberry et un moteur écrit en C s'exécutant sur un supercalculateur.
3  1 
Avatar de archqt
Membre émérite https://www.developpez.com
Le 29/12/2022 à 18:38
Citation Envoyé par archqt Voir le message
Sinon gros soucis de mémoire sur le code C++

L'héritage ne sert à rien, il suffit juste de mettre un vmax de départ dans le constructeur
Que celui qui moinsse de façon débile ou compulsive explique s'il en a le courage pourquoi d'après lui il n'y a pas de soucis de mémoire, et aussi à quoi sert l'héritage dans ce cas.
4  2