Optimisation de code Python avec ctypes

Une traduction de l'article a été préparée spécialement pour les étudiants du cours de développeur Python .




Remarque : le code de cet article est sous licence GNU AGPLv3 .

J'ai écrit ce guide parce que je n'ai pas pu trouver celui qui combine toutes les choses utiles sur les ctypes. J'espère que cet article rend la vie de quelqu'un beaucoup plus facile.

Contenu:

  1. Optimisations de base
  2. modes
  3. Compilation Python
  4. Structures en Python
  5. Appelez votre code en C
  6. Pypy

Optimisations de base


Avant de réécrire le code source Python en C, considérez les méthodes d'optimisation de base en Python.

Structures de données intégrées


Les structures de données intégrées de Python, telles que set et dict, sont écrites en C. Elles fonctionnent beaucoup plus rapidement que vos propres structures de données écrites en tant que classes Python. D'autres structures de données en plus de l'ensemble standard, dict, list et tuple sont décrites dans la documentation du module collections .

Répertorier les expressions


Au lieu d'ajouter des éléments à la liste à l'aide de la méthode standard, utilisez des expressions de liste.

# Slow
      mapped = []
      for value in originallist:
          mapped.append(myfunc(value))
      
      # Faster
      mapped = [myfunc(value) in originallist]

ctypes


Le module ctypes vous permet d'interagir avec le code C de Python sans utiliser de module subprocessou autre module similaire pour démarrer d'autres processus à partir de la CLI.

Il n'y a que deux parties: compiler du code C pour charger en qualité shared objectet configurer des structures de données en code Python pour les mapper aux types C.

Dans cet article, je connecterai mon code Python avec lcs.c , qui trouve la sous-séquence la plus longue en deux listes de chaînes. Je veux que les éléments suivants fonctionnent en Python:

list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!']
      list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.']
      
      common = lcs(list1, list2)
      
      print(common)
      # ['My', 'name', 'is', 'Stevens']

Un problème est que cette fonction C particulière est la signature d'une fonction qui prend des listes de chaînes comme types d'arguments et retourne un type qui n'a pas une longueur fixe. Je résous ce problème avec une structure de séquence contenant des pointeurs et une longueur.

Compiler du code C en Python


Tout d'abord, le code source C ( lcs.c ) est compilé lcs.sopour se charger en Python.

gcc -c -Wall -Werror -fpic -O3 lcs.c -o lcs.o
      gcc -shared -o lcs.so lcs.o

  • - Le mur affichera tous les avertissements;
  • - Werror encapsulera tous les avertissements dans des erreurs;
  • - fpic générera des instructions indépendantes de la position dont vous aurez besoin si vous souhaitez utiliser cette bibliothèque en Python;
  • - O3 maximise l'optimisation;

Et maintenant, nous allons commencer à écrire du code Python en utilisant le fichier objet partagé résultant .

Structures en Python


Voici deux structures de données utilisées dans mon code C.

struct Sequence
      {
          char **items;
          int length;
      };
      
      struct Cell
      {
          int index;
          int length;
          struct Cell *prev;
      };

Et voici la traduction de ces structures en Python.

import ctypes
      class SEQUENCE(ctypes.Structure):
          _fields_ = [('items', ctypes.POINTER(ctypes.c_char_p)),
                      ('length', ctypes.c_int)]
      
      class CELL(ctypes.Structure):
          pass
      
      CELL._fields_ = [('index', ctypes.c_int), ('length', ctypes.c_int),
                       ('prev', ctypes.POINTER(CELL))]

Quelques notes:

  • Toutes les structures sont des classes qui héritent de ctypes.Structure.
  • Le seul champ _fields_est une liste de tuples. Chaque tuple est ( <variable-name>, <ctypes.TYPE>).
  • Il ctypesexiste des types similaires dans c_char (char) et c_char_p (* char) .
  • Il y en a ctypeségalement un POINTER()qui crée un pointeur de type à partir de chaque type qui lui est transmis.
  • Si vous avez une définition récursive comme dans CELL, vous devez passer la déclaration initiale, puis ajouter les champs _fields_afin d'obtenir un lien vers vous plus tard.
  • Comme je n'ai pas utilisé CELLPython dans mon code, je n'ai pas eu besoin d'écrire cette structure, mais elle a une propriété intéressante dans le domaine récursif.

Appelez votre code en C


De plus, j'avais besoin de code pour convertir les types Python en nouvelles structures C. Maintenant, vous pouvez utiliser votre nouvelle fonction C pour accélérer le code Python.

def list_to_SEQUENCE(strlist: List[str]) -> SEQUENCE:
          bytelist = [bytes(s, 'utf-8') for s in strlist]
          arr = (ctypes.c_char_p * len(bytelist))()
          arr[:] = bytelist
          return SEQUENCE(arr, len(bytelist))
      
      
      def lcs(s1: List[str], s2: List[str]) -> List[str]:
          seq1 = list_to_SEQUENCE(s1)
          seq2 = list_to_SEQUENCE(s2)
      
          # struct Sequence *lcs(struct Sequence *s1, struct Sequence *s2)
          common = lcsmodule.lcs(ctypes.byref(seq1), ctypes.byref(seq2))[0]
      
          ret = []
      
          for i in range(common.length):
              ret.append(common.items[i].decode('utf-8'))
          lcsmodule.freeSequence(common)
      
          return ret
      
      lcsmodule = ctypes.cdll.LoadLibrary('lcsmodule/lcs.so')
      lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE)
      
      list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!']
      list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.']
      
      common = lcs(list1, list2)
      
      print(common)
      # ['My', 'name', 'is', 'Stevens']

Quelques notes:

  • **char (liste de chaînes) correspond directement à une liste d'octets en Python.
  • Il lcs.cexiste une fonction lcs()avec la signature struct Sequence * lcs (struct Sequence * s1, struct Sequence * s2) . Pour configurer le type de retour, j'utilise lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE).
  • Pour effectuer un appel avec une référence à la structure de séquence , j'utilise ctypes.byref()celui qui renvoie un «pointeur lumineux» à votre objet (plus rapidement que ctypes.POINTER()).
  • common.items- c'est une liste d'octets, ils peuvent être décodés pour se présenter retsous forme de liste str.
  • lcsmodule.freeSequence (common) libère simplement la mémoire associée à common. Ceci est important car le garbage collector (AFAIK) ne le collectera pas automatiquement.

Le code Python optimisé est le code que vous avez écrit en C et enveloppé en Python.

Quelque chose de plus: PyPy


Attention: je n'ai moi-même jamais utilisé PyPy.
L'une des optimisations les plus simples consiste à exécuter vos programmes dans le runtime PyPy , qui contient un compilateur JIT (juste à temps) qui accélère le travail des boucles, en les compilant en code machine pour une exécution répétée.

Si vous avez des commentaires ou souhaitez discuter de quelque chose, écrivez-moi (samuel.robert.stevens@gmail.com).

C'est tout. Rendez-vous sur le parcours !

All Articles