Wie man Python-Code mit pytest-benchmark benchmarket
Everett Pompeii
Was ist Benchmarking?
Benchmarking ist die Praxis, die Leistung Ihres Codes zu testen, um zu sehen, wie schnell (Latenz) oder wie viel (Durchsatz) Arbeit er leisten kann. Dieser oft übersehene Schritt in der Softwareentwicklung ist entscheidend für die Erstellung und Wartung von schnellem und leistungsstarkem Code. Benchmarking liefert die notwendigen Metriken für Entwickler, um zu verstehen, wie gut ihr Code unter verschiedenen Arbeitslasten und Bedingungen funktioniert. Aus den gleichen Gründen, aus denen Sie Unit- und Integrationstests schreiben, um Funktionsregressionen zu verhindern, sollten Sie Benchmarks schreiben, um Leistungsregressionen zu verhindern. Leistungsfehler sind Fehler!
Schreiben Sie FizzBuzz in Python
Um Benchmarks zu schreiben, benötigen wir einige Quellcodes zum Benchmarken. Zum Anfang werden wir ein sehr einfaches Programm schreiben, FizzBuzz.
Die Regeln für FizzBuzz lauten wie folgt:
Schreibe ein Programm, das die ganzen Zahlen von
1bis100(inklusiv) ausgibt:
- Für Vielfache von drei, drucke
Fizz- Für Vielfache von fünf, drucke
Buzz- Für Vielfache von drei und fünf, drucke
FizzBuzz- Für alle anderen, drucke die Zahl
Es gibt viele Möglichkeiten, FizzBuzz zu schreiben. Also wählen wir meine Lieblingsmethode:
for i in range(1, 101): if n % 15 == 0: print("FizzBuzz") elif n % 3 == 0: print("Fizz") elif n % 5 == 0: print("Buzz") else: print(i)- Iterieren Sie von
1bis100, indem Sie eine Range von101verwenden. - Berechnen Sie für jede Zahl den Modulus (Rest nach Division) sowohl für
3als auch für5. - Wenn der Rest
0ist, dann ist die Zahl ein Vielfaches des gegebenen Faktors.- Wenn der Rest
0für15ist, dann drucken SieFizzBuzz. - Wenn der Rest
0für3ist, dann drucken SieFizz. - Wenn der Rest
0für5ist, dann drucken SieBuzz.
- Wenn der Rest
- Andernfalls drucken Sie einfach die Zahl.
Schritt-für-Schritt folgen
Um diesem Schritt-für-Schritt-Tutorial folgen zu können, müssen Sie Python installieren und pipenv installieren.
🐰 Der Quellcode für diesen Beitrag ist auf GitHub verfügbar.
Erstellen Sie eine Python-Datei mit dem Namen game.py, und setzen Sie deren Inhalt auf die obige FizzBuzz-Implementierung.
Führen Sie dann python game.py aus. Die Ausgabe sollte folgendermaßen aussehen:
$ python game.py12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz...9798FizzBuzz🐰 Boom! Du knackst das Coding-Interview!
Bevor wir weitermachen, ist es wichtig, die Unterschiede zwischen Mikro-Benchmarking und Makro-Benchmarking zu besprechen.
Micro-Benchmarking vs. Macro-Benchmarking
Es gibt zwei Hauptkategorien von Software-Benchmarks: Micro-Benchmarks und Macro-Benchmarks.
Micro-Benchmarks arbeiten auf einer Ebene ähnlich wie Unit-Tests.
Zum Beispiel wäre ein Benchmark für eine Funktion, die Fizz, Buzz oder FizzBuzz für eine einzelne Zahl ermittelt, ein Micro-Benchmark.
Macro-Benchmarks arbeiten auf einer Ebene, die Integrationstests ähnelt.
Zum Beispiel wäre ein Benchmark für eine Funktion, die das gesamte Spiel von FizzBuzz spielt, von 1 bis 100, ein Macro-Benchmark.
Im Allgemeinen ist es am besten, auf der niedrigstmöglichen Abstraktionsebene zu testen. Im Falle von Benchmarks macht dies sie sowohl leichter zu pflegen, und es hilft, die Menge an Rauschen in den Messungen zu reduzieren. Allerdings können genau wie End-to-End-Tests, die für eine Überprüfung der gesamten Systemzusammenstellung sehr hilfreich sein können, Macro-Benchmarks sehr nützlich sein, um sicherzustellen, dass die kritischen Pfade durch Ihre Software performant bleiben.
Benchmarking in Python
Die zwei populären Optionen für Benchmarking in Python sind: pytest-benchmark und airspeed velocity (asv)
pytest-benchmark ist ein leistungsstarkes Benchmarking-Tool,
das in das beliebte pytest Testing Framework integriert ist.
Es ermöglicht Entwicklern, die Leistung ihres Codes zu messen und zu vergleichen, indem Benchmarks parallel zu ihren Unit-Tests durchgeführt werden.
Benutzer können ihre Benchmark-Ergebnisse einfach lokal vergleichen
und ihre Ergebnisse in verschiedenen Formaten exportieren, wie zum Beispiel JSON.
airspeed velocity (asv) ist ein weiteres fortschrittliches Benchmarking-Tool im Python-Ökosystem.
Einer der Hauptvorteile von asv ist seine Fähigkeit, detaillierte und interaktive HTML-Berichte zu erstellen,
die es einfach machen, Leistungstrends zu visualisieren und Regressionen zu identifizieren.
Außerdem unterstützt asv von Haus aus Relatives Kontinuierliches Benchmarking.
Beide werden von Bencher unterstützt.
Warum also pytest-benchmark wählen?
pytest-benchmark integriert sich nahtlos mit pytest,
das de facto Standard-Unit-Test-Framework im Python-Ökosystem.
Ich würde vorschlagen, pytest-benchmark zu verwenden, um die Latenz Ihres Codes zu benchmarken,
insbesondere wenn Sie bereits pytest verwenden.
pytest-benchmark eignet sich hervorragend zum Messen der Echtzeit.
Refaktorisierung von FizzBuzz
Um unsere FizzBuzz-Anwendung zu testen, müssen wir unsere Logik von der Hauptexecution unseres Programms entkoppeln. Benchmark-Testumgebungen können die Hauptexecution nicht benchmarken. Um dies zu erreichen, müssen wir ein paar Änderungen vornehmen.
Lassen Sie uns unsere FizzBuzz-Logik in ein paar Funktionen umstrukturieren:
def play_game(n, should_print): result = fizz_buzz(n) if should_print: print(result) return result
def fizz_buzz(n): if n % 15 == 0: return "FizzBuzz" elif n % 3 == 0: return "Fizz" elif n % 5 == 0: return "Buzz" else: return str(n)play_game: Nimmt eine ganze Zahln, ruftfizz_buzzmit dieser Zahl auf, und wennshould_printTrueist, wird das Ergebnis gedruckt.fizz_buzz: Nimmt eine ganze Zahlnund führt die tatsächlicheFizz,Buzz,FizzBuzzoder Zahlen-Logik aus und gibt das Ergebnis als String zurück.
Aktualisieren Sie dann die Hauptexecution, sodass sie folgendermaßen aussieht:
for i in range(1, 101): play_game(i, True)Die Hauptexecution unseres Programms iteriert durch die Zahlen von 1 bis 100 inklusive und ruft play_game für jede Zahl auf, wobei should_print auf True gesetzt ist.
Benchmarking von FizzBuzz
Um unseren Code zu benchmarken, müssen wir eine Testfunktion erstellen, die unseren Benchmark ausführt. Fügen Sie am Ende von game.py den folgenden Code hinzu:
def test_game(benchmark): def run_game(): for i in range(1, 101): play_game(i, False) benchmark(run_game)- Erstellen Sie eine Funktion namens
test_game, die einpytest-benchmark-benchmark-Fixture annimmt. - Erstellen Sie eine Funktion
run_game, die von1bis einschließlich100iteriert.- Rufen Sie für jede Zahl
play_gameauf, wobeishould_printaufFalsegesetzt ist.
- Rufen Sie für jede Zahl
- Übergeben Sie die
run_game-Funktion dembenchmark-Runner.
Nun müssen wir unser Projekt so konfigurieren, dass es unsere Benchmarks ausführt.
Erstellen Sie eine neue virtuelle Umgebung mit pipenv:
$ pipenv shell
Creating a Pipfile for this project...Launching subshell in virtual environment... source /usr/bencher/.local/share/virtualenvs/test-xnizGmtA/bin/activateInstallieren Sie pytest-benchmark in dieser neuen pipenv-Umgebung:
$ pipenv install pytest-benchmarkCreating a Pipfile for this project...Installing pytest-benchmark...Resolving pytest-benchmark...Added pytest-benchmark to Pipfile's [packages] ...✔ Installation SucceededPipfile.lock not found, creating...Locking [packages] dependencies...Building requirements...Resolving dependencies...✔ Success!Locking [dev-packages] dependencies...Updated Pipfile.lock (be953321071292b6175f231c7e2e835a3cd26169a0d52b7b781b344d65e8cce3)!Installing dependencies from Pipfile.lock (e8cce3)...Jetzt sind wir bereit, unseren Code zu benchmarken: Führen Sie pytest game.py aus:
$ pytest game.py======================================================= test session starts ========================================================platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)rootdir: /usr/bencher/examples/python/pytest_benchmarkplugins: benchmark-4.0.0collected 1 item
game.py . [100%]
------------------------------------------------- benchmark: 1 tests -------------------------------------------------Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations----------------------------------------------------------------------------------------------------------------------test_game 10.5416 237.7499 10.8307 1.3958 10.7088 0.1248 191;10096 92.3304 57280 1----------------------------------------------------------------------------------------------------------------------
Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean======================================================== 1 passed in 1.68s =========================================================🐰 Lass uns den Beat verwandeln! Wir haben unsere ersten Benchmark-Metriken!
Schließlich können wir unsere müden Entwicklerköpfe ausruhen… Nur ein Scherz, unsere Benutzer wollen ein neues Feature!
Schreiben Sie FizzBuzzFibonacci in Python
Unsere Key Performance Indicators (KPIs) sind gesunken, also möchte unser Product Manager (PM), dass wir ein neues Feature hinzufügen. Nach viel Brainstorming und vielen Nutzerinterviews wurde beschlossen, dass der altbekannte FizzBuzz nicht ausreicht. Die Kinder von heute möchten ein neues Spiel, FizzBuzzFibonacci.
Die Regeln für FizzBuzzFibonacci lauten wie folgt:
Schreibe ein Programm, welches die Zahlen von
1bis100(inklusiv) ausgibt:
- Für Vielfache von drei, drucke
Fizz- Für Vielfache von fünf, drucke
Buzz- Für Vielfache von sowohl drei als auch fünf, drucke
FizzBuzz- Für Zahlen, die Teil der Fibonacci-Sequenz sind, drucke nur
Fibonacci- Für alle anderen, drucke die Zahl
Die Fibonacci-Sequenz ist eine Sequenz, bei der jede Zahl die Summe der beiden vorhergehenden Zahlen ist.
Zum Beispiel wäre, beginnend bei 0 und 1, die nächste Zahl in der Fibonacci-Sequenz 1.
Gefolgt von: 2, 3, 5, 8 und so weiter.
Zahlen, die Teil der Fibonacci-Sequenz sind, werden als Fibonacci-Zahlen bezeichnet. Daher müssen wir eine Funktion schreiben, die Fibonacci-Zahlen erkennt.
Es gibt viele Arten, die Fibonacci-Sequenz zu schreiben und ebenso viele Möglichkeiten, eine Fibonacci-Zahl zu erkennen. Daher werden wir meine Lieblingsmethode wählen:
def is_fibonacci_number(n): for i in range(n + 1): previous, current = 0, 1 while current < i: next_value = previous + current previous = current current = next_value if current == n: return True return False- Erstellen Sie eine Funktion namens
is_fibonacci_number, die eine ganze Zahl entgegennimmt und einen booleschen Wert zurückgibt. - Iterieren Sie für alle Zahlen von
0bis zu unserer gegebenen Zahlninklusive. - Initialisieren Sie unsere Fibonacci-Sequenz, beginnend mit
0und1alspreviousundcurrentZahlen. - Iterieren Sie, während die
currentZahl kleiner ist als die aktuelle Iterationi. - Addieren Sie die
previousundcurrentZahl, um dienext_valueZahl zu erhalten. - Aktualisieren Sie die
previousZahl auf diecurrentZahl. - Aktualisieren Sie die
currentZahl auf dienext_valueZahl. - Sobald
currentgrößer oder gleich der gegebenen Zahlnist, beenden wir die Schleife. - Prüfen Sie, ob die
currentZahl gleich der gegebenen Zahlnist, und falls ja, geben SieTruezurück. - Andernfalls geben Sie
Falsezurück.
Jetzt müssen wir unsere fizz_buzz Funktion aktualisieren:
def fizz_buzz_fibonacci(n): if is_fibonacci_number(n): return "Fibonacci" elif n % 15 == 0: return "FizzBuzz" elif n % 3 == 0: return "Fizz" elif n % 5 == 0: return "Buzz" else: return str(n)- Benennen Sie die
fizz_buzzFunktion infizz_buzz_fibonaccium, um sie genauer zu beschreiben. - Rufen Sie unsere
is_fibonacci_numberHilfsfunktion auf. - Wenn das Ergebnis von
is_fibonacci_numberTrueist, geben SieFibonaccizurück. - Wenn das Ergebnis von
is_fibonacci_numberFalseist, führen Sie die gleicheFizz,Buzz,FizzBuzzoder Zahlenlogik aus und geben das Ergebnis zurück.
Da wir fizz_buzz in fizz_buzz_fibonacci umbenannt haben, müssen wir auch unsere play_game Funktion aktualisieren:
def play_game(n, should_print): result = fizz_buzz_fibonacci(n) if should_print: print(result) return resultSowohl unsere Hauptexekution als auch die test_game Funktion können genau gleich bleiben.
Benchmarking FizzBuzzFibonacci
Nun können wir unseren Benchmark erneut durchführen:
$ pytest game.py======================================================= test session starts ========================================================platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)rootdir: /usr/bencher/examples/python/pytest_benchmarkplugins: benchmark-4.0.0collected 1 item
game.py . [100%]
--------------------------------------------------- benchmark: 1 tests --------------------------------------------------Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations-------------------------------------------------------------------------------------------------------------------------test_game 726.9592 848.2919 735.5682 13.4925 731.4999 4.7078 146;192 1.3595 1299 1-------------------------------------------------------------------------------------------------------------------------
Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean======================================================== 1 passed in 1.97s =========================================================Wenn wir durch unsere Terminal-Historie zurückblättern,
können wir einen ungefähren Vergleich zwischen der Leistung unserer FizzBuzz- und FizzBuzzFibonacci-Spiele machen: 10.8307 us vs 735.5682 us.
Ihre Zahlen werden ein wenig anders sein als meine.
Der Unterschied zwischen den beiden Spielen liegt jedoch wahrscheinlich im Bereich von 50x.
Das erscheint mir gut! Besonders wenn man bedenkt, dass wir ein so beeindruckend klingendes Feature wie Fibonacci zu unserem Spiel hinzugefügt haben.
Die Kinder werden es lieben!
FizzBuzzFibonacci in Python erweitern
Unser Spiel ist ein Hit! Die Kinder lieben es tatsächlich, FizzBuzzFibonacci zu spielen.
So sehr, dass das Management beschlossen hat, dass sie eine Fortsetzung wollen.
Aber das ist die moderne Welt, wir brauchen jährliche wiederkehrende Einnahmen (ARR), keine Einmalkäufe!
Die neue Vision für unser Spiel ist, dass es offen gestaltet ist, keine Begrenzung mehr zwischen 1 und 100 (selbst wenn sie inklusive ist).
Nein, wir bewegen uns zu neuen Horizonten!
Die Regeln für Open World FizzBuzzFibonacci lauten wie folgt:
Schreiben Sie ein Programm, das jede positive ganze Zahl akzeptiert und ausgibt:
- Für Vielfache von drei, drucken Sie
Fizz- Für Vielfache von fünf, drucken Sie
Buzz- Für Vielfache von drei und fünf, drucken Sie
FizzBuzz- Für Zahlen, die Teil der Fibonacci-Sequenz sind, drucken Sie nur
Fibonacci- Für alle anderen drucken Sie die Zahl
Um unser Spiel für jede beliebige Zahl spielbar zu machen, müssen wir ein Kommandozeilenargument akzeptieren. Aktualisieren Sie die Hauptroutine, um folgendermaßen auszusehen:
import sys
args = sys.argvif len(args) > 1 and args[1].isdigit(): i = int(args[1]) play_game(i, True)else: print("Please, enter a positive integer to play...")- Importieren Sie das
sys-Paket. - Sammeln Sie alle Argumente (
args), die über die Kommandozeile an unser Spiel übergeben werden. - Nehmen Sie das erste Argument, das an unser Spiel übergeben wird, und prüfen Sie, ob es eine Ziffer ist.
- Wenn ja, parsen Sie das erste Argument als Ganzzahl,
i. - Spielen Sie unser Spiel mit der neu geparsten Ganzzahl
i.
- Wenn ja, parsen Sie das erste Argument als Ganzzahl,
- Falls das Parsen fehlschlägt oder kein Argument übergeben wird, fordern Sie standardmäßig eine gültige Eingabe an.
Jetzt können wir unser Spiel mit jeder Zahl spielen!
Führen Sie python game.py gefolgt von einer Ganzzahl aus, um unser Spiel zu spielen:
$ python game.py 9Fizz$ python game.py 10Buzz$ python game.py 13FibonacciUnd wenn wir eine Zahl weglassen oder eine ungültige Zahl angeben:
$ python game.pyPlease, enter a positive integer to play...$ python game.py badPlease, enter a positive integer to play...Wow, das war ein gründlicher Test! CI besteht. Unsere Chefs sind begeistert. Lassen Sie es uns veröffentlichen! 🚀
Das Ende


🐰 … das Ende Ihrer Karriere vielleicht?
Nur ein Scherz! Alles steht in Flammen! 🔥
Nun, anfangs schien alles gut zu laufen. Und dann um 02:07 Uhr am Samstag löste mein Pager aus:
📟 Dein Spiel steht in Flammen! 🔥
Nachdem ich aus dem Bett gehetzt war, versuchte ich herauszufinden, was los war. Ich versuchte, die Logs durchzusuchen, aber das war schwierig, weil alles ständig abstürzte. Schließlich fand ich das Problem. Die Kinder! Sie liebten unser Spiel so sehr, dass sie es bis zu einer Million hochspielten! In einem Geistesblitz fügte ich zwei neue Benchmarks hinzu:
def test_game_100(benchmark): def run_game(): play_game(100, False) benchmark(run_game)
def test_game_1_000_000(benchmark): def run_game(): play_game(1_000_000, False) benchmark(run_game)- Ein Mikro-Benchmark
test_game_100zum Spielen des Spiels mit der Zahl einhundert (100) - Ein Mikro-Benchmark
test_game_1_000_000zum Spielen des Spiels mit der Zahl eine Million (1_000_000)
Als ich es ausführte, bekam ich das:
$ pytest game.py======================================================= test session starts ========================================================platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)rootdir: /usr/bencher/examples/python/pytest_benchmarkplugins: benchmark-4.0.0collected 3 items
game.py ... [100%]Warten Sie… warten Sie…
-------------------------------------------------------------------------------------------------- benchmark: 3 tests --------------------------------------------------------------------------------------------------Name (time in us) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------test_game_100 15.4166 (1.0) 112.8749 (1.0) 15.8470 (1.0) 1.1725 (1.0) 15.6672 (1.0) 0.1672 (1.0) 1276;7201 63,103.3078 (1.0) 58970 1test_game 727.0002 (47.16) 1,074.3327 (9.52) 754.3231 (47.60) 33.2047 (28.32) 748.9999 (47.81) 33.7283 (201.76) 134;54 1,325.6918 (0.02) 1319 1test_game_1_000_000 565,232.3328 (>1000.0) 579,829.1252 (>1000.0) 571,684.6334 (>1000.0) 6,365.1577 (>1000.0) 568,294.3747 (>1000.0) 10,454.0113 (>1000.0) 2;0 1.7492 (0.00) 5 1------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean======================================================== 3 passed in 7.01s =========================================================Was! 15.8470 us x 1,000 sollte 15,847.0 us sein, nicht 571,684.6334 us 🤯
Obwohl mein Fibonacci-Sequenzcode funktional korrekt ist, muss irgendwo ein Leistungsproblem vorliegen.
Fix FizzBuzzFibonacci in Python
Werfen wir einen weiteren Blick auf die is_fibonacci_number-Funktion:
def is_fibonacci_number(n): for i in range(n + 1): previous, current = 0, 1 while current < i: next_value = previous + current previous = current current = next_value if current == n: return True return FalseJetzt, wo ich über die Leistung nachdenke, merke ich, dass ich eine unnötige, zusätzliche Schleife habe. Wir können die Schleife for i in range(n + 1): komplett entfernen und einfach den Wert current mit der gegebenen Zahl (n) vergleichen 🤦
def is_fibonacci_number(n): previous, current = 0, 1 while current < n: next_value = previous + current previous = current current = next_value return current == n- Aktualisieren Sie unsere
is_fibonacci_number-Funktion. - Initialisieren Sie unsere Fibonacci-Sequenz beginnend mit
0und1als diepreviousundcurrentZahlen. - Iterieren Sie, während die
currentZahl kleiner als die gegebene Zahlnist. - Fügen Sie die
previousundcurrentZahl hinzu, um dienext_valueZahl zu erhalten. - Aktualisieren Sie die
previousZahl zu dercurrentZahl. - Aktualisieren Sie die
currentZahl zu dernext_valueZahl. - Sobald
currentgrößer oder gleich der gegebenen Zahlnist, wird die Schleife beendet. - Überprüfen Sie, ob die
currentZahl gleich der gegebenen Zahlnist und geben Sie das Ergebnis zurück.
Lassen Sie uns nun diese Benchmarks erneut ausführen und sehen, wie wir abgeschnitten haben:
$ pytest game.py======================================================= test session starts ========================================================platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)rootdir: /usr/bencher/examples/python/pytest_benchmarkplugins: benchmark-4.0.0collected 3 items
game.py ... [100%]
------------------------------------------------------------------------------------------------ benchmark: 3 tests ------------------------------------------------------------------------------------------------Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Kops/s) Rounds Iterations--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------test_game_100 309.8685 (1.0) 40,197.8614 (2.38) 322.0815 (1.0) 101.7570 (1.0) 320.2877 (1.0) 5.1805 (1.0) 321;12616 3,104.8046 (1.0) 195120 16test_game_1_000_000 724.9881 (2.34) 16,912.4920 (1.0) 753.1445 (2.34) 121.0458 (1.19) 741.7053 (2.32) 12.4797 (2.41) 656;13698 1,327.7664 (0.43) 123073 10test_game 26,958.9946 (87.00) 129,667.1107 (7.67) 27,448.7719 (85.22) 1,555.0003 (15.28) 27,291.9424 (85.21) 165.7754 (32.00) 479;2372 36.4315 (0.01) 25918 1--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean======================================================== 3 passed in 3.99s =========================================================Oh, wow! Unser test_game Benchmark ist wieder etwa dort, wo er ursprünglich für FizzBuzz war.
Ich wünschte, ich könnte mich genau erinnern, was dieser Wert war. Aber es ist nun drei Wochen her.
Mein Terminalverlauf geht nicht so weit zurück.
Und pytest-benchmark speichert seine Ergebnisse nur, wenn wir es darum bitten.
Aber ich denke, es ist nah dran!
Der test_game_100 Benchmark ist fast 50x auf 322.0815 ns gesunken.
Und der test_game_1_000_000 Benchmark ist mehr als 500.000x gesunken! 571,684,633.4 ns zu 753.1445 ns!
🐰 Hey, wenigstens haben wir diesen Leistungsfehler entdeckt, bevor es in die Produktion gelangt… oh, richtig. Schon gut…
Leistungsverschlechterungen in CI auffangen
Die Geschäftsführung war nicht glücklich über die Flut von negativen Bewertungen, die unser Spiel aufgrund meines kleinen Performance-Bugs erhalten hat. Sie sagten mir, dass so etwas nicht wieder passieren darf, und als ich fragte wie, sagten sie mir einfach, dass ich es einfach nicht noch einmal tun soll. Wie soll ich das schaffen‽
Zum Glück habe ich dieses großartige Open-Source-Tool namens Bencher gefunden. Es gibt einen sehr großzügigen kostenlosen Tier, sodass ich Bencher Cloud einfach für meine persönlichen Projekte verwenden kann. Und bei der Arbeit, wo alles in unserer privaten Cloud sein muss, habe ich angefangen, Bencher Self-Hosted zu verwenden.
Bencher hat eingebaute Adapter, also ist es einfach, es in CI zu integrieren. Nachdem ich der Quick Start Anleitung gefolgt bin, kann ich meine Benchmarks ausführen und mit Bencher verfolgen.
$ bencher run --adapter python_pytest --file results.json "pytest --benchmark-json results.json game.py"======================================================= test session starts ========================================================platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)rootdir: /usr/bencher/examples/python/pytest_benchmarkplugins: benchmark-4.0.0collected 3 items
game.py ...
...
View results:- test_game (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=077449e5-5b45-4c00-bdfb-3a277413180d&start_time=1697224006000&end_time=1699816009000&upper_boundary=true- test_game_100 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=96508869-4fa2-44ac-8e60-b635b83a17b7&start_time=1697224006000&end_time=1699816009000&upper_boundary=true- test_game_1_000_000 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=ff014217-4570-42ea-8813-6ed0284500a4&start_time=1697224006000&end_time=1699816009000&upper_boundary=trueMit diesem praktischen Zeitreise-Gerät, das mir ein netter Hase gegeben hat, konnte ich zurück in die Vergangenheit reisen und nachspielen, was passiert wäre, wenn wir von Anfang an Bencher verwendet hätten. Sie können sehen, wo wir die fehlerhafte FizzBuzzFibonacci Implementierung zum ersten Mal gepusht haben. Ich habe sofort Fehler in CI als Kommentar zu meinem Pull Request bekommen. Noch am gleichen Tag habe ich den Performance-Fehler behoben, indem ich diese unnötige, zusätzliche Schleife entfernt habe. Keine Brände. Nur zufriedene Benutzer.
Bencher: Kontinuierliches Benchmarking
Bencher ist eine Suite von kontinuierlichen Benchmarking-Tools. Hatten Sie jemals eine Performance Regression, die Ihre Nutzer beeinflusste? Bencher hätte das verhindern können. Bencher ermöglicht es Ihnen, Leistungsregressionen vorher zu erkennen und zu verhindern, bevor sie in die Produktion gelangen.
- Ausführen: Führen Sie Ihre Benchmarks lokal oder in CI mit Ihren bevorzugten Benchmarking-Tools aus. Das
bencherCLI umfasst einfach Ihr vorhandenes Benchmark-Harness und speichert die Ergebnisse. - Verfolgen: Verfolgen Sie die Ergebnisse Ihrer Benchmarks im Laufe der Zeit. Überwachen, abfragen und grafisch darstellen der Ergebnisse mit der Bencher Web Konsole auf Basis des Quellzweigs, Testbetts und Maßnahme.
- Auffangen: Fangen Sie Leistungsregressionen in CI ab. Bencher verwendet modernste, anpassbare Analysen, um Leistungsregressionen zu erkennen, bevor sie in die Produktion gelangen.
Aus denselben Gründen, warum Unit Tests in CI laufen, um Feature Regressionen zu verhindern, sollten Benchmarks in CI mit Bencher ausgeführt werden, um Leistungsregressionen zu verhindern. Performance-Bugs sind Fehler!
Beginnen Sie damit, Leistungsregressionen in CI aufzufangen - probieren Sie Bencher Cloud kostenlos aus.