====== Laufzeit messen: Performance und WCET ======
Vermutungen über Laufzeit sind fast immer falsch. Lyx macht das Messen einfach: eine monotone Uhr in der Standardbibliothek, statische Analysen im Compiler, und Binaries ohne Laufzeitsystem, das die Messung verfälscht.
→ [[lyx_-_programmiersprache:guides:fehlersuche|Fehlersuche]] · [[lyx_-_programmiersprache:sprache:attributes-pragmas|Attribute & Pragmas]] · [[lyx_-_programmiersprache:guides:do-178c|DO-178C]]
----
===== Die richtige Uhr =====
var t0: int64 := GetTimeNs(CLOCK_MONOTONIC);
… Arbeit …
var t1: int64 := GetTimeNs(CLOCK_MONOTONIC);
^ Uhr ^ Wofür ^
| ''CLOCK_MONOTONIC'' | Zeitspannen — springt nicht, wenn die Systemzeit gestellt wird |
| ''CLOCK_MONOTONIC_RAW'' | dasselbe, ohne NTP-Feinregelung |
| ''CLOCK_PROCESS_CPUTIME_ID'' | Rechenzeit des Prozesses statt Wanduhr |
| ''CLOCK_THREAD_CPUTIME_ID'' | Rechenzeit eines Threads |
| ''CLOCK_REALTIME'' | Zeitpunkte, **nicht** Zeitspannen |
Für Zeitstempel im Alltag gibt es ''NowMs()'' und ''NowUs()'' aus ''std.time''; zum Messen ist die monotone Uhr die richtige Wahl.
----
===== Beispiel: zwei Fassungen vergleichen =====
import std.io;
import std.alloc;
import std.time;
// Zwei Fassungen derselben Aufgabe: Summe eines Feldes
fn SummeEinfach(a: int64, n: int64): int64 {
var s: int64 := 0;
var i: int64 := 0;
while (i < n) { s := s + peek64(a + i * 8); i := i + 1; }
return s;
}
// Vierfach abgerollt — dieselbe Rechnung, weniger Schleifenkopf
fn SummeAbgerollt(a: int64, n: int64): int64 {
var s: int64 := 0;
var i: int64 := 0;
var grenze: int64 := n - (n - (n / 4) * 4);
while (i < grenze) {
s := s + peek64(a + i * 8)
+ peek64(a + (i + 1) * 8)
+ peek64(a + (i + 2) * 8)
+ peek64(a + (i + 3) * 8);
i := i + 4;
}
while (i < n) { s := s + peek64(a + i * 8); i := i + 1; }
return s;
}
fn Messen(name: pchar, wert: int64, ns: int64): void {
PrintLn(StrConcat(StrConcat(name, ": Summe="), StrConcat(IntToStr(wert),
StrConcat(", ", StrConcat(IntToStr(ns / 1000000), " ms")))));
}
fn main(): int64 {
var n: int64 := 2000000;
var a: int64 := allocZeroed(n * 8);
var i: int64 := 0;
while (i < n) { poke64(a + i * 8, i); i := i + 1; }
// Monotone Uhr: sie springt nicht, wenn die Systemzeit gestellt wird
var t0: int64 := GetTimeNs(CLOCK_MONOTONIC);
var s1: int64 := SummeEinfach(a, n);
var t1: int64 := GetTimeNs(CLOCK_MONOTONIC);
var s2: int64 := SummeAbgerollt(a, n);
var t2: int64 := GetTimeNs(CLOCK_MONOTONIC);
Messen("einfach ", s1, t1 - t0);
Messen("abgerollt", s2, t2 - t1);
free(a, n * 8);
return 0;
}
Zwei Läufe hintereinander, gemessen mit ''lyxc 1.1.3I'':
einfach : Summe=1999999000000, 24 ms
abgerollt: Summe=1999999000000, 21 ms
einfach : Summe=1999999000000, 22 ms
abgerollt: Summe=1999999000000, 15 ms
Zwei Dinge sind daran wichtiger als die Zahlen:
* **Beide Fassungen liefern dasselbe Ergebnis.** Eine schnellere Fassung, die etwas anderes rechnet, ist keine schnellere Fassung.
* **Die Werte streuen** — 24/21 gegen 22/15 ms. Ein einzelner Lauf beweist nichts; erst mehrere Läufe zeigen, ob ein Unterschied echt ist.
----
===== Was der Compiler ohne Ausführung sagt =====
lyxc prog.lyx --static-analysis --call-graph -o prog
=== Aufrufgraph ===
Funktionen: 2, Aufrufkanten: 1
keine Rekursion
von main nicht erreichbar: 0
=== Statische Analyse: Stapelbedarf ===
Regel: 0 lokale Variablen, ca. 0 Byte Rahmen
main: 1 lokale Variablen, ca. 8 Byte Rahmen
groesster Rahmen: ca. 8 Byte
* **''--call-graph''** beantwortet die WCET-Vorfragen: Gibt es Rekursion? Ist etwas unerreichbar (also toter Code im Nachweis)?
* **''--static-analysis''** nennt den Stapelbedarf je Funktion — die Grundlage für ''@stack_limit(N)''.
* **''--mcdc''** zeigt, welche Bedingungen strukturell nicht abgedeckt sind.
----
===== Vorhersagbarkeit statt Geschwindigkeit =====
Für sicherheitskritischen Code zählt nicht der Mittelwert, sondern die **obere Schranke**. Die Sprachmittel dafür:
^ Mittel ^ Zusage ^
| ''@wcet(N)'' | Diese Funktion soll in ''N'' Zeiteinheiten fertig sein |
| ''@stack_limit(N)'' | Ihr Rahmen bleibt unter ''N'' Byte; Rekursion ohne diese Angabe ist verboten |
| ''@flight_crit'' | Kein ''new''/''alloc'', keine lokalen Felder, FPU-Traps für NaN/Inf |
| ''limit(N)'' an Schleifen | Obergrenze der Durchläufe steht im Quelltext |
> **''@wcet(N)'' ist seit lyxc 1.1.4A wieder unauffällig** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1704|#1704]], nachgemessen): ''F(20) + 1'' liefert ''42''.
>
> Bis 1.1.3O verlor der Aufruf einer so annotierten Funktion **innerhalb eines Ausdrucks** den Rückgabetyp und rechnete als ''f64'' — heraus kam ein Bitmuster. Wer mit einem älteren Compiler arbeitet, weist das Ergebnis erst einer Variablen zu (''var v: int64 := F(20);''). ''@stack_limit'', ''@dal'' und ''@flight_crit'' waren nie betroffen.
----
===== Messen, das etwas taugt =====
- **Erst messen, dann ändern.** Die teure Stelle ist selten die, die man vermutet.
- **Genug Wiederholungen**, damit die Uhr etwas zu messen hat — Millisekunden statt Mikrosekunden.
- **Mehrere Läufe**, und die Streuung ansehen.
- **Gleiche Bedingungen**: dasselbe Binary, dieselbe Maschine, möglichst ohne Last daneben.
- **Ergebnis mitprüfen**, damit die schnellere Fassung noch dasselbe rechnet.
- **''-O0'' zum Gegenprüfen**: Verschwindet der Unterschied, lag er an der Faltung, nicht am Code.
Und der Hinweis aus dem Bestand: Eine Änderung, die 3 ms bringt, aber den Code unlesbar macht, ist ein schlechtes Geschäft — es sei denn, die 3 ms stehen in einer Anforderung.
----
===== Wenn es wirklich langsam ist =====
Die Reihenfolge, in der sich das Suchen lohnt:
* **Algorithmus** — quadratisch statt linear schlägt jede Mikrooptimierung.
* **Speicherzugriffe** — jedes ''alloc'' im Innersten einer Schleife ist ein Kandidat ([[lyx_-_programmiersprache:guides:speicher-praxis|Speicher in der Praxis]]).
* **Syscalls** — I/O in der Schleife statt gepuffert.
* **Erst danach** Schleifenkörper, Abrollen, Zugriffsmuster.
Letzte Aktualisierung: 2026-08-20 — Messungen mit ''lyxc 1.1.3I'' durchgeführt; der dabei gefundene ''@wcet''-Defekt #1704 ist mit ''lyxc 1.1.4A'' nachgemessen behoben.