====== 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.