Explosion de performance Python avec Rust
Intégrer du Rust dans du Python, quelle drôle d'idée ! Est-ce worth ?
On entend souvent dire que Python est lent.
Mais entre nous, c'est plus nuancé que ça. En optimisant le code, il est possible de faire des merveilles sans avoir à dire adieu à Python.
Bien sûr, il est évident que les langages compilés, genre Rust, ont une longueur d'avance côté vitesse. Mais peu importe car Python peut embarquer ces langages tels que nous pouvons le voir dans des bibliothèques qui intègrent du C ou du Rust, comme Numpy ou Pydantic v2.
Disclaimer : J'ai commencé à faire du Rust très récemment, donc si j'ai raté des optimisations ou des bonnes pratiques, c'est normal.
Notre challenge : Advent Of Code 2022
Afin d’apprendre un nouveau langage, je trouve qu’il est pertinent de tenter de résoudre des problèmes concrets, et de monter en difficulté.
Pour ce faire j’ai décidé de prendre le Advent Of Code de 2022 et d’essayer de résoudre le challenge une première fois en full Python et une seconde fois en Python qui intègre du Rust afin de comparer les performances.
Pour plus de détails sur le défi, vous pouvez cliquer ici.
Je n’entrerai pas spécialement dans les détails techniques de la solution. Pour résumer l’énoncé, voici l’idée du challenge :
Nous avons une string contenant des calories pour différents elfes (univers de Noël). Nous devons trouver quel elfe possède le plus de calories. Le jeu de données ressemble à ceci :
1000
233
1000
1000
6000
400
...
Dans ce cas là, c’est le deuxième elfe qui possède le plus de calories, avec 1000 + 1000 + 6 000 calories = 8 000.
Le résultat attendu est donc 8 000.
Notre implémentation en Python
Voilà, c’est presque du one-line. Concrètement on itère sur chaque bloc représentant un elfe (les double “\n”), on prend chaque ligne que nous convertissons en entier puis les additionnons ensemble.
Enfin, nous ordonnons la liste de sommes afin d’avoir le plus grand nombre en premier .
Tadaaaa ! Nous savons quel est le maximum de calories parmi ces elfes.
Réécrire notre logique en Rust
Et maintenant, place à Rust.
On va utiliser une librairie appelée rustimport. Cet outil nous facilite la vie en compilant notre code Rust pour qu'il puisse être utilisé dans notre script Python, grâce à PyO3.
Rust bindings for Python, including tools for creating native Python extension modules. Running and interacting with Python code from a Rust binary is also supported.
Tiré de https://github.com/PyO3/pyo3
Voici notre fichier aoc.rs
Nous avons défini une fonction “compute” qui prend en paramètre du texte et qui réalise les mêmes traitements que notre script Python précédent.
Pour avoir plus de détails sur la syntaxe adéquate afin d’utiliser votre script dans votre programme Python, je vous renvoie vers la documentation.
Maintenant, voici à quoi ressemble notre script Python qui utilise ce fichier aoc.rs
Vous pouvez voir que le tout premier import vient de rustimport.
Qu’est-ce que rustimport.import_hook ?
C’est une ligne de code qui va permettre de regarder dans notre dossier s’il y a des fichiers .rs, et de les compiler afin d’être utilisables par Python.
Par exemple, nous avons un fichier aoc.rs, ce qui signifie que notre module Python s’appelle “aoc”.
Ensuite, nous pouvons appeler la méthode “compute” de aoc pour faire les calculs.
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.
Mesurons les performances
Pour cette occasion, j’ai décidé de mesurer le temps d’execution des deux fonctions “main” de mes scripts. Ceci exclut le temps de compilation mais mesure uniquement le temps d’execution de notre fonction.
On recommence cette opération 1 000 fois, on calcule la moyenne du speedup de Rust par rapport a Python et également l’écart type pour voir si les résultats sont proches.
En lançant notre comparatif nous avons ces résultats là :
Results :
Mean Speedup: 1.1833177238773642
Standard Deviation: 0.06543422441011415
WTF ? Rust est uniquement 1.18 fois plus rapide que Python ?
C’est étonnant, ne trouvez-vous pas ?
Et si ! Effectivement, il y un tips à savoir, un piège à éviter. Lorsqu’on compile “normalement” notre script Rust, le compilateur va générer une version “Debug” de notre script, et non pas une version optimisée telle qu’elle serait en production.
Pour ce faire, il suffit d’ajouter une ligne dans notre script Python pour préciser que nous voulons activer le build optimisé.
Et là, en relançant la mesure du temps, nous obtenons des résultats plus proches de ce qu’on espérait. Un speedup de quasiment x10.
Results :
Mean Speedup: 9.970547291038583
Standard Deviation: 0.44621048695561444
Aller plus loin
Comme je l’ai mentionné au début de cet article, je suis un débutant en Rust. Mais je suis sûr qu’avec plus d’expertise, les gains seraient bien plus importants.
A titre d’expérience, j’ai demandé à GPT de m’écrire une version optimisée de mon code. En parallèle, j’ai vérifié que les résultats étaient les mêmes.
J’ignore exactement ce qu’il se passe dans ce code, mais en l’executant nous obtenons les résultats suivants :
Results :
Mean Speedup: 21.137375292664284
Standard Deviation: 0.9441126168701539
Notre script Rust est 21x plus rapide que notre fonction Python originale ! C’est assez impressionant ! Nous avons plus que doublé les performances de notre version Rust initiale.
Conclusions
Intégration de fonctions Rust dans Python : Nous avons vu que l'intégration de fonctions Rust dans des programmes Python est plus accessible qu'il n'y paraît.
Gains de performance : L'utilisation de Rust peut offrir des avantages significatifs en termes de performance.
Ces gains de performance peuvent être utilisés pour optimiser certaines parties d'applications Python, des bottlenecks.
Ajout d'options pour optimiser la performance : En utilisant rustimport, il est essentiel d'ajouter certaines options spécifiques telles que “compile_release_binaries” lors de vos tests, sinon les résultats ne seront pas représentatifs du gain réel.
Cependant, il est important de noter que rustimport est un outil pour faciliter le prototypage, et savoir rapidement si intégrer Rust vous sera bénéfique.
Si vous comptez réellement utiliser du Rust dans votre codebase, vous devrez le packager plus proprement; de la même manière si vous souhaitez utiliser des 3rd party librairies Rust, vous devrez également ajouter de la configuration.
ET tout cela sera l’occasion d’un prochain article !
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.