Contexte

Je suis Leader Technique et mon équipe est en charge de l’évolution ainsi que de la maintenance de distributeurs automatiques de billets. Nous travaillons sur des automates conçus en 2004 et 2007, qui ont aujourd’hui des ressources limitées. Le contexte projet fait que ces automates ont une combinatoire de vente très importante. Au fur et à mesure des évolutions, la combinatoire des produits vendables est devenue de plus en plus complexe : les temps de calcul sur le plus vieux des automates étaient critiques. J’avais déjà optimisé l’algorithme  en charge de tester la gamme tarifaire associée à une carte de transport. Cette optimisation avait permis de continuer à offrir au client une expérience acceptable. Quand un usager dépose sa carte de transport sur la machine, toute la gamme tarifaire compatible avec cette carte est vérifiée. Cela implique de nombreuses règles métier à contrôler, et ce par une bibliothèque partagée. Dans certains cas extrêmes, cette bibliothèque peut être appelée plus de 60 fois. Chaque vérification prenant en moyenne 500 millisecondes, nous dépassions les 30 secondes de calcul : une expérience utilisateur bien trop longue et inacceptable.

Contraintes

Cette bibliothèque étant utilisée par plusieurs appareils aux configurations hétérogènes, il fallait que la solution retenue fonctionne sur ces différentes plateformes. Les contraintes étaient les suivantes :
  • – Compatible python 2.4 32 bits sous GNU Linux Debian 4 ;
  • – Compatible python 2.7 64 bits sous GNU Linux Debian 9 ;
  • – Compatible python 2.4 32 bits sous Windows NT4 ;
  • – Compatible avec les tests fonctionnels automatiques en place.
Des compilations à la volée (JIT) avaient déjà été tentées dans le passé sous Debian 4. Les résultats n’avaient pas été probants et le projet a été abandonné. Nous avons donc directement éliminé ces solutions. Pour garder les tests fonctionnels opérationnels, nous avons regardé du côté des « compilateurs Python ». Après recherche, Cython nous paraissait être la meilleure solution dans notre cas de figure. Après un premier test de compilation de la bibliothèque avec Cython sans aucune modification, nous avions un gain de performance de 15%.

Réalisation

Afin de réaliser ces modifications, nous avons utilisé la version 0.20 de Cython, dernière version compatible avec Python 2.4. La transformation du code Python de la bibliothèque en code Cython a alors pu démarrer. Cette transformation consiste principalement à typer le code Python pour permettre à Cython d’optimiser la compilation. Idéalement, nous aurions aimé utiliser le « Pure Python Mode » ou les « Magic Attributes » de Cython, mais ces modes ne sont pas disponibles dans la version 0,20. Ils nous auraient permis de passer facilement et progressivement le code Python en code Cython :
  • – Le « Pure Python Mode » permet de ne pas modifier le code python en ajoutant des fichiers .pxd au projet. Ces fichiers décrivent le typage à appliquer au code python, ils sont l’équivalent des .h en C;
  • – Les « Magic Attributes » sont des décorateurs à ajouter au code python, qui sont utilisés par Cython pour la compilation et ignorés par Python.
N’ayant pas les fonctionnalités de transition simplifiée, nous avons directement typé tout le code en utilisant la syntaxe de Cython. Cython a « trois niveaux » d’optimisation possible :
  • 1. Compiler du code Python pur. Cela fait gagner le temps JIT de l’interpréteur cpython ;
  • 2. Typage et compilation de méthodes/fonctions avec visibilité dans Python ;
  • 3. Typage et compilation de méthodes/fonctions internes au code Cython, donc non appelables par Python.
Le premier niveau d’optimisation nous a fait gagner 15% de performance; Pour optimiser au maximum au niveau 3, nous avons répertorié la liste des méthodes appelables par Python et qui auront donc une optimisation de niveau 2. Cette étape de transformation s’est globalement bien déroulée. Nous n’avons rencontré aucune difficulté particulière, le projet compilait correctement.

Intégration

Nous sommes alors passés à l’étape suivante : l’intégration. Pour ce faire, nous avons lancé les tests fonctionnels. Cette phase a pris plus de temps que prévu. En effet, au premier lancement, aucun test ne fonctionnait. Nous avons donc corrigé nos erreurs et après plusieurs ajustements, les tests sont enfin passés avec succès. Ils ont permis de corriger de nombreuses erreurs faites pendant la transformation : mauvaise utilisation de Cython, mauvais typage, mauvaise adaptation algorithmique, … Les tests unitaires nous ont causé beaucoup de soucis. Étant en Python, ils étaient très permissifs dans le type des objets utilisés pour tester et des objets « Mock » ont été massivement utilisés. Comme nous n’avons pas pu utiliser les modes« Pure Python Mode » ou les« Magic Attributes », deux solutions se présentaient à nous :
  • – Réécrire l’intégralité des tests unitaires ;
  • – Coder un outil qui adaptera le code Cython uniquement pour les tests unitaires.
Il n’était pas envisageable de réécrire les tests, parce qu’ils étaient là pour nous garantir le fonctionnement à l’équivalent, sans régression, de la bibliothèque. Nous avons alors créé un script modifiant le code Cython et transformant la déclaration de toutes les fonctions et méthodes, pour les rendre compatibles avec les tests unitaires. Le code ainsi modifié a été utilisé uniquement pour l’exécution des tests unitaires. Ce script n’a pas altéré les algorithmiques, juste la déclaration des méthodes/fonctions. Concrètement, le script a modifié toutes les déclarations de fonctions/méthodes pour qu’elles soient exposées à l’interpréteur python avec un typage compatible pour les cas posant un problème. Tous les tests ont été concluants, test fonctionnel et unitaire. Nous pouvions alors passer à l’intégration de la bibliothèque dans un logiciel d’une machine. Malheureusement, le premier lancement s’est conclu par un plantage de la machine. Certains mécanismes, habituellement implémentés par défaut dans l’interpreteur Cpython, ne le sont pas dans Cython. C’est le cas de la copie récursive des objets, mécanisme justement utilisé par le code des machines. Les tests fonctionnels, cette fois de l’automate, ont permis d’identifier tous ces soucis de compatibilité et de finaliser le portage de la bibliothèque. Une fois les tests réussis (test fonctionnel/unitaire de la bibliothèque et test fonctionnel des automates), il nous restait un dernier essai : les tests sur l’appareil sous Windows NT4. Notre plus gros souci fut de compiler pour Python 2.4 sous Windows NT4. Nous devions passer obligatoire par Windows XP. Grâce à une vieille machine sous Windows XP que nous avions sous la main, nous avons pu compiler la bibliothèque et tester sur machine le résultat. Cependant, pour la mise en place de l’intégration continue, nous ne pouvions pas utiliser ce système d’exploitation. Après quelques essais, nous avons réussi à compiler la bibliothèque avec WINE sous GNU Linux Debian. Le binaire, ainsi créé, était fonctionnel sur la machine cible.

Résultats

Finalement, avec juste un typage du code existant, la performance est au minimum deux fois plus rapide. Dans un test « lourd », le temps d’attente du client est passé de 22 secondes à 9 secondes comme l’attestent les graphiques ci-dessous (5 répétitions du même test) :
Utilisation du CPU avant optimisation
Utilisation du CPU après optimisation
  Il est encore possible d’améliorer la performance brute, en faisant, par exemple, moins d’appels à des modules Python pures dans la bibliothèque.

Conclusion

Le passage a Cython a répondu à notre besoin d’amélioration des performances avec un temps de calcul réduit d’un facteur d’environ 2. Les tests automatisés nous ont permis de faire cette transition sereinement et sans accroc sur la mise en production. Ces tests nous ont vraiment aidés et sécurisés pour la réalisation de cette migration. Une grosse évolution fonctionnelle, nous a permis de revoir une partie de l’algorithme pour être plus « Cython ». Nous avons maintenant un facteur 4 fois plus rapide en moyenne. Nous avons réutilisé Cython pour d’autres projets, mais dans un cadre d’interfaçage de Python avec des bibliothèques C externe. Article rédigé par Romain, Leader Technique.