Timing Ihrer Bash-Skripts und -Prozeduren aus dem Code heraus
Im Allgemeinen kann man das Zeit-Bash-Dienstprogramm (siehe Man Time für weitere Informationen) verwenden, um ein Programm auszuführen und Zusammenfassungen der Laufzeitdauer und der Systemressourcennutzung zu erhalten. Aber wie kann man bestimmte Codeabschnitte direkt aus dem Bash-Quellcode heraus herstellen?
Mit einigen einfachen Variablenzuweisungen und -berechnungen ist es möglich, genaue Timing-Metriken für die Ausführung von Bash-Skripten zu erzielen.
In diesem Tutorial lernen Sie:
- So timen Sie Bash-Skripte mithilfe von Variablenzuweisungen und Berechnungen
- So verwenden Sie überlappende Timer, um bestimmte Abschnitte Ihrer Skripte zu timen
- Beispiele, die veranschaulichen, wie bestimmte Codeabschnitte zeitlich abgestimmt werden können
Timing der Ausführung des Bash-Skripts
Verwendete Softwareanforderungen und Konventionen
Grundlagen des Datums
Wir werden den Befehl date für unsere Zeitangaben verwenden. Konkret verwenden wir das Datum +%s, um die Zeit in Sekunden seit der Epoche zu erhalten, d.h. die Anzahl der Sekunden seit 1970-01-01 00:00:00 UTC.
date +%s
1607481317
Der Befehl date kann auch eine Genauigkeit von Nanosekunden (000000000.999999999) liefern, wenn Ihre Zeitangaben supergenau sein müssen:
date +%s%N
1607488248328243029
Die Diskussion über die Implementierung von Nanosekunden-präzisen Timern würde den Rahmen dieses Artikels sprengen, aber bitte lassen Sie uns wissen, wenn dieses Thema Sie interessiert. Das Setup wäre dem unten gezeigten Setup sehr ähnlich, mit ein paar zusätzlichen Berechnungen und Vorkehrungen, um die Sekunden im Vergleich zu den Millisekunden usw. zu handhaben.
Beispiel 1: Ein einfaches Timing-Beispiel
Beginnen wir mit einem einfachen Beispiel, bei dem wir einen einzelnen Befehl, nämlich sleep 1, mit zwei date +%s-Befehlen und einer Variablenzuweisung messen. Speichern Sie das folgende Skript in einer Datei mit dem Namen test.sh:
#!/bin/bash
START="$(date +%s)"
sleep 1
DURATION=$[ $(date +%s) - ${START} ]
echo ${DURATION}
Hier geben wir zunächst an, dass das Skript als Bash-Code ausgeführt werden soll, indem wir die Auswahl des #!/bin/bash-Interpreters verwenden. Wir haben auch chmod +x ./test.sh ausgeführt, um das Skript nach der Erstellung ausführbar zu machen.
Als nächstes setzen wir die Variable START auf die aktuellen Sekunden seit der Epochenzeit, indem wir eine Subshell aufrufen (wie durch $ (...)) anzeigen und innerhalb dieser Subshell führen wir das Datum +%s aus. Anschließend verwenden wir die Schlaffunktion, um unser Skript für eine Sekunde zu pausieren. Beachten Sie, dass der Schlaf 1 Ihren eigentlichen Programmcode ersetzen kann, d.h. den Teil, den Sie timen möchten.
Schließlich setzen wir eine neue Variable DURATION, indem wir eine Berechnung durchführen (wie durch $[ ... ]) angedeutet – nämlich dass wir die aktuellen Sekunden seit der Epoche nehmen (wieder mit dem Datum +%s aus einer Subshell) und dann die START-Zeit davon subtrahieren. Das Ergebnis ist die Anzahl der Sekunden, die seit dem Start vergangen sind.
Wenn wir dieses Skript ausführen, ist die Ausgabe wie erwartet:
./test.sh
1
Beispiel 2: Ein etwas komplexeres Timing-Beispiel
Lassen Sie uns dieses Mal ein wenig expandieren und die Zeitpläne modularer gestalten. test2.sh:
#!/bin/bash
START1="$(date +%s)"
sleep 2
END1="$(date +%s)"
sleep 2
START2="$(date +%s)"
sleep 3
END2="$(date +%s)"
DURATION1=$[ ${END1} - ${START1} ]
DURATION2=$[ ${END2} - ${START2} ]
echo "The 1st part of the code took: ${DURATION1}"
echo "The 2nd part of the code took: ${DURATION2}"
Hier haben wir ein ähnliches Setup wie im ersten Beispiel vorgenommen, aber dieses Mal haben wir zwei verschiedene Befehle zeitlich abgestimmt, indem wir einen doppelten Satz von Variablen verwendet haben, und wir haben die Dinge etwas strukturierter, indem wir eine END-Variable für beide Befehle verwendet haben. Wir hätten die letzten Echozeilen auch wie folgt schreiben können test3.sh:
#!/bin/bash
START1="$(date +%s)"
sleep 2
END1="$(date +%s)"
sleep 2
START2="$(date +%s)"
sleep 3
END2="$(date +%s)"
echo "The 1st part of the code took: $[ ${END1} - ${START1} ]"
echo "The 2nd part of the code took: $[ ${END2} - ${START2} ]"
Da die beiden DURATION-Variablen in gewisser Weise unnötig waren. Sie mögen den Code klarer lesbar gemacht haben, aber sie erfüllen keine wirkliche andere Funktion, im Gegensatz zu den Variablen START und END, die für tatsächliche Berechnungen verwendet werden.
Beachten Sie jedoch, dass wir test4.sh nicht hätten schreiben können:
#!/bin/bash
START1="$(date +%s)"
sleep 2
sleep 2
START2="$(date +%s)"
sleep 3
echo "The 1st part of the code took: $[ $(date +%s) - ${START1} ]"
echo "The 2nd part of the code took: $[ $(date +%s) - ${START2} ]"
Da das in der Subshell erfasste Datum die Zeit ist, zu der das Echo ausgeführt wird, wären die Timings für beide falsch: Die Endtimings hätten stattdessen direkt nach den entsprechenden Befehlen erfolgen sollen.
Vielleicht wäre es für das zweite Timing möglich gewesen, ein Datum +%s direkt im Echo zu verwenden (da die Ausführung des ersten Echos nur einige Millisekunden dauern würde, selbst mit der Unterschale und dem Datum), aber es ist nicht perfekt und würde definitiv nicht funktionieren, wenn ein präzises Timing im Nanosekundenbereich erforderlich wäre. Es ist auch keine saubere Codierung und schwieriger zu lesen/verstehen.
Lassen Sie uns diese Skripte ausführen und die Ausgabe vergleichen:
./test2.sh
The 1st part of the code took: 2
The 2nd part of the code took: 3
./test3.sh
The 1st part of the code took: 2
The 2nd part of the code took: 3
./test4.sh
The 1st part of the code took: 7
The 2nd part of the code took: 3
Die test2.sh und test3.sh meldeten wie erwartet korrekte Timings. Das test4.sh-Skript meldete wie erwartet falsche Timings.
Können Sie sehen, wie lange das Skript insgesamt gelaufen ist, ungefähr in Sekunden, unabhängig von den Timings? Wenn deine Antwort sechs Sekunden dauerte, liegst du richtig. Sie können sehen, wie es in test2.sh und test3.sh einen zusätzlichen Schlaf 2 gibt, der in den Timing-Befehlen nicht erfasst wird. Dies ist ein Beispiel dafür, wie Sie verschiedene Codeabschnitte zeitlich festlegen können.
Beispiel 3: Überlappende Timer
Schauen wir uns nun ein letztes Beispiel an, das überlappende Timer und mal eine Funktion hat. test5.sh:
#!/bin/bash
my_sleep_function(){
sleep 1
}
OVERALL_START="$(date +%s)"
FUNCTION_START="$(date +%s)"
my_sleep_function
FUNCTION_END="$(date +%s)"
sleep 2
OVERALL_END="$(date +%s)"
echo "The function part of the code took: $[ ${FUNCTION_END} - ${FUNCTION_START} ] seconds to run"
echo "The overall code took: $[ ${OVERALL_END} - ${OVERALL_START} ] seconds to run"
Hier definieren wir eine Funktion, my_sleep_function die einfach eine Sekunde lang schläft. Als nächstes setzen wir einen Gesamtstart-Timer, indem wir die Variable OVERALL_START und wieder unser Datum +%s in einer Subshell verwenden. Als nächstes starten wir einen weiteren Timer (den Funktionstimer basierend auf der Variablen FUNCTION_START). Wir führen die Funktion aus und beenden sofort den Funktionstimer, indem wir die Variable FUNCTION_END setzen.
Wir machen dann einen zusätzlichen Schlaf 2 und beenden dann den Gesamttimer, indem wir den OVERALL_END Timer einstellen. Zum Schluss geben wir die Informationen in einem schönen Format gegen Ende des Skripts aus. Die beiden echo-Anweisungen sind nicht Teil des Timings, aber ihre Laufzeit wäre minimal. Normalerweise versuchen wir, verschiedene und spezifische Abschnitte unseres Codes zu timen, die dazu neigen, lange Zeiträume zu haben, wie z.B. umfangreiche Schleifen, externe Programmaufrufe, viele Subshells usw.
Schauen wir uns die Ergebnisse test5.sh an:
./test5.sh
The function part of the code took: 1 seconds to run
The overall code took: 3 seconds to run
Sieht gut aus. Das Skript hat die Funktion korrekt auf 1 Sekunde und die Gesamtlaufzeit des Skripts auf 3 Sekunden festgelegt, wobei es sich um die Kombination aus dem Funktionsaufruf und den zusätzlichen zwei Sekunden Schlaf handelt.
Beachten Sie, dass es sinnvoll sein kann, eine zusätzliche globale Timing-Variable zu verwenden, zu der die Funktionslaufzeit hinzugefügt werden kann, wenn die Funktion rekursiv ist. Sie könnten auch die Anzahl der Funktionsaufrufe zählen und dann am Ende die Anzahl der Funktionsaufrufe mit bc dividieren (siehe How To Make Decimal Calculations In Bash Using Bc). In diesem Anwendungsfall kann es am besten sein, die Start- und Stopp-Timer sowie die Berechnung der Funktionsdauer in die Funktion zu verschieben. Es sorgt für saubereren und klareren Code und kann unnötige Code-Duplizierung vermeiden.
Schlussfolgerung
In diesem Artikel haben wir uns die Zeitsteuerung verschiedener Teile unseres Bash-Skriptcodes angesehen, indem wir das Datum +%s als Grundlage für das Abrufen von Sekunden seit der Epochenzeit sowie eine oder mehrere Variablenzuweisungen zum Berechnen der Leistungszeiten für einen oder mehrere Abschnitte des Codes verwendet haben. Mit diesen Grundbausteinen kann man komplexe Timing-Messstrukturen erstellen, pro Funktion, pro aufgerufenem Skript oder sogar Timer, die sich überlappen (z.B. einer pro Skript sowie einer pro Funktion usw.), indem verschiedene Variablen verwendet werden. Genießen!
Wenn Sie mehr über Bash erfahren möchten, lesen Sie bitte unsere Reihe Nützliche Bash-Befehlszeilen-Tipps und Tricks.
