Wie man Python-Code mit pytest-benchmark benchmarket

Everett Pompeii

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 1 bis 100 (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 1 bis 100, indem Sie eine Range von 101 verwenden.
  • Berechnen Sie für jede Zahl den Modulus (Rest nach Division) sowohl für 3 als auch für 5.
  • Wenn der Rest 0 ist, dann ist die Zahl ein Vielfaches des gegebenen Faktors.
    • Wenn der Rest 0 für 15 ist, dann drucken Sie FizzBuzz.
    • Wenn der Rest 0 für 3 ist, dann drucken Sie Fizz.
    • Wenn der Rest 0 für 5 ist, dann drucken Sie Buzz.
  • 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.py
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 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 Zahl n, ruft fizz_buzz mit dieser Zahl auf, und wenn should_print True ist, wird das Ergebnis gedruckt.
  • fizz_buzz: Nimmt eine ganze Zahl n und führt die tatsächliche Fizz, Buzz, FizzBuzz oder Zahlen-Logik aus und gibt das Ergebnis als String zurück.

Aktualisieren Sie dann die Hauptexecution, sodass sie folgendermaßen aussieht:

game.py
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 ein pytest-benchmark-benchmark-Fixture annimmt.
  • Erstellen Sie eine Funktion run_game, die von 1 bis einschließlich 100 iteriert.
    • Rufen Sie für jede Zahl play_game auf, wobei should_print auf False gesetzt ist.
  • Übergeben Sie die run_game-Funktion dem benchmark-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/activate

Installieren Sie pytest-benchmark in dieser neuen pipenv-Umgebung:

$ pipenv install pytest-benchmark
Creating a Pipfile for this project...
Installing pytest-benchmark...
Resolving pytest-benchmark...
Added pytest-benchmark to Pipfile's [packages] ...
✔ Installation Succeeded
Pipfile.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.0
benchmark: 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_benchmark
plugins: benchmark-4.0.0
collected 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 1 bis 100 (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 0 bis zu unserer gegebenen Zahl n inklusive.
  • Initialisieren Sie unsere Fibonacci-Sequenz, beginnend mit 0 und 1 als previous und current Zahlen.
  • Iterieren Sie, während die current Zahl kleiner ist als die aktuelle Iteration i.
  • Addieren Sie die previous und current Zahl, um die next_value Zahl zu erhalten.
  • Aktualisieren Sie die previous Zahl auf die current Zahl.
  • Aktualisieren Sie die current Zahl auf die next_value Zahl.
  • Sobald current größer oder gleich der gegebenen Zahl n ist, beenden wir die Schleife.
  • Prüfen Sie, ob die current Zahl gleich der gegebenen Zahl n ist, und falls ja, geben Sie True zurück.
  • Andernfalls geben Sie False zurü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_buzz Funktion in fizz_buzz_fibonacci um, um sie genauer zu beschreiben.
  • Rufen Sie unsere is_fibonacci_number Hilfsfunktion auf.
  • Wenn das Ergebnis von is_fibonacci_number True ist, geben Sie Fibonacci zurück.
  • Wenn das Ergebnis von is_fibonacci_number False ist, führen Sie die gleiche Fizz, Buzz, FizzBuzz oder 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 result

Sowohl 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.0
benchmark: 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_benchmark
plugins: benchmark-4.0.0
collected 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:

game.py
import sys
args = sys.argv
if 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.
  • 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 9
Fizz
$ python game.py 10
Buzz
$ python game.py 13
Fibonacci

Und wenn wir eine Zahl weglassen oder eine ungültige Zahl angeben:

$ python game.py
Please, enter a positive integer to play...
$ python game.py bad
Please, 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


SpongeBob Schwammkopf Drei Wochen später
Dies ist in Ordnung Meme

🐰 … 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_100 zum Spielen des Spiels mit der Zahl einhundert (100)
  • Ein Mikro-Benchmark test_game_1_000_000 zum 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.0
benchmark: 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_benchmark
plugins: benchmark-4.0.0
collected 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 1
test_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 1
test_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 False

Jetzt, 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 0 und 1 als die previous und current Zahlen.
  • Iterieren Sie, während die current Zahl kleiner als die gegebene Zahl n ist.
  • Fügen Sie die previous und current Zahl hinzu, um die next_value Zahl zu erhalten.
  • Aktualisieren Sie die previous Zahl zu der current Zahl.
  • Aktualisieren Sie die current Zahl zu der next_value Zahl.
  • Sobald current größer oder gleich der gegebenen Zahl n ist, wird die Schleife beendet.
  • Überprüfen Sie, ob die current Zahl gleich der gegebenen Zahl n ist 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.0
benchmark: 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_benchmark
plugins: benchmark-4.0.0
collected 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 16
test_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 10
test_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.0
benchmark: 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_benchmark
plugins: benchmark-4.0.0
collected 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=true

Mit 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

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

🤖 Dieses Dokument wurde automatisch von OpenAI GPT-4 generiert. Es ist möglicherweise nicht korrekt und kann Fehler enthalten. Wenn Sie Fehler finden, öffnen Sie bitte ein Problem auf GitHub.