Introduction à Cython

Gabor Laszlo Hajba

Traducteur

mathv

Dans ce tutoriel, je vous donnerai une brève introduction concernant Cython, une bibliothèque Python qui permet une interaction avec du code écrit en C. Et peu importe que vous utilisiez du code C depuis Python ou que vous appeliez du code Python depuis le C.

Cela semble magique non ? Et cela n’est pas si compliqué que ça en a l’air. Nous allons creuser un peu et voir ce qu’il est possible de faire.

Commentez

- Titre : Introduction à Cython

- Auteur : Gabor Laszlo Hajba

- Traducteur : mathv

- Parution : 26 mars 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Gabor Laszlo Hajba. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Brève Introduction

- II. Un simple exemple

- III. Fonctionnalités de Cython

- IV. Conclusion

I - Brève Introduction

Cython est une bibliothèque, ce n’est pas la même que CPython, l’implémentation de référence du langage de programmation Python. Elle convertit du code Python en C qui peut être joint à l’exécution du runtime CPython.

Pour installer la bibliothèque, utilisez la commande suivante :

pip install cython

L’idée derrière Cython est de gagner en rapidité avec Python en le convertissant en code C/C++ et d’intégrer du code C/C++ directement au code Python existant. L’optimisation du code Python devrait se produire, car Python est un langage interprété et cela entraîne une surcharge d’interprétation et les appels de fonctions coûtent plus de temps. Une autre raison peut être le GIL (Global Interpreter Lock) qui réduit l’exécution des threads à 1 en CPython, ce qui signifie que les tâches demandant des ressources CPU ne bénéficient pas du multitâche (pour plus d’informations sur ce sujet : http://www.discoversdk.com/blog/parallel-processing-in-python).

L’inconvénient avec l’utilisation de Cython est que vous devez savoir coder en C bien que le langage lui-même est un surensemble de Python.

Petite note de ce que j’apprécie vraiment à propos de Cython, c’est qu’à la compilation de code, la génération d’erreur fournit un fichier .c avec un contenu similaire à : #erreur. N’utilisez pas ce fichier, il est le résultat d’erreur de compilation Cython.

II - Un simple exemple

Après cette introduction, il est temps de vous montrer un simple exemple. Je dois vous dire avant de commencer que je ne fournis qu’un exemple basique pour vous donner un aperçu de comment le code Cython est écrit et comment il fonctionne.

Voici le code que nous allons convertir avec Cython dans le prochain bout de code :

1. __author__ = 'GHajba'

3. def factors(n, result):

4. if n <= 1:

5. return result

6. for i in range(2, n + 1):

7. if n % i == 0:

8. result.append(i)

9. return factors(n // i, result)

11. def main():

12. numbers = range(1, 50001)

14. for n in numbers:

15. factors(n, [1])

17. if __name__ == '__main__':

18. main()

Comme vous pouvez le constater ci-dessus, ce code est trivial et peut être optimisé : la fonction factors calcule le facteur d’un nombre donné, la totalité du code calcule le facteur des nombres de 1 à 50 000 inclus.

J’ai sauvegardé ce code dans le fichier factorizer.pyx. Il s’agit d’un code Python 3 valide, vous pouvez donc l’essayer et mesurer son temps exécution avec Python 3 :

1. GHajba$ time python3.5 factorizer.pyx

3. real    0m26.515s

4. user    0m26.369s

5. sys     0m0.073s

Comme vous pouvez le constater, cela prend un peu plus de 26 secondes pour exécuter ce bout de code.

Il est maintenant temps de le compiler avec Cython :

cythonize -b -i factorizer.pyx

L’option -b construit les modules d’extension que vous pouvez charger dans l’interpréteur Python lors de son exécution.

L’option -i construit le module sur place.

Cela implique que l’option -b soit présente. Si vous omettez l’option -i, vous vous retrouverez avec une structure de répertoire comme à l’utilisation de distutils.

Après ceci, nous allons importer le code généré à Python et voir comment cela fonctionne :

1. GHajba$ time python3.5 -c "import factorizer;factorizer.main()"

3. real    0m17.192s

4. user    0m17.154s

5. sys     0m0.019s

Comme vous pouvez le constater, le code s’exécute plus rapidement après sa compilation avec Cython. Nous avons éliminé la surcharge d’interprétation mentionnée précédemment. L’inconvénient est la taille du code :

GHajba$ wc -l factorizer.pyx
18 factorizer.pyx
GHajba$ wc -l factorizer.c
3384 factorizer.c

Le code généré en C a « quelques » lignes de plus. Si vous y regardez d’un peu plus près, c’est un peu déconcertant sauf si vous êtes un développeur C confirmé.

Il contient beaucoup de #défine pour la portabilité, des commentaires utiles avec des extraits de code Python pour comprendre quelle partie de votre code a généré du code C.

En somme, il s’agit de beaucoup de code que n’avez pas envie d’écrire vous-même.

III - Fonctionnalités de Cython

L’exemple ci-dessus était vraiment basique : nous n’y avons utilisé aucune fonctionnalité de Cython.

Réécrivons l’application pour voir si nous pouvons gagner un peu plus en vitesse. Pour cela, nous allons modifier un peu notre code en utilisant les définitions de C pour laisser le compilateur tirer meilleur parti de notre code.

La première partie de code vous montre le code en pur Python et nous le réécrivons en un code valide pour Cython :

1. __author__ = 'GHajba'

3. def factors(n, counter, result):

4. if n <= 1:

5. return result

6. for i in range(2, n + 1):

7. if n % i == 0:

8. result[counter] = i

9. return factors(n // i, counter + 1, result)

11. def main():

12. max_value = 50000

14. for n in range(1, max_value+1):

15. f = [0] * max_value

16. factors(n, 0, f)

18. if __name__ == '__main__':

19. main()

Ce bloc de code utilise les tableaux comme vous pouvez vous y attendre en C ou Java : en utilisant leur index. Naturellement, ce n’est pas économiseur de mémoire en Python parce que chaque fois dans la boucle for nous réallouons tout le tableau f. Si vous exécutez ce code avec Python 3.5, il prendra +/- 37 secondes pour s’exécuter. Nous perdons un peu en performance.

Après avoir converti et exécuté ce code en Cython, l’exécution tourne autour de 27 secondes, ce qui signifie une modification du temps d’exécution en code C, plus rapide que le Python pur.

Mais comme mentionné précédemment, nous allons convertir ce code pour être encore plus efficace.

Voici la première approche :

1. import array

3. __author__ = 'GHajba'

5. def factors(n, counter, result):

6. if n <= 1:

7. return result

8. for i in range(2, n + 1):

9. if n % i == 0:

10. result[counter] = i

11. return factors(n // i, counter + 1, result)

13. def main():

14. cdef int max_value = 50000

15. cdef object a

16. cdef int[:] f

18. cdef int n

19. for n in range(1, max_value+1):

20. a = array.array('i', [0])*max_value

21. f = a

22. factors(n, 0, a)

24. if __name__ == '__main__':

25. main()

Le code ci-dessus ressemble presque à l’identique à sa version en Python, mais nous avons défini de manière concrète le type de la variable max_value, créé l’espace mémoire du tableau qui nous permet de générer du code qui accédera directement aux données de celui-ci.

Comme vous pouvez le constater, j’ai laissé la fonction factors inchangée.

« Cythonisons » ce code et exécutons-le :

GHajba$ time python3.5 -c "import factorizer_cython;factorizer_cython.main()"

real    0m17.504s
user    0m17.439s
sys     0m0.036s

Bien, nous avons atteint le temps que nous avions précédemment avec du code plus orienté Python. Je peux comprendre que vous soyez quelque peu déçu parce que vous vous attendiez à un réel gain de temps. Mais ne perdons pas de vue qu’il nous reste une fonction à changer.

Ajoutons quelques définitions de type et regardons le résultat :

1. def factors(int n, counter, result):

2. if n <= 1:

3. return result

4. cdef int i

5. for i in range(2, n + 1):

6. if n % i == 0:

7. result[counter] = i

8. return factors(n // i, counter + 1, result)

À la première étape, j’ai défini la variable i comme un entier (int). Cela n’apporte pas un gros changement, j’ai donc ajouté quelques définitions de type aux arguments de la fonction également.

Comme vous pouvez le deviner, cela ne fait pas encore l’affaire.

Le bout de code ci-dessus a une définition de type pour les variables i et n uniquement. Si je le convertis et l’exécute, j’obtiens les résultats suivants :

GHajba$ time python3.5 -c "import factorizer_cython;factorizer_cython.main()"

real    0m1.268s
user    0m1.241s
sys     0m0.019s

Et voilà le gain de performance que nous attendions ! Factoriser 50 000 nombres en moins de 2 secondes.

IV - Conclusion

Comme nous avons pu le voir, Cython est principalement utilisé pour optimiser du code Python. Toutefois, ne vous lancez pas à corps perdu à apprendre le C et à réécrire tous vos programmes Python ! Premièrement, si votre application est vraiment lente, commencez par créer une version alternative en Cython et si le résultat est suffisamment rapide, modifiez votre code d’origine. Vous pouvez également identifier les petites parties lentes de votre code et les optimiser/réécrire.

Et pour terminer, quelques fois il est suffisant de « cythoniser » le code et d’utiliser le module d’extension compilé pour obtenir un gain de performance. Il est parfois nécessaire également de réécrire le code en Cython qui permet l’optimisation pour le compilateur C comme l’ajout de typage de variables.