Outils pour utilisateurs

Outils du site


exemple_cython_cpp

Exemple d'utilisation de Cython avec C++

Objectif

En utilisant C++, faire un exemple simple permettant de comparer la mise en oeuvre et les performances de:

  • une fonction codée en Python pur
  • une fonction codée en Cython pour être compilée en C++
  • idem, mais appelant la fonction codée en C++ directement

La fonction choisie pour l'exemple: le PGCD: plus grand commun diviseur de 2 nombres entiers.

Suggestion: si vous voulez construire les 3 exemples ci-dessous et en faire des comparaisons de performances, il faut placer chacun d'eux dans un répertoire séparé, par exemple pgcd1, pgcd2 et pgcd3, et placer un fichier “__init__.py” vide (signature du “package” Python) à la racine de chacun d'eux:

essaicython (répertoire)
    test.py (code de test des 3 exemples)
    
    pgcd1 (répertoire)
    __init__.py
    ... (code de l'exemple 1)

    pgcd2 (répertoire)
    __init__.py
    ... (code de l'exemple 2)

    pgcd3 (répertoire)
    __init__.py
    ... (code de l'exemple 3)

Cela vous permettra de tester les 3 exemples dans un programme test, dans lequel vous pourrez importer les 3 pgcd comme suit:

from pgcd1.pgcd1 import pgcd1
from pgcd2.pgcd2 import pgcd2
from pgcd3.pgcd3 import pgcd3

Attention sous Windows: choix du compilateur

Python prendra le compilateur par défaut. Si vous avez Visual Studio de Microsoft (comme moi), c'est lui qui sera utilisé (ça fonctionne très bien!). Mais si vous voulez MinGW au lieu de Visual Studio, il y a 2 choses à faire:

1- créer un fichier texte “setup.cfg” placé au même niveau que setup.py, et contenant:

[build]
compiler = mingw32

2- corriger le fichier C:\Python27\Lib\distutils\cygwinccompiler.py pour retirer toute mention de l'option “-mno-cygwin” si elle génère une erreur.

Moyennant quoi, chez moi, ça marche avec les 2 compilateurs.

Fonction codée en Python pur

Module pgcd1.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
def pgcd1(a,b):
    """Plus Grand Commun Diviseur entre les 2 nombres entiers a et b"""
    while b<>0:
        a,b=b,a%b
    return a

Il est difficile de faire plus simple!

Fonction codée en Cython pour être compilée en C++

Fonction en Cython pgcd2.pyx:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
# definition des types Python
from cpython cimport bool, list, dict, tuple, long as llong
 
# definition des type d'objets
from cython import cclass, ccall, cfunc, returns as creturns, locals as clocals
 
#############################################################################
@ccall
@creturns(int)
@clocals(a=int, b=int)
def pgcd2(a,b):
    """Plus Grand Commun Diviseur entre les 2 nombres entiers a et b"""
    while b<>0:
        a,b=b,a%b
    return a

A noter que j'utilise une syntaxe peu utilisée mais très pratique: toutes les définitions nécessaires à Cython pour compiler sont ajoutées sous forme de décorateurs. Cela permet de garder quasi intactes les fonctions en pur Python.

Pour le traitement Cython, on fabrique le setup.py suivant:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
 
ext_modules = [Extension("pgcd2",
                         sources=["pgcd2.pyx"],
                         include_dirs=["."],
                         language="c++"
                        )
              ]
 
setup(
  name = 'PGCD2',
  ext_modules = ext_modules,
  cmdclass = {'build_ext': build_ext},
)

Vous voyez que c'est dans le setup qu'on désigne le C++ comme choix du compilateur.

Il ne reste plus qu'à exécuter le traitement par (à exécuter dans le répertoire du setup):

python setup.py build_ext --inplace

Fonction codée en Cython qui appelle une fonction écrite en C++

Petit complément par rapport au cas précédent: la fonction pgcd écrite pour Cython va en fait appeler une fonction pgcd écrite en C++ et renvoyer sa valeur. Il s'agit d'un simple exemple qui permettra au code Cython de faire appel à du code C++ directement.

Voilà déjà le code C++ choisi à titre d'exemple:

bib.cpp:

#include "bib.h"
 
int pgcd_(int a, int b){
    int r;
    while (b!=0) {
        r = a%b;
        a = b;
        b = r;
        }
    return a;
    }    

Et l'en-tête bib.h:

int pgcd_(int a, int b);

Voici le code destiné à Cython pgcd3.pyx:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
# definition des types Python
from cpython cimport bool, list, dict, tuple, long as llong
 
# definition des type d'objets
from cython import cclass, ccall, cfunc, returns as creturns, locals as clocals
 
# reference a un objet defini en C++
cdef extern from "bib.h": 
    cdef int pgcd_(int a, int b)
 
#############################################################################
@ccall
@creturns(int)
@clocals(a=int, b=int)
def pgcd3(a, b):
    """PGCD de a et de b"""
    return pgcd_(a,b)

Et voici le setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
 
ext_modules = [Extension("pgcd3",
                         sources=["pgcd3.pyx", "bib.cpp"],
                         include_dirs=["."],
                         language="c++"
                        )
              ]
 
setup(
  name = 'PGCD3',
  ext_modules = ext_modules,
  cmdclass = {'build_ext': build_ext},
)

Vous voyez que le fichier en code C++ est dans la liste des sources.

Pour exécuter la compilation, on fera dans la console et dans le répertoire du setup:

python setup.py build_ext --inplace

Utilisation et test de performance

Voilà le code qui permettra de tester les 3 exemples:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
from pgcd1.pgcd1 import pgcd1
from pgcd2.pgcd2 import pgcd2
from pgcd3.pgcd3 import pgcd3
 
from timeit import default_timer 
from random import randint
 
n = 1000000
a = randint(10,10000)
b = randint(10,10000)
 
t1 = default_timer()
for c in xrange(0, n):
    pgcd1(a, b)
t1 = default_timer()-t1
print t1
 
t2 = default_timer()
for c in xrange(0, n):
    pgcd2(a, b)
t2 = default_timer()-t2
print t2
 
t3 = default_timer()
for c in xrange(0, n):
    pgcd3(a, b)
t3 = default_timer()-t3
print t3
print t2/t1, t3/t2

Exemple d'affichage:

1.51095759895
0.330764389199
0.295863063216
0.218910437612 0.894482818819

Constatations:

  • le temps d'exécution de la fonction pgcd est divisé par 4 à 5 quand on prend le code Cython plutôt que le code en Python pur.
  • Lorsque Cython appelle directement du code C++, on gagne encore 10% environ. Ce qui peut être intéressant si on dispose déjà du code C++, mais pas assez s'il faut l'écrire.

Conclusion: la solution “Cython” est vraiment intéressante!

Attention cependant: lorsqu'on utilise dans Cython des variables de même type que C++, c'est très rapide, mais si on veut utiliser des variables Python qui n'existent pas en C++, ça se gâte un peu, parce qu'on oblige le C++ à faire de nombreux appels à l'API Python. A la limite, on peut obtenir un temps d'exécution plus long qu'en pur Python (ça m'est déjà arrivé)!

Amusez-vous bien!

exemple_cython_cpp.txt · Dernière modification: 2013/10/18 18:20 par tyrtamos

Outils de la page