====== LU07f - Leistung messen ====== Ab hier geht es nicht mehr um Lesbarkeit, sondern um Laufzeit - das zweite Standbein von Kompetenzband D. Die Regel dazu ist kurz: **Erst messen, dann ändern, dann nachmessen.** **Ohne Messung ist es keine Optimierung, sondern eine Vermutung.** Und Vermutungen über Laufzeit sind erstaunlich oft falsch - auch bei erfahrenen Entwicklerinnen und Entwicklern. Für **D1A** verlangt das Portfolio ausdrücklich eine Messung vorher und nachher. ===== timeit: einzelne Varianten vergleichen ===== ''timeit'' führt ein Stück Code mehrfach aus und misst die Zeit. Mehrfach deshalb, weil ein einzelner Durchlauf zu stark schwankt. import timeit, random random.seed(42) gesperrt_liste = [f"user{i}" for i in range(10000)] gesperrt_set = set(gesperrt_liste) anfragen = [f"user{random.randrange(20000)}" for _ in range(2000)] def mit_liste(): return sum(1 for a in anfragen if a in gesperrt_liste) def mit_set(): return sum(1 for a in anfragen if a in gesperrt_set) assert mit_liste() == mit_set() # gleiches Ergebnis, sonst ist der Vergleich wertlos print(min(timeit.repeat(mit_liste, number=1, repeat=5))) print(min(timeit.repeat(mit_set, number=1, repeat=5))) 0.1070 # Liste 0.000068 # Set -> rund 1500-mal schneller Drei Dinge machen die Messung erst brauchbar: * **''repeat'' und ''min''**: Der schnellste Durchlauf ist der aussagekräftigste, weil er am wenigsten durch andere Prozesse gestört wurde. * **Gleiche Daten** für beide Varianten, und zwar realistische - eine Liste mit 10 Einträgen misst nichts. * **''assert'' auf gleiches Ergebnis.** Eine schnellere Funktion, die etwas anderes liefert, ist keine Optimierung. Auf der Kommandozeile geht es auch ohne Skript: python -m timeit -s "d = list(range(10000))" "9999 in d" python -m timeit -s "d = set(range(10000))" "9999 in d" ===== cProfile: den Hotspot finden ===== ''timeit'' vergleicht zwei Varianten, die Sie schon im Verdacht haben. ''cProfile'' sagt Ihnen, //wo// die Zeit überhaupt hingeht. import cProfile, pstats cProfile.run("pruefen(anfragen)", "profil.stats") p = pstats.Stats("profil.stats") p.sort_stats("cumulative").print_stats(6) 12004 function calls in 0.104 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.002 0.002 0.104 0.104 prof_demo.py:12(pruefen) 3000 0.099 0.000 0.099 0.000 prof_demo.py:9(ist_gesperrt) 3000 0.002 0.000 0.003 0.000 prof_demo.py:6(normalisieren) 3000 0.001 0.000 0.001 0.000 {method 'strip' of 'str' objects} So lesen Sie die Spalten: ^ Spalte ^ Bedeutung ^ | ''ncalls'' | wie oft die Funktion aufgerufen wurde | | ''tottime'' | Zeit **in** der Funktion selbst, ohne Unterfunktionen | | ''cumtime'' | Zeit inklusive allem, was sie aufruft | Der Befund ist eindeutig: ''ist_gesperrt'' verbraucht 0.099 der 0.104 Sekunden. ''normalisieren'' wird **gleich oft** aufgerufen und kostet 0.003 Sekunden. Wer ohne Profiling optimiert hätte, wäre gut über ''normalisieren'' gestolpert - die Funktion sieht teuer aus, ''strip'' und ''lower'' auf 3000 Strings. Sie ist es aber nicht. **Vorgehen bei Performance-Problemen** - Reproduzierbaren Fall mit realistischen Daten bauen. - Mit ''cProfile'' den Hotspot suchen, nicht raten. - **Nur den Hotspot** ändern, alles andere in Ruhe lassen. - Mit ''timeit'' nachmessen und das Ergebnis mit ''assert'' auf Gleichheit prüfen. - Vorher- und Nachher-Zahl notieren - das ist der Nachweis für D1A. ===== Warum Messen und nicht Auswendiglernen ===== Eine oft zitierte Regel lautet: «Strings in einer Schleife mit ''+='' zusammenzuhängen ist quadratisch, nimm ''join''.» Die Messung mit 20'000 Teilstücken: s += t 0.00060 s "".join() 0.00012 s -> Faktor 5, nicht Faktor 1000 CPython optimiert genau dieses Muster intern, wenn der String nirgends sonst referenziert wird. ''join'' bleibt trotzdem die bessere Wahl - weil es kürzer und klarer ist und in anderen Python-Implementationen zuverlässig schneller. Aber die Begründung lautet **Lesbarkeit**, nicht Faktor 1000. Genau das ist der Unterschied zwischen Niveau I und Niveau A: Auf Niveau I setzen Sie eine vorgegebene Massnahme um. Auf Niveau A prüfen Sie nach, ob sie in Ihrem Fall überhaupt etwas bringt - und sagen es ehrlich, wenn nicht. ===== Was Sie messen sollten ===== * **Realistische Datenmenge.** Mit 20 Datensätzen ist alles schnell genug. * **Denselben Ablauf.** Kein Cache aus dem vorherigen Durchlauf, keine bereits geöffnete Datenbankverbindung nur in einer Variante. * **Mehrere Wiederholungen**, und den kleinsten Wert nehmen. * **Die Zahlen aufschreiben**, mit Datum und Datenmenge. Sonst ist die Messung in zwei Wochen wertlos. ---- {{tag>M323-LU07 M323-D1I M323-D1A}} [[https://creativecommons.org/licenses/by-nc-sa/4.0/ch/|{{https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png}}]] (c) Kevin Maurizi