Vitesse

CPython, l’implémentation la plus couramment utilisée de Python, est lente pour les tâches liées au CPU (processeur). PyPy est rapide.

En utilisant une version légèrement modifiée du code de test lié au CPU de David Beazley’s (boucle ajoutée pour de multiples tests), vous pouvez voir la différence entre le traitement CPython et PyPy.

# PyPy
$ ./pypy -V
Python 2.7.1 (7773f8fc4223, Nov 18 2011, 18:47:10)
[PyPy 1.7.0 with GCC 4.4.3]
$ ./pypy measure2.py
0.0683999061584
0.0483210086823
0.0388588905334
0.0440690517426
0.0695300102234
# CPython
$ ./python -V
Python 2.7.1
$ ./python measure2.py
1.06774401665
1.45412397385
1.51485204697
1.54693889618
1.60109114647

Contexte

Le GIL

The GIL (Global Interpreter Lock) est comment Python permet à plusieurs threads de fonctionner en même temps. La gestion de la mémoire de Python est pas entièrement thread-safe, de sorte que le GIL est requis pour empêcher plusieurs threads d’exécuter le même code Python à la fois.

David Beazley a un bon guide sur la manière dont le GIL opère. I lcouvre aussi le new GIL dans Python 3.2. Ses résultats montrent que maximiser les performances dans une application Python nécessite une bonne compréhension de la GIL, comment il affecte votre application spécifique, combien de cœurs que vous avez, et où sont vos goulots d’étranglement dans l’application.

Extensions C

Le GIL

Une Special care doit être prise lors de l’écriture d’extensions C pour vous assurer que vous enregistrez vos threads avec l’interpréteur.

Extensions C

Cython

Cython implémente un sur-ensemble du langage Python avec lequel vous êtes en mesure d’écrire des modules C et C ++ pour Python. Cython vous permet également d’appeler des fonctions depuis des bibliothèques compilées C. Utiliser Cython vous permet de tirer avantage du typage fort des variables Python et des opérations.

Voici un exemple de typage fort avec Cython:

def primes(int kmax):
"""Calculation of prime numbers with additional
Cython keywords"""

    cdef int n, k, i
    cdef int p[1000]
    result = []
    if kmax > 1000:
        kmax = 1000
    k = 0
    n = 2
    while k < kmax:
        i = 0
        while i < k and n % p[i] != 0:
            i = i + 1
        if i == k:
            p[k] = n
            k = k + 1
            result.append(n)
        n = n + 1
    return result

L’implémentation d’un algorithme pour trouver des nombres premiers a quelques mots-clés supplémentaires par rapport à la suivante, qui est implémentée en pur Python:

def primes(kmax):
"""Calculation of prime numbers in standard Python syntax"""

    p = range(1000)
    result = []
    if kmax > 1000:
        kmax = 1000
    k = 0
    n = 2
    while k < kmax:
        i = 0
        while i < k and n % p[i] != 0:
            i = i + 1
        if i == k:
            p[k] = n
            k = k + 1
            result.append(n)
        n = n + 1
    return result

Notez que dans la version Cython, vous déclarez les entiers et les tableaux d’entiers qui seront compilés en types C, tout en créant aussi une liste Python:

def primes(int kmax):
    """Calculation of prime numbers with additional
    Cython keywords"""

    cdef int n, k, i
    cdef int p[1000]
    result = []
def primes(kmax):
    """Calculation of prime numbers in standard Python syntax"""

    p = range(1000)
    result = []

Quelle est la différence? Dans la version Cython ci-dessus, vous pouvez voir la déclaration des types de variables et le tableau entier d’une manière similaire à celle du standard C. Par exemple cdef int n,k,i dans la ligne 3. Cette déclaration de type supplémentaire (c’est à dire entier) permet au compilateur Cython de générer du code C plus efficace à partir de la deuxième version. Alors que le code standard de Python est sauvé dans des fichiers *.py, le code Cython est sauvé dans des fichiers *.pyx.

Quelle est la différence de vitesse? Essayons!

import time
#activate pyx compiler
import pyximport
pyximport.install()
#primes implemented with Cython
import primesCy
#primes implemented with Python
import primes

print "Cython:"
t1= time.time()
print primesCy.primes(500)
t2= time.time()
print "Cython time: %s" %(t2-t1)
print ""
print "Python"
t1= time.time()
print primes.primes(500)
t2= time.time()
print "Python time: %s" %(t2-t1)

Ces deux lignes nécessitent une remarque:

import pyximport
pyximport.install()

Le module pyximport vous permet d’importer les fichiers *.pyx (par exemple, primesCy.pyx) avec la version compilée par Cython de la fonction primes. La commande pyximport.install()`permet à l’interpréteur Python de démarrer directement le compilateur Cython pour générer le code C, qui est automatiquement compilé en une bibliothèque C :file:`*.so. Cython est alors capable d’importer cette bibliothèque pour vous dans votre code Python, facilement et efficacement. Avec la fonction time.time(), vous êtes en mesure de comparer le temps entre ces 2 différents appels pour trouver 500 nombres premiers. Sur un ordinateur portable standard (dual core AMD E-450 1,6 GHz), les valeurs mesurées sont:

Cython time: 0.0054 seconds

Python time: 0.0566 seconds

Et voici la sortie sur une machine ARM beaglebone intégreé:

Cython time: 0.0196 seconds

Python time: 0.3302 seconds

Pyrex

Shedskin?

Numba

À faire

Écrire à propos de Numba et du compilateur autojit pour NumPy

Concurrence

Concurrent.futures

Le module concurrent.futures est un module dans la bibliothèque standard qui fournit une “interface de haut-niveau pour exécuter des callables de manière asynchrone”. Il abstrait une grande partie des détails les plus compliqués sur l’utilisation de plusieurs threads ou processus pour la concurrence, et permet à l’utilisateur de se concentrer sur l’accomplissement de la tâche à accomplir.

Le module concurrent.futures expose deux classes principales, ThreadPoolExecutor et ProcessPoolExecutor. Le ThreadPoolExecutor va créer un pool de worker threads auquel un utilisateur peut soumettre des jobs à faire. Ces jobs seront ensuite exécutés dans un autre thread quand le prochain worker thread va devenir disponible.

Le ProcessPoolExecutor fonctionne de la même manière, sauf au lieu d’utiliser plusieurs threads pour ses workers, elle utilisera de multiples processus. Cela permet de mettre de côté le GIL, cependant à cause de la façon dont les choses sont passées à des processus workers, seuls les objets picklables peuvent être exécutés et retournés.

En raison de la manière dont le GIL fonctionne, une bonne règle de base est d’utiliser une ThreadPoolExecutor lorsque la tâche en cours d’exécution implique beaucoup de blocage (à savoir faire des requêtes sur le réseau) et d’utiliser un exécuteur ProcessPoolExecutor lorsque la tâche est informatiquement coûteuse.

Il existe deux principales manières d’exécuter des choses en parallèle en utilisant les deux exécuteurs. Une façon est avec la méthode map(func, iterables). Cela fonctionne presque exactement comme la fonction intégrée map(), sauf qu’il exécutera tout en parallèle. :

from concurrent.futures import ThreadPoolExecutor
import requests

def get_webpage(url):
    page = requests.get(url)
    return page

pool = ThreadPoolExecutor(max_workers=5)

my_urls = ['http://google.com/']*10  # Create a list of urls

for page in pool.map(get_webpage, my_urls):
    # Do something with the result
    print(page.text)

Pour encore plus de contrôle, la méthode submit(func, *args, **kwargs) programmera qu’un callable soit exécuté ( comme func(*args, **kwargs)) et retourne un objet Future qui représente l’exécution du callable.

L’objet Future fournit diverses méthodes qui peuvent être utilisées pour vérifier l’état d’avancement du callable programmé. Cela inclut:

cancel()

Tentative d’annulation de l’appel.

cancelled()

Retourne True si l’appel a été annulé avec succès.

running()

Retourne True si l’appel a été exécuté à ce moment et ne peut pas annulé.

done()

Retourne True si l’appel a été annulé avec succès ou a fini de s’exécuter.

result()

Retourne la valeur retournée par l’appel. Notez que cet appel sera bloquant jusqu’à le callable programmé soit retourné par défaut.

exception()

Retourne l’exception levée par l’appel. Si aucune exception n’a été levée alors cela retourne None. Notez que cela va bloquer tout comme result().

add_done_callback(fn)

Attache une fonction de callback qui sera exécutée (comme fn(future)) quand le callable prévu sera retourné.

from concurrent.futures import ProcessPoolExecutor, as_completed

def is_prime(n):
    if n % 2 == 0:
        return n, False

    sqrt_n = int(n**0.5)
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return n, False
    return n, True

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

futures = []
with ProcessPoolExecutor(max_workers=4) as pool:
    # Schedule the ProcessPoolExecutor to check if a number is prime
    # and add the returned Future to our list of futures
    for p in PRIMES:
        fut = pool.submit(is_prime, p)
        futures.append(fut)

# As the jobs are completed, print out the results
for number, result in as_completed(futures):
    if result:
        print("{} is prime".format(number))
    else:
        print("{} is not prime".format(number))

Le module concurrent.futures contient deux helpers pour travailler avec Futures. La fonction as_completed(futures) retourne un itérateur sur la liste des futures, en faisant un yield des futures jusqu’à ce qu’elles soient complètes.

La fonction wait(futures) va tout simplement bloquer jusqu’à ce que toutes les futures dans la liste des futures soient terminées.

Pour plus d’informations, sur l’utilisation du module concurrent.futures, consulter la documentation officielle.

Threading

La bibliothèque standard est livré avec un module threading qui permet à un utilisateur de travailler avec plusieurs threads manuellement.

Exécuter une fonction dans un autre thread est aussi simple que de passer un callable et ses arguments constructeur du Thread’ et d’appeler ensuite `start():

from threading import Thread
import requests

def get_webpage(url):
    page = requests.get(url)
    return page

some_thread = Thread(get_webpage, 'http://google.com/')
some_thread.start()

Pour attendre jusqu’à ce que le thread soit terminé, appelez join():

some_thread.join()

Après l’appel du join(), c’est toujours une bonne idée de vérifier si le thread est toujours en vie (parce que l’appel join a expiré):

if some_thread.is_alive():
    print("join() must have timed out.")
else:
    print("Our thread has terminated.")

Parce que plusieurs threads ont accès à la même section de la mémoire, parfois il peut y avoir des situations où deux ou plusieurs threads tentent d’écrire sur la même ressource en même temps ou lorsque la sortie est dépendante de la séquence ou du timing de certains événements. Ceci est appelé une data race ou “race condition”. Lorsque cela se produit, la sortie sera dénaturée ou vous pouvez rencontrer des problèmes qui sont difficiles à débuguer. Un bon exemple est ce stackoverflow post.

La façon dont cela peut être évité est d’utiliser un `Lock`_ que chaque thread aura besoin d’acquérir avant d’écrire dans une ressource partagée. Les locks peuvent être acquis et libérés soit par le protocole de contextmanager (déclaration with), ou en utilisant acquire() et release() directement. Voici un exemple (plutôt artificiel):

from threading import Lock, Thread

file_lock = Lock()

def log(msg):
    with file_lock:
        open('website_changes.log', 'w') as f:
            f.write(changes)

def monitor_website(some_website):
    """
    Monitor a website and then if there are any changes,
    log them to disk.
    """
    while True:
        changes = check_for_changes(some_website)
        if changes:
            log(changes)

websites = ['http://google.com/', ... ]
for website in websites:
    t = Thread(monitor_website, website)
    t.start()

Ici, nous avons un tas de threads vérifiant des changements sur une liste de sites et chaque fois qu’il y a des changements, ils tentent d’écrire ces modifications dans un fichier en appelant log(changes). Lorsque log() est appelé, il attendra d’acquérir le lock avec avec file_lock:. Cela garantit qu’à tout moment, seulement un seul thread est en train d’écrire dans le fichier.

Processus de spawning

Multiprocessing