[PYTHON] Ich denke, es ist ein Verlust, den Profiler nicht für die Leistungsoptimierung zu verwenden

Die Leistungsoptimierungsarbeit, die die Druckanweisung vorbereitet und die Ausführungszeit ausgibt, ist schmerzhaft, daher spreche ich davon, sie zu stoppen.

Verbesserungen sind einfach, wenn das Programm langsam laufende Logik erkennen kann. Wenn Sie den Profiler verwenden, können Sie die Ursache leicht identifizieren, sodass ich Ihnen zeigen werde, wie Sie ihn verwenden. Die erste Hälfte ist eine Methode zum Identifizieren der Logik für langsame Ausführung mithilfe von line_profiler, und die zweite Hälfte ist eine Beschleunigungstechnik in Python.

Identifizieren Sie, welche Zeile mit dem Profiler schwer ist

Verwenden Sie den Profiler in Ihrer lokalen Umgebung, um festzustellen, welche Zeile schwer ist. Es gibt verschiedene Profiler in Python, aber ich persönlich benutze line_profiler, weil es die notwendigen und ausreichenden Funktionen hat. Was wir hier spezifizieren, ist das "welche Zeile N-mal ausgeführt wurde und die Gesamtausführungszeit M% beträgt".

Beispiel für die Verwendung von line_profiler

Ich habe einen Beispielcode geschrieben, dessen Ausführung ungefähr 10 Sekunden dauert. Bitte lesen Sie den Prozess von time.sleep () als DB-Zugriff. Es ist ein Programm, das die Daten zurückgibt, dass der Benutzer 1000 Karten und 3 Fähigkeiten für jede Karte mit json hat.

■ Der Profiler sagt Ihnen alles, sodass Sie den Code überspringen können

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),  #Der SkillID-Bereich ist 1-Angenommen 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)]  #Karte hat 3 Fähigkeiten

    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)  #Der Karten-ID-Bereich beträgt 1-Angenommen, 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)]  #Angenommen, der Benutzer hat 1000 Karten

    def to_dict(self):
        """
Konvertieren Sie Karteninformationen in Diktat und kehren Sie zurück
        """
        return {
            "name": self.name,
            "skills": [skill.to_dict() for skill in self.skills],
        }


def main(user_id):
    """
Antworten Sie mit json auf die Karteninformationen des Benutzers
    """
    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)

Identifizieren Sie dicke Linien mit line_profiler

Lassen Sie uns nun das Profiler-Tool installieren und die schweren Stellen identifizieren.

install


pip install line_profiler 

sample1_profiler.py


~~Kürzung~~

#Profiler-Instanziierung und Funktionsregistrierung
from line_profiler import LineProfiler
profiler = LineProfiler()
profiler.add_module(UserCard)
profiler.add_module(UserCardSkill)
profiler.add_function(main)

#Ausführung der registrierten Hauptfunktion
user_id = "A0001"
profiler.runcall(main, user_id)

#Ergebnisanzeige
profiler.print_stats()

Ausführungsergebnis von line_profiler

Ausführungsergebnis


>>>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),  #Der SkillID-Bereich ist 1-Angenommen 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)  #Der Karten-ID-Bereich beträgt 1-Angenommen, 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 Karteninformationen in Diktat konvertieren und zurückgeben
    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 Antworten Sie mit json auf die Karteninformationen des Benutzers
    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


■ Ich konnte eine dicke Linie mit dem Profiler identifizieren. Aus dem Ausführungsergebnis von line_profiler wurde festgestellt, dass die Verarbeitung der Zeilen 65 und 55 schwer war. Es scheint, dass der Benutzer über 1000 Karten verfügt. Aufgrund der 1000-maligen Abfrage von UserCardSkill für jede Karte dauerte die Ausführung mehr als 10 Sekunden.

Beschleunigungstechnik

Dies ist eine Technik zur Verbesserung der Ausführungsgeschwindigkeit eines bestimmten Programms. Wir werden das vom Profiler untersuchte Programm optimieren, indem wir Notizen mit dem Cache machen und nach Hash suchen, ohne die Codestruktur so weit wie möglich zu ändern. Ich möchte über Python sprechen, daher werde ich nicht über die Beschleunigung von SQL sprechen.

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

Reduzierung der Anzahl der DB-Anfragen in Verbindung mit der Memo-Konvertierung

Reduzieren Sie die Anzahl der Skill-Abfragen für Benutzerkarten, ohne die Codestruktur so weit wie möglich zu ändern. Es ist ein Code, der UserCardSkill abruft, das dem Benutzer in einem Stapel zugeordnet ist, es im Speicher speichert und ab dem zweiten Mal den Wert aus den Daten im Speicher zurückgibt.

sample1_memoize.py


class UserCardSkill(object):
    _USER_CACHE = {}
    @classmethod
    def get_by_card(cls, user_id, card_id):
        #Funktion, um jedes Mal vor der Verbesserung auf die Datenbank zuzugreifen
        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):
        #Funktion zum erstmaligen Zugriff auf die Datenbank nach der Verbesserung
        if user_id not in cls._USER_CACHE:
            #Wenn sich keine Daten im Cache befinden, beziehen Sie alle mit dem Benutzer verbundenen Fähigkeiten aus der Datenbank
            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):
        #Erwerben Sie alle mit dem Benutzer verbundenen Fähigkeiten gleichzeitig von der Datenbank
        return list(cls.objects.filter(user_id=user_id))

from timeit import timeit
@timeit  #Die Ausführungszeit wird gedruckt
def main(user_id):

Ausführungsergebnis


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

Es ist mehr als 15-mal schneller als 11,1061 Sekunden vor der Verbesserung auf 0,6718 Sekunden. Der Grund für die Verbesserung der Ausführungsgeschwindigkeit ist, dass die Anzahl der Anfragen an UserCardSkill von 1000 auf 1 reduziert wurde.

Schreiben Sie von der linearen Suche zur Hash-Suche um

Im Memorandum-Code wird die Liste "cls._USER_CACHE [user_id]" mit 3 * 1000 Elementen jedes Mal linear durchsucht (vollständiger Scan), um die Fertigkeit für jede Karte in der Funktion "get_by_card_from_cache" zu linearisieren. Da es ineffizient ist, jede Zeile zu durchsuchen, generieren Sie im Voraus ein Diktat mit card_id als Schlüssel und schreiben Sie es als Hash-Suche neu. In diesem Code beträgt der Rechenaufwand für die lineare Suche O (n) und der Rechenaufwand für die Hash-Suche O (1).

python


~~Kürzung~~

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:
            #Wenn sich keine Daten im Cache befinden, beziehen Sie alle mit dem Benutzer verbundenen Fähigkeiten aus der Datenbank
            users_skill = cls.get_all_by_user(user_id)

            # card_In Diktat mit ID als SCHLÜSSEL konvertieren
            cardskill_dict = defaultdict(list)
            for skill in users_skill:
                cardskill_dict[skill.card_id].append(skill)

            #Im Cache speichern
            cls._USER_CACHE[user_id] = cardskill_dict

        #Von der linearen Suche zur Hash-Suche umgeschrieben
        return cls._USER_CACHE[user_id].get(card_id)

    @classmethod
    def get_all_by_user(cls, user_id):
        #Erwerben Sie alle Fähigkeiten in Bezug auf Benutzer von DB
        return list(cls.objects.filter(user_id=user_id))

Ausführungsergebnis


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

Vor der Verbesserung wurde die Liste mit 3000 Elementen vollständig auf 1000 Karten gescannt. "Skill.card_id == card_id:" wurde also 3 Millionen Mal aufgerufen. Da es durch Ersetzen durch Hash-Suche verschwunden ist, führt dies zu einer Verbesserung der Ausführungsgeschwindigkeit, selbst wenn die Kosten für die Generierung von Hash abgezogen werden.

Verwenden Sie 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

Ausführungsergebnis


# cached_Vor dem Anwenden von Eigentum
>>>python ./cached_property.py 
func:'main' args:[(u'A0001',), {}] took: 0.1443 sec

# cached_Nach dem Anwenden der Eigenschaft
>>> python ./sample1_cp.py 
func:'main' args:[(u'A0001',), {}] took: 0.0451 sec

Verwenden Sie den lokalen Thread-Speicher

Es ist eine Geschichte unter der Annahme, dass der Webserver auf wsgi und Apache läuft.

Thread Local Storage (TLS) ist ein Mittel zum Zuweisen eines Speicherorts für eindeutige Daten für jeden Thread in einem bestimmten Multithread-Prozess. Wenn Sie einen Webserver mit wsgi und Apache ausführen und "MaxRequestsPerChild" in der Konfiguration auf einen Wert größer oder gleich 1 setzen, wird der untergeordnete Prozess nach "MaxRequestsPerChild" -Anforderungen beendet. Wenn Sie ein Programm schreiben, das TLS (Thread Local Storage) verwendet, können Sie den Cache für jeden untergeordneten Prozess speichern. Durch das Speichern von Daten, die allen Benutzern gemeinsam sind, wie z. B. Stammdaten, in TLS kann eine erhebliche Beschleunigung erwartet werden.

Ich habe ein Programm geschrieben, das Primzahlen aus ganzen Zahlen im Bereich von 0-500010 berechnet. Durch Aufzeichnen des Ergebnisses der Primzahlenberechnung in TLS werden die zweite und nachfolgende Primzahlenberechnung übersprungen.

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"
        #TLS-Initialisierung
        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):
    """
Geben Sie eine Liste mit Primzahlen zurück
    :param N: int
    :rtype : list of int
    """
    #Wenn TLS Daten enthält, geben Sie diese aus dem Cache zurück
    if N in threadLocal.prime:
        return threadLocal.prime[N]

    #Berechnen Sie eine Primzahl
    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:]

    #Ergebnisse in TLS aufzeichnen
    threadLocal.prime[N] = result
    return result

for x in xrange(100):
    worker()

Ausführungsergebnis


>>>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
...

Der im TLS (Thread Local Storage) gespeicherte Cache wird für jeden untergeordneten Prozess von Apache gespeichert und bleibt bestehen, bis der untergeordnete Prozess beendet wird.

Cache hat Nebenwirkungen

Durch die ordnungsgemäße Verwendung des Caches wird die Ausführungsgeschwindigkeit des Programms verbessert. Seien Sie jedoch vorsichtig, da es viele Fälle gibt, in denen Cache-spezifische Fehler auftreten, die als Nebenwirkungen bezeichnet werden. Wenn ich in der Vergangenheit etwas gesehen oder getan habe

■ Zeigen Sie den Fehler an, dass ein neuer Wert nicht erhalten werden kann, selbst wenn er aktualisiert wurde Dies ist ein Fehler, der auftritt, wenn Sie ihn verwenden, ohne das Design des Cache-Lebenszyklus zu kennen.

  1. Werterfassung >> 2. Wertaktualisierung >> 3. Beim Schreiben eines Programms, das die Werterfassung in dieser Reihenfolge durchführt, wurde der Wert in 1 zwischengespeichert, und der Cache verschwand beim Aktualisieren in 2 und beim Aktualisieren in 3 beim Erfassen nicht. Es ist ein Fehler, dass der alte Wert so erfasst und angezeigt wird, wie er ist, ohne den angegebenen Wert erfassen zu können.

■ Fehler, dass Daten verschwinden Es ist ein tödlicher Kerl. 1. Werterfassung >> 2. In einem Programm, das den erfassten Wert addiert und den Wert aktualisiert, wird das Ergebnis des Werts 1 im Cache referenziert und nicht aktualisiert, z. B. 1234 + 100, 1234 + 200, 1234 Bei +50 gibt es einen Fehler, bei dem der Wert verschwindet.

■ So verhindern Sie Nebenwirkungen Jeder kann es sicher verwenden, indem er es wie einen "cached_property" -Dekorator verpackt und mit dem Cache aus einem gut getesteten Paket arbeitet. Sie können damit umgehen, ohne die Theorie zu kennen, aber wenn möglich, ist es besser, die Theorie über den Lebenszyklus des Caches zu kennen.

memo Das Veröffentlichungsdatum von line_profiler ist 2008

Recommended Posts

Ich denke, es ist ein Verlust, den Profiler nicht für die Leistungsoptimierung zu verwenden
[Pyto] Ich habe versucht, ein Smartphone als Flick-Tastatur für den PC zu verwenden
[Python] Ich möchte nur den Index verwenden, wenn ich eine Liste mit einer for-Anweisung schleife
Praktisch, um Matplotlib-Unterzeichnungen in for-Anweisungen zu verwenden
Ich möchte eine virtuelle Umgebung mit Jupyter Notebook verwenden!
Ich wusste nicht, wie ich die [Python] für die Anweisung verwenden sollte
Ich habe versucht, einen Bot für die Ankündigung eines Wiire-Ereignisses zu erstellen
Wovon ich in Kapitel 3 der kollektiven Intelligenz abhängig war. Es ist kein Tippfehler, daher denke ich, dass etwas mit meinem Code nicht stimmt.
Verwenden Sie eine Skriptsprache für ein komfortables C ++ - Leben - OpenCV-Port Python zu C ++ -
Ich möchte vorerst eine Docker-Datei erstellen.
Ich möchte eine Datei, die keine bestimmte Zeichenfolge ist, als logrotate Ziel angeben, aber ist es unmöglich?
Obwohl es diesmal ist, werde ich zu Hause einen Linux-Server einrichten. Ich werde später darüber nachdenken, wie man es benutzt.
Ich möchte einen Platzhalter verwenden, den ich mit Python entfernen möchte
Wenn ich versuche, pip zu verwenden, ist das SSL-Modul nicht verfügbar.
Ich habe versucht, Jojo mit LSTM ein seltsames Zitat zu machen
Es ist eine Verschwendung, es nicht zu benutzen, ein System, um das E-Qualifizierungszertifizierungsprogramm zum halben Preis zu absolvieren!