La mémoïsation est une technique d'optimisation de code consistant à réduire le temps d'exécution d'une fonction en mémorisant ses résultats d'une fois sur l'autre, une sorte de cache quoi. Ça peut-être super pratique dans certains cas, surtout lorsqu'il y a des calculs complexes ou beaucoup de données redondantes.
Étant donné que Sam & Max ont déjà couvert le sujet, nous allons seulement montrer un exemple concrêt : le cas de notre module MSS.
Le contexte
Le module MSS est, comme sont nom ne l'indique pas, un module permettant de prendre des captures d'écran sans module tiers, seulement du python pur. Il utilise intensément le module ctypes, qui permet d'appeler des fonctions C/C++ depuis Python.
Sous GNU/Linux, avec un code équivalent C <=> Python permettant de prendre une capture d'écran, la différence de vitesse d'exécution est quintuplée, voire sextuplée. Ça fait une sacrée différence. Je me suis donc plongé dans le code pour voir ce qui pouvait être revu ou amélioré. Au final, il s'avère que les routines pour récupérer les pixels et générer l'image PNG seront améliorées. Mais le gain fût minime.
Le stockage des pixels reçus par la fonction XGetPixel est un gouffre. C'est là qu'il faut farfouiller, voici le code fautif :
def pix(pixel):
""" Apply shifts to a pixel to get the RGB values. """
return b((pixel & 16711680) >> 16) + b((pixel & 65280) >> 8) + b(pixel & 255)
get_pix = self.XGetPixel
pixels = [pix(get_pix(image, x, y)) for y in range(height) for x in range(width)]
Dans la fonction pix(), le décalage de bits n'est pas le plus gourmant, ce sont les concaténations. En effet, à chaque fois, il y a réallocation de mémoire et copie de données dont je me passerais bien.
Mémoïsation
Dans une certaine logique, il y a énormément de pixels identiques sur un écran. Avec une telle quantitié, et sachant les opérations qu'il faille faire sur chacun des pixels, la mémoïsation prend tout son sens :
def pix(px, _resultats={}):
# Apply shifts to a pixel to get the RGB values.
# This method uses of memoization.
if not px in _resultats:
_resultats[px] = b((px & 16711680) >> 16) + b((px & 65280) >> 8) + b(px & 255)
return _resultats[px]
get_pix = self.XGetPixel
pixels = [pix(get_pix(image, x, y)) for y in range(height) for x in range(width)]
Étant donné que les paramètres sont initialisés à la déclaration, le paramètre _resultats de la fonction pix() ne sera pas recréé à chaque appel. Il s'agit d'un dict qui recevra tous les pixels et le résultat attendu pour chacun d'eux. Dans ladite fonction, on vérifie si le pixel a déja été traité, si non, on effectue les opérations sur celui-ci, puis on renvoie ce que le dict a en mémoire. Un petit cache tout simple et efficace.
Résultats
Avec un écran de 1280x1024, je passe de ±4 sec à <2 sec, reduisant les pixels traités de 1 310 720
à 11 204
.
Avec un écran de 1920x1280, je passe de ±6 sec à <3 sec, reduisant les pixels traités de 2 457 600
à 8 236
.
Un gain non négligeable !