Programmation et développement logiciel

Comment intégrer progressivement Rust pour accélérer une base de code existante sans la réécrire entièrement

Lily Mara explique une méthodologie de refactorisation des fonctions via l’interface de liaison entre langages FFI, afin de transférer certaines parties de Python vers Rust à l’aide de PyO3, tout en conservant l’application existante et en réduisant les risques d’une réécriture complète. L’expérience montre que l’accélération au niveau d’une fonction peut dépasser 100 fois, tandis que l’amélioration globale lors d’un test HTTP n’a atteint qu’environ 15 %, ce qui souligne l’importance des mesures au niveau du système.

2026-09-15
6 min de lecture
10 vues
فريق تحرير certi.news
Comment intégrer progressivement Rust pour accélérer une base de code existante sans la réécrire entièrement

Plutôt que de réécrire une application entière en Rust, Lily Mara, Staff Engineer chez Discord, propose de transférer progressivement les fonctions les plus gourmandes en ressources d’un langage dynamique comme Python vers Rust, puis de relier les deux implémentations au sein du même processus. Mara a présenté cette méthodologie lors d’une session publiée par InfoQ, en s’appuyant sur son expérience dans la conception de systèmes distribués capables d’envoyer quotidiennement des dizaines de milliards de notifications aux utilisateurs de Discord, ainsi que sur son livre Refactoring to Rust.

L’idée centrale n’est pas de remplacer un langage par un autre simplement parce que Rust est plus rapide, mais d’identifier les parties restreintes du système dont l’optimisation aurait le plus grand impact. Cette approche permet de limiter l’ampleur des changements, de tester le nouveau comportement par rapport à l’ancien et de conserver l’implémentation d’origine lorsqu’il est difficile de garantir l’identité des résultats.

Pourquoi ne pas commencer par une réécriture complète ?

Mara estime que l’envie de reconstruire entièrement un ancien système dans un langage plus récent est compréhensible, mais qu’elle comporte d’importants risques pratiques. Les projets de réécriture peuvent dépasser les délais, devenir plus complexes que prévu ou réintroduire des erreurs que l’ancien système avait déjà corrigées. En outre, l’ancien code n’est pas toujours complexe en raison de son âge : il peut refléter des contraintes et des détails accumulés au fil de l’utilisation réelle ainsi que des connaissances institutionnelles difficiles à transférer vers un nouveau projet.

La session met également en garde contre la réduction des problèmes de performance au seul langage de programmation. La lenteur peut provenir du schéma de la base de données, des modes d’interrogation et de mise en cache, ou encore de la conception des services, et non du coût d’exécution d’une ligne de code. Ainsi, tirer parti de Rust ne signifie pas qu’une réécriture corrigera automatiquement les goulets d’étranglement architecturaux.

Qu’entend-on par refactorisation via FFI ?

La méthodologie utilise ce que Mara appelle le FFI refactoring, c’est-à-dire la réécriture d’une fonction ou d’une petite partie des fonctions dans un autre langage, puis son raccordement à l’application existante via une interface de liaison entre langages. Dans l’exemple présenté, l’application Flask continue de recevoir la requête HTTP et de décoder le JSON, puis transmet les données à une fonction statistique écrite en Rust, avant de renvoyer les résultats à Python et d’envoyer la réponse.

La liaison repose sur une interface C, qui sert concrètement de langage commun entre de nombreux systèmes et langages. Dans le cas de Python, le projet PyO3 fournit des outils pour créer un module importable depuis Python, tandis que Maturin peut être utilisé pour construire ce module. Des attributs tels que pyfunction et pyclass transforment les fonctions et les structures de données Rust en interfaces pouvant être appelées depuis Python.

Choisir la bonne fonction et mesurer l’impact

L’expérience recommande de rechercher les fonctions qui combinent une fréquence d’appel élevée et un coût d’appel important. Une fonction peut être relativement peu coûteuse à chaque exécution, tout en étant exécutée pour chaque requête, comme la logique de validation placée devant les gestionnaires d’API. À l’inverse, certaines opérations peuvent être rares mais extrêmement coûteuses. Le critère est l’impact cumulé sur le temps processeur, et non l’impression qu’une fonction donnée semble lente.

Dans l’exemple statistique, l’implémentation Rust était un peu plus de cent fois plus rapide lorsque la fonction elle-même était mesurée depuis Python, bien que le test ait exécuté les deux applications via l’interpréteur Python. Toutefois, lors de la mesure d’un gestionnaire HTTP complet, incluant Flask ainsi que la sérialisation et la désérialisation JSON, l’amélioration n’a atteint qu’environ 15 %. Le temps d’exécution de la version d’origine de la fonction était d’environ 86 microsecondes, une durée faible prise isolément, mais susceptible de devenir un coût important lorsqu’elle est répétée à grande échelle.

Cette différence entre la mesure partielle et la mesure globale constitue l’un des principaux enseignements de la session : il ne suffit pas d’enregistrer l’accélération d’une fonction. Il faut également effectuer des tests de bout en bout représentant le parcours réel d’utilisation et incluant, si nécessaire, le réseau ou le framework web ainsi que la sérialisation des données.

Compatibilité fonctionnelle, tests et limites

La réimplémentation de l’exemple a révélé une différence entre les résultats de la bibliothèque statistique en Python et ceux de la bibliothèque Rust. L’une des bibliothèques calculait les quartiles avec précision, tandis que l’autre utilisait des estimations adaptées aux ensembles de données très volumineux. De légères différences d’arrondi décimal sont également apparues. Il ne faut donc pas supposer que le remplacement d’une bibliothèque préserve automatiquement le même comportement.

Si l’identité des résultats est indispensable, il est possible de rechercher une autre bibliothèque, de réimplémenter l’algorithme d’origine en Rust ou de conserver la partie sensible en Python. L’intérêt d’un transfert au niveau de la fonction réside dans la possibilité de combiner les deux langages au sein du même processus, plutôt que d’isoler le composant dans un service indépendant et d’ajouter des communications réseau ainsi que de nouveaux coûts opérationnels.

Les tests comprennent des tests directs de Rust, les tests Python existants pour les gestionnaires Flask, des tests comparant les résultats des deux implémentations, ainsi qu’un test aléatoire des propriétés lorsque cela est pertinent. Toutefois, l’ajout de code natif entraîne des complications opérationnelles : les environnements de développement doivent disposer d’un compilateur Rust ou de binaires compatibles avec le système d’exploitation et l’architecture, le déploiement devient plus complexe et de nouvelles erreurs peuvent apparaître.

En pratique, cette méthodologie offre une voie relativement peu risquée pour améliorer certaines parties ciblées de systèmes existants, mais elle ne supprime pas la nécessité d’une analyse architecturale, de tests de compatibilité et de mesures globales. Les gains réels ne sont confirmés que lorsque l’amélioration se répercute sur l’ensemble du parcours du service, et non dans un benchmark isolé de la fonction.

Source de l’actualité
InfoQ - Architecture Articles
Ouvrir la source originale ↗
ف
Auteur

فريق تحرير certi.news

Dans la même catégorie

À lire également

Voir toutes les actualités