Python et sa lenteur légendaire, comment y remédier ?

10 Octobre 2019
Image d'un paysage en 3D sortant de l'écran d'un smartphone
Python est un langage lent. C’est un fait. Et pourtant, il reste très utilisé pour des projets de grande envergure. Découvrons ensemble pourquoi…

Le site de partage de fichiers en ligne « Dropbox », le comparateur de fichiers installé en standard sous Linux « Meld », l’outil de gestion de sources distribuées « Mercurial » ou encore le programme d’art et d‘animation « Blender » ne sont que quelques exemples parmi tant d’autres de produits développés en Python. Et pourtant, Python est un langage dédaigné pour sa lenteur…

En effet, selon le site « The computer Language Benchmarks Game », qui réalise des comparaisons exhaustives sur les performances des différents langages informatiques, Python3 est souvent plusieurs dizaines de fois plus lent que C++ sur les principaux cas de test :

Comparaisons des performances différents logiciels de programmation en

Toutefois, il existe diverses manières de rendre du code Python plus rapide. La plus connue étant d’utiliser la bibliothèque externe au langage « Numpy » qui traite les matrices de manière très efficace et permet à Python de se placer en alternative viable au très coûteux écosystème « Matlab ». Cette méthode ne s’applique cependant que pour du calcul matriciel.

Mais alors, comment optimiser du Python ?  Voici quelques méthodes réellement utilisées par l’auteur, la dernière étant sa préférée.

 

Option 1 : Recourir à PyPy

Il suffit d’installer PyPy et de lancer son programme tout en l’utilisant en lieu et place de l’interpréteur standard. PyPy incorpore un compilateur « Just in Time », c’est-à-dire à la volée, juste avant l’exécution.

AUSY recommande cette solution pour sa facilité de mise en œuvre. Cependant, le gain est quasi nul sur les exemples précédemment utilisés.

 

Option 2 : Recourir à Cython

Cela consiste à réécrire des parties du code dans un langage spécifique plus proche du langage C. On obtient une sorte d’hybride entre les deux langages. Bien entendu, il faut installer Cython et fournir un effort de compréhension afin d’apprendre à l’utiliser.

Tableau Comparatif Python et Cython

Il est possible, pour un script simple (un seul fichier), d’obtenir un résultat sans modifier le script.

Cython semble difficile à mettre en œuvre. En effet, le rédacteur de cet article constate un taux de réussite faible (uniquement sur un programme avec un seul fichier). Cette peine de réalisation n’est pas à la hauteur du gain, qui reste léger sur les exemples testés.

 

Option : Récrire des fonctions en C

Python est un langage extensible. Cela signifie qu’il peut appeler des modules dans des langages de plus bas niveau, tel que C.

Après avoir identifié la fonction qui pose problème, il suffit de la réécrire en langage C ou C++. Attention, un petit travail d’adaptation est à réaliser pour passer les paramètres et les récupérer.

Place à un exemple pour illustrer cette troisième solution :

Tableau Comparatif Python original et Python modifié

On aura bien entendu pris soin de compiler la fonction « make_lum » dans un fichier « make_lum.so » et ce, par deux commandes de compilation et édition de liens du type :

                gcc -c -O3 -Wall  -fPIC  -o  make_lum.o  make_lum.c

                gcc -O3 -shared  -Wl,-soname, make_lum.o -o  make_lum.o  make_lum.so

Cette option est bien meilleure que les précédentes puisqu’elle est assez facile à mettre en œuvre et que le gain est conséquent (à condition que la situation s’y prête).

 

Option 4 : utiliser le parallélisme

Cette méthode est proposée de manière native par la langage Go (parallélisme de thread).

Elle consiste à isoler la partie critique en une liste de données à traiter, puis à répartir sur différents petits « process » le travail en parallèle. En théorie, le gain peut aller jusqu’à un facteur égal au nombre de processeurs de la machine.

Place à un petit exemple concret simplifié d’après un programme développé par notre consultant, dans lequel il faut placer des chenilles dans une grille :

Tableau Chenille grille

Pour simplifier les codes, des fonctions ont été omises. La fonction « try caterpillar » évalue l’intérêt d’un choix de chenille à placer sur une grille et la fonction « best one » agrège ces résultats pour sélectionner la meilleure.

A noter : Python ne permet pas de gagner du temps sur du parallélisme « de thread » pour des raisons d’implémentation et ce, à cause du GIL « Global Interpreter Lock » qui empêche plusieurs threads de fonctionner simultanément.

Pour rappel, le parallélisme de thread est un parallélisme plus léger dans lequel les données sont partagées. Le parallélisme de process est quant à lui un parallélisme plus lourd dans lequel rien n’est partagé.

C’est ce dernier que nous avons utilisé dans notre exemple. Le premier permet toutefois d’optimiser un script Python à condition que la ressource ne soit pas le calcul mais plutôt (par exemple) des entrées sorties.

Cette solution est assez longue à mettre en œuvre mais permet un gain qui peut être impressionnant. Ce résultat est visible toutefois à condition que l’on puisse décomposer le programme de manière appropriée et investir dans un PC avec beaucoup de cœurs et de processeurs.

 

Last but not least, Option 5 : Optimiser le programme lui-même

Afin d’optimiser le programme en lui-même, on utilise le « profiling ». Le profiling consiste donc dans un premier temps à déterminer les fonctions les plus coûteuses, puis dans un second temps, si besoin est, les lignes de code les plus coûteuses. Il reste ensuite à modifier les programmes pour réduire les temps de calcul. Un des aspects les plus intéressants et les plus créatifs de la programmation…

Profiler par fonction est très facile en Python : il suffit d’ajouter quelques lignes de code à son programme.

Place à l’exemple :

Tableau Option 5 Python
On obtient ensuite ce genre de listing qui indique les fonctions les plus coûteuses en temps :

Tableau 5

On voit que le goulot d’étranglement est la fonction « install caterpillar() », définie en ligne 411 du script. On pourrait être amené à détailler ligne par ligne le code de cette fonction.

A noter : l’approche décrite n’est pas spécifique à Python, elle est juste une déclinaison de l’approche traditionnelle à base de gprof du monde Linux pour les programmes C/C++. Bien que s’adaptant d’une autre approche, cette solution fonctionne parfaitement pour Python. 

Le profiling est un exercice intellectuel très intéressant, permettant un gain variable. On pourra être surpris d’un gain attendu qui ne se produit malheureusement pas, et inversement.

Profiler par ligne de code est également très aisé mais nécessite un outil non fourni dans le standard du Python : « line_profiler ».

Et vous, quelle est votre option favorite ?

 

Jérémie Lefrançois
Jérémie Lefrançois, consultant AUSY depuis 2004, a écrit ses premiers programmes en Basic sur ZX Spectrum en 1982. Diplômé d'un DESS en Informatique en 1989, il a touché à de nombreux langages informatiques à titre personnel ou professionnel. Il aime explorer et comparer les possibilités des différents langages et plus spécifiquement approfondir Python qui est sa prédilection depuis 2014. C'est donc avec un grand intérêt qu'il surveille les évolutions de ce langage dynamique dont il apprécie la facilité de mise en œuvre.

 

Aussi n’hésitez pas à visiter notre offre Big Data.

Parlons ensemble de vos projets.

contact