[PYTHON] Je pense que c'est une perte de ne pas utiliser le profileur pour le réglage des performances

Le travail de réglage des performances qui prépare l'instruction d'impression et génère le temps d'exécution est pénible, donc je parle de l'arrêter.

Les améliorations sont faciles si le programme peut identifier une logique à exécution lente. Si vous utilisez le profileur, vous pouvez facilement identifier la cause, je vais donc vous montrer comment l'utiliser. La première moitié est une méthode d'identification de la logique d'exécution lente à l'aide de line_profiler, et la seconde moitié est une technique d'accélération en Python.

Identifiez quelle ligne est lourde avec le profileur

Utilisez le profileur dans votre environnement local pour identifier quelle ligne est lourde. Il existe différents profileurs en Python, mais j'utilise personnellement line_profiler car il possède les fonctions nécessaires et suffisantes. Ce que nous spécifions ici, c'est que «quelle ligne a été exécutée N fois, et le temps total d'exécution est de M%».

Exemple d'utilisation de line_profiler

J'ai écrit un exemple de code qui prend environ 10 secondes à exécuter. Veuillez lire le processus de time.sleep () comme accès à la base de données. C'est un programme qui renvoie les données que l'utilisateur a 1000 cartes et 3 compétences pour chaque carte avec json.

■ Le profileur vous dira tout pour que vous puissiez ignorer le code

sample1.py


# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import random
import time
import simplejson


class UserCardSkill(object):
    def __init__(self, user_id, card_id):
        self.id = random.randint(1, 1000),  #La plage SkillID est 1-Supposé 999
        self.user_id = user_id
        self.card_id = card_id

    @property
    def name(self):
        return "skill:{}".format(str(self.id))

    @classmethod
    def get_by_card(cls, user_id, card_id):
        time.sleep(0.01)
        return [cls(user_id,  card_id) for x in xrange(3)]  #La carte a 3 compétences

    def to_dict(self):
        return {
            "name": self.name,
            "skill_id": self.id,
            "card_id": self.card_id,
        }


class UserCard(object):
    def __init__(self, user_id):
        self.id = random.randint(1, 300)  #La plage d'identification de la carte est 1-En supposant 299
        self.user_id = user_id

    @property
    def name(self):
        return "CARD:{}".format(str(self.id))

    @property
    def skills(self):
        return UserCardSkill.get_by_card(self.user_id, self.id)

    @classmethod
    def get_by_user(cls, user_id):
        time.sleep(0.03)
        return [cls(user_id) for x in range(1000)]  #En supposant que l'utilisateur dispose de 1000 cartes

    def to_dict(self):
        """
Convertir les informations de la carte en dict et retourner
        """
        return {
            "name": self.name,
            "skills": [skill.to_dict() for skill in self.skills],
        }


def main(user_id):
    """
Répondez avec json aux informations de la carte possédées par l'utilisateur
    """
    cards = UserCard.get_by_user(user_id)
    result = {
        "cards": [card.to_dict() for card in cards]
    }
    json = simplejson.dumps(result)
    return json

user_id = "A0001"
main(user_id)

Identifiez les lignes lourdes avec line_profiler

Maintenant, installons l'outil de profilage et identifions les points lourds.

install


pip install line_profiler 

sample1_profiler.py


~~réduction~~

#Instanciation du profileur et enregistrement des fonctions
from line_profiler import LineProfiler
profiler = LineProfiler()
profiler.add_module(UserCard)
profiler.add_module(UserCardSkill)
profiler.add_function(main)

#Exécution de la fonction principale enregistrée
user_id = "A0001"
profiler.runcall(main, user_id)

#Affichage des résultats
profiler.print_stats()

Résultat d'exécution de line_profiler

Résultat d'exécution


>>>python ./sample1_profiler.py 
Timer unit: 1e-06 s

Total time: 0.102145 s
File: ./sample1_profiler.py
Function: __init__ at line 9

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     9                                               def __init__(self, user_id, card_id):
    10      3000        92247     30.7     90.3          self.id = random.randint(1, 1000),  #La plage SkillID est 1-Supposé 999
    11      3000         5806      1.9      5.7          self.user_id = user_id
    12      3000         4092      1.4      4.0          self.card_id = card_id

Total time: 0.085992 s
File: ./sample1_profiler.py
Function: to_dict at line 23

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    23                                               def to_dict(self):
    24      3000        10026      3.3     11.7          return {
    25      3000        66067     22.0     76.8              "name": self.name,
    26      3000         6091      2.0      7.1              "skill_id": self.id,
    27      3000         3808      1.3      4.4              "card_id": self.card_id,
    28                                                   }

Total time: 0.007384 s
File: ./sample1_profiler.py
Function: __init__ at line 32

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    32                                               def __init__(self, user_id):
    33      1000         6719      6.7     91.0          self.id = random.randint(1, 300)  #La plage d'identification de la carte est 1-En supposant 299
    34      1000          665      0.7      9.0          self.user_id = user_id

Total time: 11.0361 s
File: ./sample1_profiler.py
Function: to_dict at line 49

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    49                                               def to_dict(self):
    50                                                   """
51 Conversion des informations de la carte en dict et retour
    52                                                   """
    53      1000         1367      1.4      0.0          return {
    54      1000        10362     10.4      0.1              "name": self.name,
    55      4000     11024403   2756.1     99.9              "skills": [skill.to_dict() for skill in self.skills],
    56                                                   }

Total time: 11.1061 s
File: ./sample1_profiler.py
Function: main at line 59

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    59                                           def main(user_id):
    60                                               """
61 Répondez avec json aux informations de la carte possédées par l'utilisateur
    62                                               """
    63         1        41318  41318.0      0.4      cards = UserCard.get_by_user(user_id)
    64         1            1      1.0      0.0      result = {
    65      1001     11049561  11038.5     99.5          "cards": [card.to_dict() for card in cards]
    66                                               }
    67         1        15258  15258.0      0.1      json = simplejson.dumps(result)
    68         1            2      2.0      0.0      return json


■ J'ai pu identifier une ligne lourde avec profiler. D'après le résultat de l'exécution de line_profiler, il a été constaté que le traitement des lignes 65 et 55 était lourd. Il semble que l'utilisateur dispose de 1000 cartes, et après avoir interrogé UserCardSkill 1000 fois pour chaque carte, il a fallu plus de 10 secondes pour s'exécuter.

Technique d'accélération

Il s'agit d'une technique pour améliorer la vitesse d'exécution d'un programme spécifique. Nous ajusterons le programme étudié par le profileur en prenant des notes avec Cache et en recherchant Hash sans changer autant que possible la structure du code. Je veux parler de Python, donc je ne parlerai pas d'accélérer SQL.

スクリーンショット 2015-11-25 21.53.09.png

Réduction du nombre de requêtes DB combinée à la conversion des mémos

Réduisez le nombre de requêtes de compétence de carte utilisateur sans modifier autant que possible la structure du code. C'est un code qui acquiert UserCardSkill associé à l'utilisateur dans un lot, l'enregistre en mémoire et renvoie la valeur à partir des données en mémoire à partir de la deuxième fois.

sample1_memoize.py


class UserCardSkill(object):
    _USER_CACHE = {}
    @classmethod
    def get_by_card(cls, user_id, card_id):
        #Fonction d'accès à la base de données à chaque fois avant amélioration
        time.sleep(0.01)
        return [cls(user_id,  card_id) for x in xrange(3)]

    @classmethod
    def get_by_card_from_cache(cls, user_id, card_id):
        #Fonction permettant d'accéder à la base de données uniquement pour la première fois après amélioration
        if user_id not in cls._USER_CACHE:
            #S'il n'y a pas de données dans le cache, obtenez toutes les compétences liées à l'utilisateur à partir de la base de données
            cls._USER_CACHE[user_id] = cls.get_all_by_user(user_id)

        r = []
        for skill in cls._USER_CACHE[user_id]:
            if skill.card_id == card_id:
                r.append(skill)
        return r

    @classmethod
    def get_all_by_user(cls, user_id):
        #Obtenez toutes les compétences liées à l'utilisateur de DB à la fois
        return list(cls.objects.filter(user_id=user_id))

from timeit import timeit
@timeit  #L'heure d'exécution est imprimée
def main(user_id):

Résultat d'exécution


>>>sample1_memoize.py
func:'main' args:[(u'A0001',), {}] took: 0.6718 sec

C'est plus de 15 fois plus rapide que 11.1061 sec avant l'amélioration à 0.6718 sec. La raison de l'amélioration de la vitesse d'exécution est que le nombre de requêtes adressées à UserCardSkill a été réduit de 1000 à 1.

Réécrire de la recherche linéaire à la recherche par hachage

Dans le code mémorandum, la liste cls._USER_CACHE [user_id] avec 3 * 1000 éléments est recherchée linéairement (scan complet) à chaque fois afin de linéariser la compétence pour chaque carte dans la fonction get_by_card_from_cache. Comme il est inefficace de rechercher chaque ligne, générez un dict avec card_id comme clé à l'avance et réécrivez-le comme une recherche par hachage. Dans ce code, la quantité de calcul pour la recherche linéaire est O (n) et la quantité de calcul pour la recherche par hachage est O (1).

python


~~réduction~~

class UserCardSkill(object):
    _USER_CACHE = {}
    @classmethod
    def get_by_card_from_cache(cls, user_id, card_id):
        if user_id not in cls._USER_CACHE:
            #S'il n'y a pas de données dans le cache, obtenez toutes les compétences liées à l'utilisateur à partir de la base de données
            users_skill = cls.get_all_by_user(user_id)

            # card_Convertir en dict avec identifiant comme clé
            cardskill_dict = defaultdict(list)
            for skill in users_skill:
                cardskill_dict[skill.card_id].append(skill)

            #Enregistrer dans le cache
            cls._USER_CACHE[user_id] = cardskill_dict

        #Réécrit de la recherche linéaire à la recherche par hachage
        return cls._USER_CACHE[user_id].get(card_id)

    @classmethod
    def get_all_by_user(cls, user_id):
        #Acquérir toutes les compétences liées à l'utilisateur de DB
        return list(cls.objects.filter(user_id=user_id))

Résultat d'exécution


>>>sample1_hash.py
func:'main' args:[(u'A0001',), {}] took: 0.3840 sec

Avant l'amélioration, la liste de 3000 éléments était entièrement scannée pour 1000 cartes, donc ʻif skill.card_id == card_id: `a été appelé 3 millions de fois. Puisqu'il a disparu en le remplaçant par une recherche de hachage, même si le coût de génération de hachage est déduit, cela conduit à une amélioration de la vitesse d'exécution.

Utilisez cached_property

お手軽なメモ化といえばcached_propertyではないでしょうか。インスタンスキャッシュにself.func.__name__(サンプル実装であれば"skills")をKEYにして戻り値を保存しています。2回目以降の問い合わせではキャッシュから値を返却することで実行速度が改善します。実装は数行なのでコード読んだ方が早いかもしれません。cached_property.py#L12

cached_property.py


from cached_property import cached_property

class Card(object):
    @cached_property
    def skills(self):
        return UserCardSkill.get_by_card(self.user_id, self.id)

@timeit
def main(user_id):
    cards = Card.get_by_user(user_id)
    for x in xrange(10):
        cards[0].skills

Résultat d'exécution


# cached_Avant d'appliquer la propriété
>>>python ./cached_property.py 
func:'main' args:[(u'A0001',), {}] took: 0.1443 sec

# cached_Après l'application de la propriété
>>> python ./sample1_cp.py 
func:'main' args:[(u'A0001',), {}] took: 0.0451 sec

Utiliser le stockage local des threads

C'est une histoire sur l'hypothèse que le serveur Web fonctionne sur wsgi et Apache.

Thread Local Storage (TLS) est un moyen d'allouer un emplacement pour stocker des données uniques pour chaque thread dans un processus multithread donné. Si vous exécutez un serveur Web avec wsgi et Apache et que vous définissez MaxRequestsPerChild sur une valeur supérieure ou égale à 1 dans config, le processus enfant se terminera après les requêtes MaxRequestsPerChild. Si vous écrivez un programme qui utilise le stockage local des threads (TLS), vous pouvez enregistrer le cache pour chaque processus enfant. En stockant des données communes à tous les utilisateurs, telles que les données de base, dans TLS, on peut s'attendre à une accélération significative.

J'ai écrit un programme qui calcule les nombres premiers à partir d'entiers compris entre 0 et 5 00010. En enregistrant le résultat du calcul du nombre premier dans TLS, le deuxième calcul du nombre premier et les suivants sont sautés.

tls.py


# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import random
import threading
import time

threadLocal = threading.local()


def timeit(f):
    def timed(*args, **kw):
        # http://stackoverflow.com/questions/1622943/timeit-versus-timing-decorator
        ts = time.time()
        result = f(*args, **kw)
        te = time.time()

        print 'func:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, te-ts)
        return result
    return timed


@timeit
def worker():
    initialized = getattr(threadLocal, 'initialized', None)
    if initialized is None:
        print "init start"
        #Initialisation TLS
        threadLocal.initialized = True
        threadLocal.count = 0
        threadLocal.prime = {}
        return []
    else:
        print "loop:{}".format(threadLocal.count)
        threadLocal.count += 1
        return get_prime(random.randint(500000, 500010))


def get_prime(N):
    """
Renvoie une liste de nombres premiers
    :param N: int
    :rtype : list of int
    """
    #S'il y a des données dans TLS, renvoyez-les à partir du cache
    if N in threadLocal.prime:
        return threadLocal.prime[N]

    #Calculer un nombre premier
    table = list(range(N))
    for i in range(2, int(N ** 0.5) + 1):
        if table[i]:
            for mult in range(i ** 2, N, i):
                table[mult] = False
    result = [p for p in table if p][1:]

    #Enregistrer les résultats dans TLS
    threadLocal.prime[N] = result
    return result

for x in xrange(100):
    worker()

Résultat d'exécution


>>>python tls.py 
init start
func:'worker' args:[(), {}] took: 0.0000 sec
loop:0
func:'worker' args:[(), {}] took: 0.1715 sec
loop:1
func:'worker' args:[(), {}] took: 0.1862 sec
loop:2
func:'worker' args:[(), {}] took: 0.0000 sec
loop:3
func:'worker' args:[(), {}] took: 0.2403 sec
loop:4
func:'worker' args:[(), {}] took: 0.2669 sec
loop:5
func:'worker' args:[(), {}] took: 0.0001 sec
loop:6
func:'worker' args:[(), {}] took: 0.3130 sec
loop:7
func:'worker' args:[(), {}] took: 0.3456 sec
loop:8
func:'worker' args:[(), {}] took: 0.3224 sec
loop:9
func:'worker' args:[(), {}] took: 0.3208 sec
loop:10
func:'worker' args:[(), {}] took: 0.3196 sec
loop:11
func:'worker' args:[(), {}] took: 0.3282 sec
loop:12
func:'worker' args:[(), {}] took: 0.3257 sec
loop:13
func:'worker' args:[(), {}] took: 0.0000 sec
loop:14
func:'worker' args:[(), {}] took: 0.0000 sec
loop:15
func:'worker' args:[(), {}] took: 0.0000 sec
...

Le cache stocké dans Thread Local Storage (TLS) est enregistré pour chaque processus enfant d'Apache et le reste jusqu'à ce que le processus enfant se termine.

Le cache a des effets secondaires

Une utilisation correcte du cache améliorera la vitesse d'exécution du programme. Cependant, soyez prudent car il existe de nombreux cas où des bogues spécifiques au cache appelés effets secondaires se produisent. Quand j'ai vu ou fait quelque chose dans le passé

■ Afficher le bogue que la nouvelle valeur ne peut pas être obtenue même si elle est mise à jour Il s'agit d'un bogue qui se produit lorsque vous l'utilisez sans être conscient de la conception du cycle de vie du cache.

  1. Acquisition de valeur >> 2. Mise à jour de valeur >> 3. Lors de l'écriture d'un programme qui effectue l'acquisition de valeur dans cet ordre, la valeur était mise en cache dans 1 et le cache ne disparaissait pas lors de la mise à jour en 2, et mise à jour en 3 lors de l'acquisition. C'est un bug que l'ancienne valeur soit acquise et affichée telle quelle sans pouvoir acquérir la valeur spécifiée.

■ Bug que les données disparaissent C'est un type mortel. 1. Acquisition de valeur >> 2. Dans un programme qui ajoute à la valeur acquise et met à jour la valeur, le résultat de la valeur 1 étant référencé dans le cache et n'étant pas mis à jour, par exemple, 1234 + 100, 1234 + 200, 1234 À +50, il y a un bug qui fait que la valeur disparaît.

■ Comment prévenir les effets secondaires N'importe qui peut l'utiliser en toute sécurité en l'empaquetant comme un décorateur cached_property et en travaillant avec le cache à partir d'un package bien testé. Vous pourrez le gérer sans connaître la théorie, mais si possible, il vaut mieux connaître la théorie sur le cycle de vie du cache.

memo La date de sortie de line_profiler est 2008

Recommended Posts

Je pense que c'est une perte de ne pas utiliser le profileur pour le réglage des performances
[Pyto] J'ai essayé d'utiliser un smartphone comme clavier pour PC
[Python] Je souhaite utiliser uniquement l'index lors de la mise en boucle d'une liste avec une instruction for
Pratique pour utiliser les sous-graphiques matplotlib dans l'instruction for
Je souhaite utiliser un environnement virtuel avec jupyter notebook!
Je ne savais pas comment utiliser l'instruction [python] for
J'ai essayé de créer un bot pour annoncer un événement Wiire
Ce à quoi j'étais accro dans Collective Intelligence Chaprter 3. Ce n'est pas une faute de frappe, donc je pense que quelque chose ne va pas avec mon code.
Utilisez un langage de script pour une vie C ++ confortable-OpenCV-Port Python vers C ++ -
Je veux créer un Dockerfile pour le moment.
Je souhaite spécifier un fichier qui n'est pas une certaine chaîne de caractères comme cible logrotate, mais est-ce impossible?
Même si c'est cette fois, je vais installer un serveur Linux chez moi. Je réfléchirai à comment l'utiliser plus tard.
Je souhaite utiliser un caractère générique que je souhaite décortiquer avec Python remove
Lorsque j'essaye d'utiliser pip, le module SSL n'est pas disponible.
J'ai essayé de faire une étrange citation pour Jojo avec LSTM
C'est un gaspillage de ne pas l'utiliser, un système pour suivre le programme de certification de qualification E à moitié prix!