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

73PARTAGES

41  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 :

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import math
import random
 
from datetime import datetime
random.seed(datetime.now().timestamp())
 
WORLD_WIDTH = 2560
WORLD_HEIGHT = 1440
 
class Agent():
    def __init__(self, x=None, y=None):
        super().__init__()
 
        # default values
        self.vmax = 2.0
 
        # initial position
        self.x = x if x else random.randint(0, WORLD_WIDTH)
        self.y = y if y else random.randint(0, WORLD_HEIGHT)
 
        # initial velocity
        self.dx = 0
        self.dy = 0
 
        # inital values
        self.is_alive = True
        self.target = None
        self.age = 0
        self.energy = 0
 
    def update(self, food=()):
        self.age = self.age + 1
 
        # we can't move
        if self.vmax == 0:
            return
 
        # target is dead, don't chase it further
        if self.target and not self.target.is_alive:
            self.target = None
 
        # eat the target if close enough
        if self.target:
            squared_dist = (self.x - self.target.x) ** 2 + (self.y - self.target.y) ** 2
            if squared_dist < 400:
                self.target.is_alive = False
                self.energy = self.energy + 1
 
        # agent doesn't have a target, find a new one
        if not self.target:
            min_dist = 9999999
            min_agent = None
            for a in food:
                if a is not self and a.is_alive:
                    sq_dist = (self.x - a.x) ** 2 + (self.y - a.y) ** 2
                    if sq_dist < min_dist:
                        min_dist = sq_dist
                        min_agent = a
            if min_dist < 100000:
                self.target = min_agent
 
        # initalize 'forces' to zero
        fx = 0
        fy = 0
 
        # move in the direction of the target, if any
        if self.target:
            fx += 0.1*(self.target.x - self.x)
            fy += 0.1*(self.target.y - self.y)
 
        # update our direction based on the 'force'
        self.dx = self.dx + 0.05 * fx
        self.dy = self.dy + 0.05 * fy
 
        # slow down agent if it moves faster than it max velocity
        velocity = math.sqrt(self.dx ** 2 + self.dy ** 2)
        if velocity > self.vmax:
            self.dx = (self.dx / velocity) * (self.vmax)
            self.dy = (self.dy / velocity) * (self.vmax)
 
        # update position based on delta x/y
        self.x = self.x + self.dx
        self.y = self.y + self.dy
 
        # ensure it stays within the world boundaries
        self.x = max(self.x, 0)
        self.x = min(self.x, WORLD_WIDTH)
        self.y = max(self.y, 0)
        self.y = min(self.y, WORLD_HEIGHT)
 
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():
    # open the ouput file
    f = open('output.csv', 'w')
    print(0, ',', 'Title', ',', 'Predator Prey Relationship / Example 02 / Pthon', file=f)
 
    # 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
        #[f.update() for f in plants]  # no need to update the plants; they do not move
        [a.update(food=plants) for a in preys]
        [a.update(food=preys) for a in predators]
 
        # handle eaten and create new plant
        plants = [p for p in plants if p.is_alive is True]
        plants = plants + [Plant() for i in range(2)]
 
        # handle eaten and create new preys
        preys = [p for p in preys if p.is_alive is True]
 
        for p in preys[:]:
            if p.energy > 5:
                p.energy = 0
                preys.append(Prey(x = p.x + random.randint(-20, 20), y = p.y + random.randint(-20, 20)))
 
        # handle old and create new predators
        predators = [p for p in predators if p.age < 2000]
 
        for p in predators[:]:
            if p.energy > 10:
                p.energy = 0
                predators.append(Predator(x = p.x + random.randint(-20, 20), y = p.y + random.randint(-20, 20)))
 
        # write data to output file
        #[print(timestep, ',', 'Position', ',', 'Predator', ',', a.x, ',', a.y, file=f) for a in predators]
        #[print(timestep, ',', 'Position',  ',', 'Prey', ',', a.x, ',', a.y, file=f) for a in preys]
        #[print(timestep, ',', 'Position',  ',', 'Plant', ',', a.x, ',', a.y, file=f) for a in plants]
 
        timestep = timestep + 1
 
    print(len(predators), len(preys), len(plants))
 
if __name__ == "__main__":
    main()

Résultats du côte de Python 3.10, Python 3.11, Cython et C++

Résultats Python 3.10

Jusqu'à présent, les tests ont été effectués avec Python 3.10. Ainsi, lorsque nous examinons la sortie de synchronisation ci-dessus, nous constatons que l'exécution de la simulation dans Python 3.10 a pris 9,5 secondes.

C'est la valeur de base pour une expérimentation ultérieure. C'est la valeur que nous voulons battre.

Une note sur l'ordinateur utilisé pour effectuer ces mesures. Il a une spécification raisonnablement normale qui devrait être représentative d'un système utilisé pour effectuer de petites simulations scientifiques.
  • Processeur Intel Core i7–8700 à 3,20 GHz
  • 6 cœurs de processeur
  • 32 Go de RAM

Résultats Python 3.11

Il y avait beaucoup de bruit autour de la nouvelle version Python 3.11. On dit qu'elle est beaucoup plus rapide que les versions précédentes de Python. De nombreux articles confirment ce fait. La raison, cependant, est profondément enfouie dans les notes de version de la nouvelle version de Python :

L'idée générale est que bien que Python soit un langage dynamique, la plupart du code a des régions où les objets et les types changent rarement. Ce concept est connu sous le nom de stabilité de type.

Au moment de l'exécution, Python essaiera de rechercher des modèles communs et la stabilité du type dans le code d'exécution. Python remplacera alors l'opération en cours par une autre plus spécialisée. Cette opération spécialisée utilise des raccourcis disponibles uniquement pour les cas/types d'utilisation, qui surpassent généralement leurs homologues génériques.
Fort de cette information, Multi-Agent AI a exécuté à nouveau la même simulation, cette fois en utilisant Python 3.11. Et voici les résultats :


Le résultat est plus impressionnant qu'avec la version précédente : 5,8 secondes, ce qui est beaucoup plus rapide que les 9,5 secondes mesurées auparavant. Les améliorations fonctionnent mieux si le programme passe la plupart du temps dans une fonction ou une boucle de base interne. Si vous le pouvez, utilisez la dernière version de Python, cela peut potentiellement vous faire gagner beaucoup de temps.

Les simulations typiques passent la plupart de leur temps à calculer la même formule mathématique encore et encore - c'est exactement là que cette spécialisation des opérations mentionnée ci-dessus est la plus importante.

Résultats Cython

Normalement, le code Python est interprété à chaque fois qu'une ligne est exécutée. Cython, cependant, compile une fonction en code machine rapide (via C, d'où son nom). Il utilise la même syntaxe de base que Python, est entièrement interopérable et ajoute quelques fonctionnalités de langage supplémentaires pour optimiser les performances.

Le langage Cython est un sur-ensemble du langage Python qui prend également en charge l'appel de fonctions C et la déclaration de types C sur des variables et des attributs de classe. Cela permet au compilateur de générer du code C très efficace à partir du code Cython. Le code C est généré une fois, puis compilé avec tous les principaux compilateurs C/C++.

Apprendre Cython n'est pas difficile, mais cela nécessite un peu d'effort supplémentaire par rapport à Python. C'est plus compliqué à écrire, et plus d'étapes sont nécessaires pour exécuter le code.

Il existe différentes techniques d'optimisation disponibles dans Cython. La plus simple, et probablement la plus efficace, est la déclaration des types de variables. Cette étape simple libère le programme de la vérification du type d'une variable à chaque étape (exactement ce qui rend Python 3.11 si rapide).

Code cython : 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
cdef class Agent():

    cdef readonly double x
    cdef readonly double y

    cdef double dx
    cdef double dy

    cdef Agent target

    # here: more definitions

    def __init__(self, x=None, y=None, world_width=0, world_height=0):
       
    # here: agent initialization
  
    cpdef void update(self, list food) except *:
        cdef double min_dist
        cdef double squared_dist
        cdef double fx
        cdef double fy

        self.age = self.age + 1

        # here: agent position update logic

Cette nouvelle version de l'exemple de simulation s'exécute maintenant en 4,2 secondes. C'est deux fois plus rapide que le programme original, et toujours plus rapide que Python 3.11. La seule partie réécrite dans Cython était la classe Agent() comme indiqué ci-dessus. Le reste du programme a été laissé en Python normal.


Pour les plus curieux, voici le code :

Code Cython : 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""
python setup.py build_ext --inplace
"""

import random

from datetime import datetime
random.seed(datetime.now().timestamp())

WORLD_WIDTH = 2560
WORLD_HEIGHT = 1440

from agent import Agent

class Predator(Agent):
    def __init__(self, x=None, y=None, world_width=0, world_height=0):
        super().__init__(world_width=world_width, world_height=world_height)
        self.vmax = 2.5

class Prey(Agent):
    def __init__(self, x=None, y=None, world_width=0, world_height=0):
        super().__init__(world_width=world_width, world_height=world_height)
        self.vmax = 2.0

class Plant(Agent):
    def __init__(self, x=None, y=None, world_width=0, world_height=0):
        super().__init__(world_width=world_width, world_height=world_height)
        self.vmax = 0


def main():
    # open the ouput file
    f = open('output.csv', 'w')
    print(0, ',', 'Title', ',', 'Predator Prey Relationship / Example 02 / Cython', file=f)

    # create initial agents
    preys = [Prey(world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT) for i in range(10)]
    predators = [Predator(world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT) for i in range(10)]
    plants = [Plant(world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT) for i in range(100)]

    timestep = 0
    while timestep < 10000:
        # update all agents
        #[f.update([]) for f in plants]  # no need to update the plants; they do not move
        [a.update(plants) for a in preys]
        [a.update(preys) for a in predators]

        # handle eaten and create new plant
        plants = [p for p in plants if p.is_alive is True]
        plants = plants + [Plant(world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT) for i in range(2)]

        # handle eaten and create new preys
        preys = [p for p in preys if p.is_alive is True]

        for p in preys[:]:
            if p.energy > 5:
                p.energy = 0
                preys.append(Prey(x = p.x + random.randint(-20, 20), y = p.y + random.randint(-20, 20), world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT))

        # handle old and create new predators
        predators = [p for p in predators if p.age < 2000]

        for p in predators[:]:
            if p.energy > 10:
                p.energy = 0
                predators.append(Predator(x = p.x + random.randint(-20, 20), y = p.y + random.randint(-20, 20), world_width=WORLD_WIDTH, world_height=WORLD_HEIGHT))

        # write data to output file
        #[print(timestep, ',', 'Position', ',', 'Predator', ',', a.x, ',', a.y, file=f) for a in predators]
        #[print(timestep, ',', 'Position',  ',', 'Prey', ',', a.x, ',', a.y, file=f) for a in preys]
        #[print(timestep, ',', 'Position',  ',', 'Plant', ',', a.x, ',', a.y, file=f) for a in plants]

        timestep = timestep + 1

    print(len(predators), len(preys), len(plants))

if __name__ == "__main__":
    main()

Résultat C++

Python est un langage interprété avec typage dynamique. Cela signifie que chaque ligne de code est interprétée pendant l'exécution du programme et traduite en code machine à la volée. Le type de variables n'est pas connu à l'avance et peut même changer.

Le C++, quant à lui, est compilé et utilise le typage statique. Cela a des avantages de vitesse. Le compilateur peut analyser l'ensemble du programme et optimiser le code machine à l'avance. Et il n'est pas nécessaire de vérifier le type des variables pendant l'exécution du programme.

Les deux langages sont des langages orientés objet, et la structure de notre programme est identique.

Code C++ : 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
using namespace std;
 
class Agent {
    // here: Agent definition
}
 
class Predator: public Agent {
    public:
    Predator() : Agent() {
         vmax = 2.5;
    }
};
 
// here: the same for Prey and Plant classes
 
int main(int argc, char *argv[]) {
    srand (time(NULL));
 
    // create initial agents        
    list<Agent*> predators;
    for (int i = 0; i < 10; i++) {
        Predator *p = new Predator();
        predators.push_back(p);
    }
 
    // here: the same for Preys and Plants
 
    int timestep = 0;
    while (timestep < 10000) {
        // update all agents
        for (auto p: predators) { p->update(preys); }
        for (auto p: preys) { p->update(plants); }
 
        // 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++;
    }
}

Lorsque nous compilerons ceci, nous pourrions être surpris. Le temps d'exécution peut être assez lent, dans la région de la vitesse de Python. La raison en est que, par défaut, le compilateur laisse de nombreuses vérifications dans le code. C'est un peu comme Python alors et ralentit vraiment la simulation.

L'activation des indicateurs d'optimisation oblige le compilateur à tenter d'améliorer les performances et/ou la taille du code au détriment du temps de compilation et éventuellement de la possibilité de déboguer le programme. Le compilateur effectue une optimisation basée sur la connaissance qu'il a du programme.
g++ example_02.cpp -o example_02 -O3
Si nous activons l'optimisation du temps de compilation à l'aide de l'indicateur -O3, le résultat est différent :


La même simulation qu'avant s'exécute maintenant en 0,78 seconde, soit 10 fois plus rapidement que l'équivalent Python.

Voici le code pour les plus curieux :

Code C++ : 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Use something like this to compile:
// g++ example_02.cpp -o example_02  -std=c++11 -O3
 
#include <iostream>
#include <random>
#include <list>
#include <fstream>
 
using namespace std;
 
const double WORLD_WIDTH = 2560.0;
const double WORLD_HEIGHT = 1440.0;
 
class Agent {
public:
    double vmax = 0.0;
    double x;
    double y;
    double dx = 0.0;
    double dy = 0.0;
    bool is_alive = true;
    Agent* target = NULL;
    int age = 0;
    int energy = 0;
 
    Agent() {
        x = WORLD_WIDTH * (rand() / (RAND_MAX + 1.0));
        y = WORLD_HEIGHT * (rand() / (RAND_MAX + 1.0));
    }
 
    void update(const list<Agent*>& food) {
        age++;
 
        // we can't move
        if (vmax == 0.0) { return; }
 
        // target is dead, don't chase it further
        if ((target != NULL) && (target->is_alive == false)) {
            target = NULL;
        }
 
        // eat the target if close enough
        if (target != NULL) {
            double squared_dist = pow((x - target->x), 2) + pow((y - target->y), 2);
            if (squared_dist < 400) {
                target->is_alive = false;
                energy++;
            }
        }
 
        // agent doesn't have a target, find a new one
        if (target == NULL) {
            double min_dist = 9999999;
            Agent* min_agent = NULL;
 
            for (const auto a: food) {
                if (a->is_alive == true) {
                    double squared_dist = pow((x - a->x), 2) + pow((y - a->y), 2);
                    if (squared_dist < min_dist) {
                        min_dist = squared_dist;
                        min_agent = a;
                    }
                }
            }
            if (min_dist < 100000) {
                target = min_agent;
            }
        }
 
        // initialize forces to zero
        double fx = 0;
        double fy = 0;
 
        // move in the direction of the target, if any
        if (target != NULL) {
            fx += 0.1*(target->x - x);
            fy += 0.1*(target->y - y);
        }
 
        // update our direction based on the 'force'
        dx += 0.05 * fx;
        dy += 0.05 * fy;
 
        // slow down agent if it moves faster than its max velocity
        double velocity = sqrt(pow(dx, 2) + pow(dy, 2));
        if (velocity > vmax) {
            dx = (dx / velocity) * (vmax);
            dy = (dy / velocity) * (vmax);
        }
 
        // update position based on delta x/y
        x += dx;
        y += dy;
 
        // ensure it stays within the world boundaries
        x = max(x, 0.0);
        x = min(x, WORLD_WIDTH);
        y = max(y, 0.0);
        y = min(y, WORLD_HEIGHT);
    }
};
 
 
class Predator: public Agent {
    public:
    Predator() : Agent() {
         vmax = 2.5;
    }
};
 
class Prey: public Agent {
    public:
    Prey() : Agent() {
         vmax = 2.0;
    }
};
 
class Plant: public Agent {
    public:
    Plant() : Agent() {
         vmax = 0.0;
    }
};
 
int main(int argc, char *argv[]) {
    srand (time(NULL));
 
    std::ios_base::sync_with_stdio(false);
 
    list<Agent*> predators;
    list<Agent*> preys;
    list<Agent*> plants;
 
    list<Agent*> empty;
 
    // create initial agents
    for (int i = 0; i < 10; i++) {
        Predator *p = new Predator();
        predators.push_back(p);
    }
    for (int i = 0; i < 10; i++) {
        Prey *p = new Prey();
        preys.push_back(p);
    }
    for (int i = 0; i < 100; i++) {
        Plant *p = new Plant();
        plants.push_back(p);
    }
 
    std::ofstream outfile;
    outfile.open ("output.csv");
 
    int timestep = 0;
    outfile << timestep << ',' << "Title" << ',' << "Predator Prey Relationship / Example 02 / C++" << endl;
 
    while (timestep < 10000) {
        // update all agents
        for (auto p: predators) { p->update(preys); }
        for (auto p: preys) { p->update(plants); }
        //for (auto p: plants) { p->update(empty); }  // no need to update the plants; they do not move
 
        // handle eaten and create new plants
        plants.remove_if([](const Agent* a) { return (a->is_alive == false) ? true : false; });
        for (int i=0; i < 2; i++) { plants.push_back(new Plant()); };
 
        // handle eaten and create new preys
        preys.remove_if([](const Agent* a) { return (a->is_alive == false) ? true : false; });
 
        for (auto p: preys) {
            if (p->energy > 5) {
                p->energy = 0;
                Prey* np = new Prey();
                np->x = p->x + -20 + 40 * (rand() / (RAND_MAX + 1.0));
                np->y = p->y + -20 + 40 * (rand() / (RAND_MAX + 1.0));
                preys.push_back(np);
            }
        }
 
        // handle old and create new predators
        predators.remove_if([](const Agent* a) { return (a->age > 2000) ? true : false; });
 
        for (auto p: predators) {
            if (p->energy > 10) {
                p->energy = 0;
                Predator* np = new Predator();
                np->x = p->x + -20 + 40 * (rand() / (RAND_MAX + 1.0));
                np->y = p->y + -20 + 40 * (rand() / (RAND_MAX + 1.0));
                predators.push_back(np);
            }
        }
 
        // write data to output file
        /*
        for (const auto p: predators) {
            outfile << timestep << ',' << "Position" << ',' << "Predator" << ',' << p->x << ',' << p->y << endl;
        }
        for (const auto p: preys) {
            outfile << timestep << ',' << "Position" << ',' << "Prey" << ',' << p->x << ',' << p->y << endl;
        }
        for (const auto p: plants) {
            outfile << timestep << ',' << "Position" << ',' << "Plant" << ',' << p->x << ',' << p->y << endl;
        }
        */
 
        timestep++;
    }
 
    outfile.close();
    cout << predators.size() << ", " << preys.size() << ", " << plants.size() << endl;
}

Et Multi-Agent AI d'aller de ce commentaire :

C++ est la norme de facto pour les simulations basées sur la physique. Pour de bonnes raisons; il est bien connu, capable de gérer des structures de données complexes et l'un des langages les plus rapides disponibles.

Il est environ 10 fois plus rapide que Python, selon la taille et la complexité de la simulation. Ces exemples de jouets prennent ici quelques secondes à s'exécuter, donc le gain de performances n'a pas vraiment d'importance. Pour des simulations à grande échelle, un gain de performances de cet ordre de grandeur est très important. C'est vraiment important si une simulation dure une journée ou une semaine.
Conclusion

Voici le résumé des résultats obtenus par langage :

Langage Temps (en secondes)
Python 3.10 9,5
Python 3.11 5,8
Cython 4,2
C++ 0.78

Cette approche suggère que Python 3.11 peut être deux fois plus rapide que les anciennes versions de Python. Pourtant, il existe un écart important par rapport aux performances du C++ optimisé. Toutefois, il faut prendre ces résultats avec du recul, étant donné que cela dépend vraiment de vos besoins. D'ailleurs, Multi-Agent AI fait les recommandations suivantes aux professionnels dans le domaine des data science :
  • Si vous voulez un prototype de simulation, faites-le en Python. Il est propre et facile à lire à la fois par les développeurs et les personnes connaissant le domaine.
  • S'il y a une partie qui est au cœur de la simulation, envisagez de la réécrire dans Cython. Surtout si elle est utilisée par d'autres personnes comme module stable.
  • Si vous construisez une simulation à grande échelle ou travaillez sur un projet de plus longue durée, divisez la simulation en parties significatives, comme la simulation, la journalisation, la visualisation et l'analyse des données.
  • Ensuite, réécrivez la simulation réelle en C++, par exemple. Pour les autres parties, utilisez les langages qui conviennent le mieux*; comme Python pour l'analyse des données.
  • Pour obtenir les meilleures performances, utilisez toutes les techniques ensemble. Une simulation vectorisée en C++ s'exécutant sur plusieurs processeurs est la plus rapide. De plus, c'est le plus difficile à programmer et à maintenir.

Source : Muti-Agent AI

Et vous ?

Lequel de ces langages utilisez-vous ?
Que pensez-vous de cette approche pour mesurer la vitesse de ces langages ?
Êtes-vous surpris par le classement ? Les écarts de vitesse ?
Un langage devrait-il être utilisé en fonction du besoin ou en fonction de la compétence ? Pourquoi ?

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

Avatar de floyer
Membre averti 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.
11  1 
Avatar de amorgos
Nouveau 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.
10  0 
Avatar de redcurve
Membre extrêmement actif 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
6  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.
4  0 
Avatar de pebaroiller
Futur Membre du Club 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é ..
7  4 
Avatar de floyer
Membre averti 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 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é.
8  6 
Avatar de archqt
Membre chevronné 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.
2  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
7  6 
Avatar de eric44000
Membre actif 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.
2  1