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.
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%».
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)
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
>>>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.
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.
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.
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.
お手軽なメモ化といえば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
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.
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.
■ 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