====== Lyx – Das Energy-Aware Programmiermodell ====== In batteriebetriebenen Systemen, Embedded-Knoten und Rechenzentren ist **Energie** eine knappe Ressource. Maximale Rechenleistung ist nicht immer das Ziel — oft ist "Performance pro Watt" die entscheidende Größe. Ein Temperatursensor, der alle 10 Sekunden misst, muss keine AVX-512-Instruktionen nutzen. Ein Kryptographie-Kern auf demselben Chip hingegen schon. Lyx löst diesen Widerspruch durch das **Energy-Aware Programmiermodell**: Der Entwickler teilt dem Compiler mit, wie viel Energie eine Funktion oder ein ganzes Programm verbrauchen darf — und der Compiler optimiert den generierten Maschinencode entsprechend. Das Modell arbeitet auf zwei Ebenen: **global** über den Compiler-Flag ''--target-energy'' und **granular** über das ''@energy''-Pragma direkt im Quellcode. ---- ===== 1. Die fünf Energy-Level ===== Lyx kennt fünf Energy-Level, von minimal bis extrem. Sie steuern, welche Instruktionen, Optimierungen und Backend-Strategien der Compiler einsetzt. ^ Level ^ Name ^ Einsatz ^ Loop Unrolling ^ SIMD/FPU ^ Cache-Prefetch ^ FP-Optimierung ^ | **1** | Minimal | Batteriebetrieb, Sleep-Modes, Polling | Max. 2× | Deaktiviert | Nein | Deaktiviert | | **2** | Balanced-Low | Sensorknoten, Dauerbetrieb mit Akku | Max. 4× | Skalare NEON/SSE | Nein | Einfach | | **3** | Balanced | Standard (Default) | 8× | SSE4/NEON voll | Selektiv | Vollständig | | **4** | High | Server, Desktop, Hochlast | 16× | AVX2/NEON aggressiv | Ja | Aggressiv | | **5** | Extreme | HPC, Signalverarbeitung, GPU-nah | 32× | AVX-512/SVE | Vollständig | Maximal | Level 3 ist der Default — er entspricht dem, was andere Compiler mit ''-O2'' produzieren. Levels 1 und 2 sind bewusst konservativer als klassische ''-O1'', weil sie nicht nur auf Größe, sondern explizit auf **Energie** optimieren. ==== Was "Energie sparen" konkret bedeutet ==== ^ Strategie ^ Level 1–2 ^ Level 4–5 ^ | **Instruktionswahl** | Einfache Integer-Ops bevorzugt — Divisionen durch Bit-Shifts angenähert | Alle Instruktionen nutzbar, Throughput vor Latenz | | **SIMD** | Deaktiviert oder nur skalarer Modus | AVX2/AVX-512/NEON/SVE voll ausgeschöpft | | **Loop Unrolling** | Minimal — kleiner Code, ruhiger Instruction-Cache | Massiv — Schleifenoverhead eliminiert | | **Cache Prefetch** | Kein Prefetching — vermeidet unnötige Speichertransfers | Aggressives Prefetching für Cache-Wärme | | **Register Toggling** | Compiler bevorzugt Register mit weniger Bit-Änderungen | Keine Einschränkung | | **FPU** | Fließkomma nur wenn zwingend — Integer-Approximation bevorzugt | Volle FPU-Pipeline, FMA-Instruktionen | | **Spekulative Ausführung** | Eingeschränkt | Vollständig aktiviert | ---- ===== 2. Globale Steuerung: --target-energy ===== Der einfachste Einstieg ist der Compiler-Flag. Er setzt das Energy-Level für das gesamte Programm: # IoT-Sensorknoten — maximales Energiesparen lyxc sensor_node.lyx --target=arm64 --target-energy=1 -o sensor.elf # Standardbuild (Level 3 ist Default — explizit oder implizit) lyxc server.lyx --target-energy=3 -o server # Hochperformanter Signalprozessor — volle AVX-2-Nutzung lyxc dsp.lyx --target=x86_64 --target-energy=5 -o dsp.elf Das Energy-Level beeinflusst das gesamte Compiler-Backend — Instruktionsselektion, Scheduling, Registerallokation und alle Optimierungspässe. ---- ===== 3. Granulare Steuerung: @energy ===== In der Praxis haben die meisten Programme keine einheitliche Energiecharakteristik. Ein eingebettetes System hat gleichzeitig: - Einen **Polling-Loop**, der 99 % der Zeit in einem energiearmen Idle-Zustand läuft - Einen **Verarbeitungskern**, der für kurze Zeitfenster volle Rechenleistung braucht ''@energy'' löst das: Jede Funktion bekommt ihr eigenes Level — unabhängig vom globalen Flag. import std.io; // Schläft energiesparend — fragt Hardware-Register ab, sonst nichts @energy(1) fn PollSensor(port: int64): int64 { // Compiler vermeidet hier FPU, SIMD und komplexe Scheduling-Entscheidungen var raw: int64 := MemRead32(port); if (raw == 0) { return -1; } return raw & 0xFF; } // Verarbeitungskern — kurze Hochlast-Phase @energy(4) fn ProcessBatch(data: int64, n: int64): f64 { // Compiler setzt hier SIMD ein, rollt Schleifen aus var sum: f64 := 0.0; var i: int64 := 0; while (i < n) limit(65536) { sum := sum + (data + i * 8) as f64; i := i + 1; } return sum / (n as f64); } // Logging — sparsam, nicht zeitkritisch @energy(1) fn LogEvent(msg: int64): void { PrintLn(msg); } // Kryptographie — braucht volle Leistung @energy(5) fn EncryptBlock(input: int64, key: int64, output: int64): void { // AES-NI oder ähnliche Instruktionen werden genutzt // ... } fn main(): int64 { while (true) limit(2147483647) { var val: int64 := PollSensor(0x40020000); if (val > 0) { var avg: f64 := ProcessBatch(val, 64); LogEvent("Messung verarbeitet"); } } return 0; } ''@energy'' überschreibt ''--target-energy'' für die markierte Funktion. Der Rest des Programms läuft weiterhin mit dem globalen Level. ---- ===== 4. Backend-Optimierungen im Detail ===== Details zu den verfügbaren Backend-Optimierungen: ==== Level 1 — Minimal ==== Der Compiler trifft aktiv Entscheidungen gegen Energieverbrauch: * **Keine SIMD**: AVX, SSE, NEON und SVE-Einheiten bleiben inaktiv. Diese Rechenwerke brauchen beim Aufwachen aus dem Idle-Zustand messbare Energie, selbst wenn sie nur wenige Zyklen rechnen. * **Integer-Approximation**: Divisionen durch Konstanten werden zu Multiplikationen + Shifts umgewandelt. Fließkomma-Berechnungen werden, wo möglich, durch Integer-Fixpunkt-Arithmetik ersetzt. * **Minimales Loop Unrolling**: Schleifen werden maximal 2× abgerollt. Mehr Unrolling erzeugt größeren Code, der mehr Instruction-Cache-Lines belegt und damit mehr Energie beim Laden verbraucht. * **Register-Selektion**: Der Allocator bevorzugt Register, die in der vorigen Instruktion bereits geschrieben wurden — weniger Bit-Toggles auf dem internen Datenbus. * **Kein Prefetching**: Kein ''PREFETCH''/''PRFM''. Speichertransfers nur wenn tatsächlich benötigt. ==== Level 2 — Balanced-Low ==== Für Sensorknoten und Geräte, die dauerhaft aktiv sein müssen, aber keine harte Echtzeitlast tragen: * **Skalare NEON/SSE**: Vektoreinheiten werden genutzt, aber nur im 64-Bit-Modus (D-Register bei NEON, XMM low-half bei SSE). Das vermeidet den Aufwachstrom der breiten 128-Bit-Einheiten. * **Einfache FP-Optimierung**: Fließkomma wird nicht durch Integer-Approximation ersetzt, aber FMA und Vektorfließkomma bleiben deaktiviert. * **Max. 4× Loop Unrolling**: Kompromiss zwischen Schleifenoverhead und Code-Größe. Eng geschlossene Schleifen mit kurzen Körpern profitieren, ohne den Instruction-Cache zu fluten. * **Kein Prefetching**: Wie Level 1 — bewusst kein spekulativer Speichertransfer. * **Selektive Integer-Approximation**: Ganzzahldivisionen durch Zweierpotenzen werden als Shifts kodiert; andere Divisionen bleiben exakt. Typischer Einsatz: Dauerhafte Datenerfassung mit gelegentlicher Auswertung — z.B. ein CO₂-Sensor, der sekündlich misst und minütlich sendet. ==== Level 3 — Balanced (Default) ==== Aktive Optimierungen auf Level 3: * Volle Integer- und FPU-Pipeline genutzt * SSE4.1 / NEON voll aktiv (128 Bit) für Vektoroperationen * 8× Loop Unrolling für enge Schleifen * Selektives Prefetching bei erkennbaren Zugriffsmustern (Stride-1, Stride-2) * Constant Folding, Dead Code Elimination, Inlining (bis 32 Instruktionen) * FMA aktiviert, aber nur wenn der Compiler es beweisbar für sicher hält (keine Neuordnung von Fließkommaoperationen die das Ergebnis verändern würden) Level 3 entspricht qualitativ ''-O2'' in GCC/Clang — ausgewogen zwischen Compilezeit, Code-Größe und Laufzeitleistung. ==== Level 4 — High ==== Für Server, Desktop-Anwendungen und Hochlastphasen, wo Energie keine primäre Einschränkung ist: * **AVX2 / NEON 128-Bit aggressiv**: Voll 256-Bit-AVX2 (x86_64) bzw. aggressives NEON-Bündeln (ARM64). 8 float32 oder 4 float64 gleichzeitig. * **16× Loop Unrolling**: Schleifenoverhead in kritischen Pfaden auf unter 1 % reduziert. * **Aggressives Inlining**: Funktionen bis 64 Instruktionen werden immer inlined. Call-Overhead und Registerübertragungskosten entfallen. * **Aggressives Prefetching**: Der Compiler setzt ''PREFETCH''/''PRFM''-Instruktionen mit einem Lookahead von 4–8 Cache-Lines. Funktioniert gut bei vorhersehbaren Zugriffsmustern (Arrays, Puffer, Matrizen). * **FMA und fused Schleifenkörper**: Fused Multiply-Add wird systemisch eingesetzt. Schleifen mit mehreren unabhängigen Akkumulatoren werden zu Pipeline-freundlichen Superscalar-Sequenzen umgebaut. * **Vollständige FP-Optimierung**: Neuordnung von Fließkomma-Operationen nach IEEE 754 erlaubt (leicht abweichende Ergebnisse durch andere Rundungsreihenfolge sind möglich). > **Hinweis:** Auf Laptops und Tablets kann Level 4 den Prozessor in einen höheren P-State treiben. Für kurze Bursts (< 1 s) ist das unproblematisch; Dauerlast erhöht den Gesamtenergieverbrauch trotz höherer Performance-pro-Instruktion. ==== Level 5 — Extreme ==== Aktive Optimierungen auf Level 5: * **AVX-512 / SVE**: 512-Bit breite Vektoroperationen — 16 float32 oder 8 float64 gleichzeitig * **32× Loop Unrolling**: Schleifenoverhead quasi eliminiert * **FMA**: Fused Multiply-Add — eine Instruktion für a × b + c * **Aggressives Inlining**: Auch tiefe Call-Trees werden inlined * **Full Prefetch**: Speicher mehrere Cache-Lines voraus geladen > **Hinweis:** Level 5 kann auf einigen Prozessoren thermisches Throttling auslösen. AVX-512 ist für Dauerlast auf mobilen oder batteriebetriebenen Chips nicht geeignet. ---- ===== 5. Energy Statistics ===== Mit dem Flag ''--target-energy'' erzeugt der Compiler nach dem Build eine Schätzung des Energieverbrauchs auf Basis eines architekturspezifischen Energie-Modells: lyxc sensor_node.lyx --target=arm64 --target-energy=1 --energy-stats -o sensor.elf === Energy Statistics (arm64, Level 1) === Estimated energy units: 1 450 ALU operations: 890 (61 %) Memory accesses: 340 (23 %) Branches: 120 ( 8 %) FPU operations: 60 ( 4 %) SIMD operations: 0 ( 0 %) L1 code footprint: 1 024 bytes L1 data footprint: 512 bytes Estimated battery cycles: ~8.2 M (at 3.3V / 50 mA baseline) Zum Vergleich derselbe Code mit Level 5: === Energy Statistics (arm64, Level 5) === Estimated energy units: 620 ALU operations: 180 (29 %) Memory accesses: 90 (14 %) SIMD operations: 310 (50 %) Branches: 40 ( 6 %) L1 code footprint: 4 096 bytes Estimated battery cycles: ~3.9 M (at 3.3V / 50 mA baseline) Level 5 ist mehr als doppelt so schnell — verbraucht aber durch SIMD-Aufwachen, Cache-Befüllung und größeren Code unter Umständen mehr Gesamtenergie, wenn die Berechnungsphase kurz ist. Das Energie-Modell ist architekturspezifisch: ''--target=x86_64'' und ''--target=arm64'' haben unterschiedliche Kosten-Tabellen. Das Modell ist eine Schätzung, kein Messgerät — für exakte Messungen bleibt Hardware-Profiling (z.B. mit einem Strommesssensor am VDDA-Pin) unersetzlich. ==== Energy-Statistics-Ausgabe interpretieren ==== Die vier Hauptkennzahlen: ^ Kennzahl ^ Was sie bedeutet ^ Aktion wenn zu hoch ^ | **Estimated energy units** | Dimensionsloser Vergleichswert (Summe gewichteter Instruktionskosten) | Profil-geführte Umstrukturierung des kritischen Pfads | | **SIMD operations (%)** | Anteil der Vektorinstruktionen — niedrig bei Level 1 gewünscht | Bei Level 1 prüfen ob ''@energy'' an der richtigen Stelle sitzt | | **Memory accesses (%)** | Cache-Miss-Anteil und Speicherbandbreite | Cache-Footprint reduzieren — dazu Abschnitt 8 | | **L1 code footprint** | Wie viele Bytes des L1-Instruction-Cache belegt werden | Inlining oder Loop Unrolling reduzieren (kleineres Level) | **Faustregel:** Wenn ''SIMD operations'' bei Level 1–2 mehr als 5 % zeigt, wurde entweder ''@energy'' vergessen oder eine Bibliotheksfunktion intern SIMD-Code aufgerufen. Mit ''--energy-stats=verbose'' sieht man pro Funktion den Aufschlüsselung. ==== Hardware-Profiling-Workflow ==== ''--energy-stats'' ist eine Schätzung. Für echte Messungen: # Schritt 1: Build mit Debug-Symbolen und Energy-Stats lyxc sensor_node.lyx --target=arm64 --target-energy=1 --energy-stats -g -o sensor.elf # Schritt 2: Auf Hardware flashen und mit Strommessung ausführen # (INA219, Nordic PPK2, o.ä. am VDDA-Pin) # Strommessung protokollieren: current_log.csv # Schritt 3: Compiler-Schätzung vs. gemessene Werte vergleichen # Korrektur: --energy-model=custom wenn Abweichung > 20 % lyxc ... --energy-model=/path/to/my_chip_energy.toml Das Custom-Energy-Model ist eine TOML-Datei mit gemessenen Kosten pro Instruktionskategorie: # my_chip_energy.toml (Beispiel: STM32H7 bei 3.3V/120MHz) [alu] integer_add = 1 integer_mul = 2 integer_div = 8 shift = 1 [fpu] fp_add = 4 fp_mul = 5 fp_div = 20 fma = 6 [simd] neon_128 = 12 # Aufwachkosten + Betrieb [memory] l1_hit = 1 l2_hit = 4 dram_access = 40 ---- ===== 6. @energy und Funktionsketten ===== Was passiert, wenn eine Funktion mit niedrigem Energy-Level eine Funktion mit hohem Level aufruft — oder umgekehrt? ==== Grundregel: Der Callee bestimmt sich selbst ==== Jede Funktion wird mit dem Energy-Level kompiliert, das direkt für sie gilt — unabhängig vom Aufrufer. Der Aufruf selbst (die CALL-Instruktion plus Stackrahmen) unterliegt dem Level des **Aufrufers**. @energy(1) fn Idle(): void { // Dieser Code wird mit Level-1-Optimierungen compiliert var v: int64 := PollSensor(0x40020000); // Der Aufruf von ProcessBatch ist eine normale CALL-Instruktion // Sie kostet keine SIMD-Energie — das ist Level-1-Aufrufer-Code if (v > 0) { ProcessBatch(v, 64); // <- ProcessBatch selbst läuft mit @energy(5) } } @energy(5) fn ProcessBatch(data: int64, n: int64): void { // Dieser Code wird mit Level-5-Optimierungen compiliert // AVX-512, 32× Unrolling, FMA — alles aktiv } Der Compiler trennt die beiden Funktionskörper klar. Der Overhead beim Aufrufpunkt (CALL + Argumente) folgt Level 1; sobald der Instruction Pointer in ''ProcessBatch'' springt, gelten Level-5-Regeln. ==== Inlining und Energy-Level ==== Wenn der Compiler eine Funktion inlined, **übernimmt sie das Energy-Level des Aufrufers** — es sei denn, die Funktion ist explizit mit ''@energy'' annotiert. // Keine @energy-Annotation: wird beim Inlining adoptiert fn helper(x: int64): int64 { return x * 2 + 1; } @energy(5) fn HotPath(data: int64, n: int64): void { var i: int64 := 0; while (i < n) limit(65536) { // helper() wird hier inlined — mit Level-5-Optimierungen var v: int64 := helper(i); poke64(data + i * 8, v); i := i + 1; } } @energy(1) fn ColdPath(x: int64): int64 { // helper() wird hier inlined — aber mit Level-1-Optimierungen return helper(x); } Mit explizitem ''@energy'' wird die Annotation beim Inlining **beibehalten**: @energy(5) fn FastHelper(x: int64): int64 { // Diese Funktion bringt ihre eigenen Level-5-Optimierungen mit — // auch wenn sie in einen @energy(1)-Kontext inlined wird return x * x + x; } @energy(1) fn SlowContext(x: int64): int64 { // FastHelper wird inlined, aber sein Körper bleibt Level-5-optimiert. // Der Compiler erzeugt eine "Energie-Barriere": kurzer SIMD-Burst // innerhalb eines ansonsten energiesparsamen Frames. return FastHelper(x) + 1; } > Das Verhalten bei explizit annotierten Inline-Funktionen entspricht dem von ''@flight_crit'' — die Annotation des Callees hat Vorrang vor dem Kontext. ==== Rekursion und Energy-Level ==== Bei rekursiven Funktionen gilt durchgängig das Level der annotierten Funktion: @energy(3) fn Fibonacci(n: int64): int64 { if (n <= 1) { return n; } // Rekursive Aufrufe erben @energy(3) return Fibonacci(n - 1) + Fibonacci(n - 2); } ---- ===== 7. Cache und Speicher als Energiefaktor ===== Der Instruction-Cache und der Datencache sind oft unterschätzte Energiesenken. Ein DRAM-Zugriff kostet auf ARM Cortex-A53 ca. 40–60× mehr Energie als ein L1-Cache-Hit. ==== Warum Cache-Misses teuer sind ==== ^ Cache-Ebene ^ Typische Latenz ^ Relative Energiekosten (ARM Cortex-A) ^ | L1-Cache (Hit) | 1–4 Zyklen | 1× (Referenz) | | L2-Cache (Hit) | 10–15 Zyklen | 4–8× | | L3-Cache (Hit) | 30–50 Zyklen | 15–25× | | DRAM (Miss) | 100–300 Zyklen | 40–80× | Ein Programm, das bei Level 5 den gesamten L1-Instruction-Cache (32 KB) durch massives Loop Unrolling belegt, kann trotz weniger ausgeführter Instruktionen mehr Energie verbrauchen als Level 3 mit kleinerem Code-Footprint — weil jeder Funktionsaufruf einen partiellen L1-Flush auslöst. ==== L1 Code Footprint beobachten ==== Der ''--energy-stats''-Output zeigt ''L1 code footprint''. Wenn dieser Wert die L1-Instruction-Cache-Größe des Zielchips überschreitet, lohnt sich ein niedrigeres Level: # L1-Cache-Größe typischer Chips: # ARM Cortex-M4: 16 KB # ARM Cortex-A53: 32 KB # ARM Cortex-A72: 48 KB # x86_64 (Intel Core): 32–64 KB lyxc hotpath.lyx --target=arm64 --target-energy=5 --energy-stats -o hotpath.elf # Output: L1 code footprint: 48 384 bytes # Warnung: Überschreitet Cortex-A53 L1 (32 KB) — versuche --target-energy=4 ==== Data-Locality und Energiesparen ==== Speicherzugriffsmuster haben direkten Einfluss auf Energie. Der Compiler berücksichtigt das bei Level 1–2: // Energieschonend: sequenzieller Zugriff — vorhersehbar für Cache @energy(1) fn SumSequential(data: int64, n: int64): int64 { var sum: int64 := 0; var i: int64 := 0; while (i < n) limit(65536) { sum := sum + peek64(data + i * 8); // Stride 8 — eine Cache-Line alle 8 Zugriffe i := i + 1; } return sum; } // Energieteuer: streuender Zugriff — jeder Zugriff ein potenzieller Cache-Miss @energy(1) fn SumScattered(ptrs: int64, n: int64): int64 { var sum: int64 := 0; var i: int64 := 0; while (i < n) limit(65536) { var addr: int64 := peek64(ptrs + i * 8); // indirekte Adresse sum := sum + peek64(addr); // zufälliger Speicherbereich i := i + 1; } return sum; } Der Compiler kann ''SumSequential'' mit einem Software-Prefetch optimieren (Level 3+). ''SumScattered'' bleibt schwierig — hier hilft Data-Locality-Refactoring auf Anwendungsebene mehr als ein höheres Energy-Level. ==== Empfehlungen für energie-bewusstes Speicherdesign ==== * Datenstrukturen **struct-of-arrays** statt **array-of-structs** bei Batch-Verarbeitung — Vektorisierung und Cache-Effizienz verbessern sich gleichzeitig. * **Puffer auf Cache-Line-Größe ausrichten** (64 Bytes auf x86_64/ARM64): ''alloc(n)'' gibt page-aligned Speicher zurück; für Cache-Line-Alignment: ''alloc(n + 63) & ~63''. * **Hot/Cold-Trennung**: Felder, die im kritischen Pfad gebraucht werden, in den ersten 64 Bytes einer Datenstruktur platzieren. Selten genutzte Felder (Logging, Debug, Config) ans Ende. * **Stack-Variablen für Schleifen-Akkumulatoren**: Der Compiler kann Register-Resident-Keeping besser garantieren wenn Akkumulatoren als lokale Variablen deklariert sind. ---- ===== 8. @energy und DO-178C ===== In sicherheitskritischen Systemen gelten zusätzliche Einschränkungen für das Energy-Level. Die Kombination von ''@energy'' mit DO-178C-Annotationen ist vollständig unterstützt, folgt aber klaren Regeln. ==== Welche Levels sind zertifizierbar? ==== ^ Energy-Level ^ DAL-A ^ DAL-B ^ DAL-C ^ DAL-D ^ Begründung ^ | Level 1 | ✓ | ✓ | ✓ | ✓ | Deterministisch, kein SIMD, minimale Variabilität | | Level 2 | ✓ | ✓ | ✓ | ✓ | Skalare SIMD akzeptiert; Loop Unrolling begrenzt | | Level 3 | Bedingt | ✓ | ✓ | ✓ | WCET gut berechenbar; FMA-Neuordnung kann WCET leicht verbreitern | | Level 4 | Nein | Bedingt | ✓ | ✓ | Aggressives Inlining erschwert Stack-Analyse | | Level 5 | Nein | Nein | Bedingt | ✓ | AVX-512 Frequency Scaling macht WCET nicht garantierbar | ==== Kombination mit @flight_crit ==== ''@flight_crit(DAL-A)'' impliziert automatisch ''@energy(1)'' wenn kein anderes ''@energy'' angegeben ist. Beide Annotationen können aber explizit kombiniert werden: // Implizit @energy(1) durch @flight_crit: @flight_crit(DAL-A) fn ComputeAttitude(gyro: int64, accel: int64): int64 { // Compiler setzt Level 1 — kein SIMD, deterministisch return CalculateQuaternion(gyro, accel); } // Explizit: DAL-B mit Level 2 erlaubt skalare Vektoren @flight_crit(DAL-B) @energy(2) fn FilterSensorData(data: int64, n: int64): void { // Skalare NEON/SSE sind zertifizierbar auf DAL-B } // FEHLER: Compiler lehnt diese Kombination ab @flight_crit(DAL-A) @energy(4) // ERROR: @energy(4) inkompatibel mit DAL-A fn BadCombination(): void { } ==== Stack-Limit und Energy ==== Bei Level 4–5 vergrößert aggressives Inlining den Stack-Footprint. Das ''@stack_limit''-Pragma sollte bei allen DAL-annotierten Funktionen gesetzt werden: @flight_crit(DAL-B) @energy(2) @stack_limit(512) // Bytes — Compiler bricht ab wenn überschritten fn NavigationUpdate(pos: int64, vel: int64): int64 { return IntegratePosition(pos, vel); } Wenn ''@stack_limit'' gesetzt ist und ein höheres Energy-Level durch Inlining den Limit überschreiten würde, gibt der Compiler einen Fehler aus — nicht nur eine Warnung. ==== WCET-Analyse ==== Für DAL-A/B-Code ist eine WCET-Analyse (Worst-Case Execution Time) Pflicht. Der Lyx-Compiler unterstützt das mit: # WCET-Annotation im Build lyxc flight_computer.lyx --target=arm64 --target-energy=1 \ --wcet-analysis --wcet-output=wcet_report.txt -o flight.elf Das ''wcet_report.txt'' enthält pro Funktion den berechneten Worst-Case-Pfad und die Zyklenanzahl. Bei Level 1–2 ist die WCET-Schranke typischerweise scharf (< 5 % Überschätzung); bei Level 3 kann sie durch FMA-Neuordnung und selektives Prefetching um bis zu 15 % konservativ sein. ---- ===== 9. QBool — Probabilistisches Computing ===== Ein besonderer Baustein des Energy-Aware-Modells ist der Typ ''qbool'' (Quantum Bool). Er speichert keinen binären Wahrheitswert, sondern eine **Wahrscheinlichkeit** zwischen 0.0 und 1.0. ==== Motivation ==== Klassische boolesche Entscheidungen sind exakt — aber Exaktheit kostet. Ein Temperatursensor hat eine Messunschärfe von ±0.5 °C. Ein Schwellwert-Vergleich ''temp > 80.0'' gibt ein hartes true oder false zurück, obwohl bei 80.2 °C mit verrauschtem Sensor "wahrscheinlich überschritten" die treffendere Aussage wäre. ''qbool'' modelliert diese Unsicherheit direkt im Typ. Bei ''@energy(1)'' kann der Compiler ''qbool''-Entscheidungen probabilistisch auflösen — statt einen exakten Vergleich durchzuführen, wird mit der gespeicherten Wahrscheinlichkeit "gewürfelt". Das spart Rechenarbeit auf Kosten einer kontrollierten Unschärfe. ==== Grundoperationen ==== import std.qbool; import std.io; fn main(): int64 { // qbool erstellen — Literal mit q-Suffix var high_conf: qbool := 0.92q; // 92 % Wahrscheinlichkeit "wahr" var low_conf: qbool := 0.35q; // 35 % Wahrscheinlichkeit "wahr" var certain: qbool := 1.0q; // deterministisch wahr var impossible: qbool := 0.0q; // deterministisch falsch // Observe: kollabiert qbool zu einem konkreten bool (stochastisch) var result: bool := Observe(high_conf); // Mit 92 % Wahrscheinlichkeit: true. Mit 8 %: false. // Logische Operationen (wahrscheinlichkeitsbasiert) var both: qbool := QBoolAnd(high_conf, low_conf); // P(A ∧ B) = 0.92 × 0.35 = 0.322q Print("P(beide): "); PrintF64(GetProbability(both)); PrintLn(""); var either: qbool := QBoolOr(high_conf, low_conf); // P(A ∨ B) = 1 - (1 - 0.92) × (1 - 0.35) = 0.948q Print("P(mindestens eine): "); PrintF64(GetProbability(either)); PrintLn(""); var negated: qbool := QBoolNot(high_conf); // P(¬A) = 1 - 0.92 = 0.08q Print("P(Gegenteil): "); PrintF64(GetProbability(negated)); PrintLn(""); // Deterministisch prüfen if (QBoolIsDeterministic(certain)) { PrintLn("certain ist exakt — kein Zufallsanteil"); } return 0; } ==== Anwendungsfall: Heuristischer Sensor-Filter ==== import std.qbool; import std.io; // Gibt an, ob Handlungsbedarf besteht — mit Unsicherheitsmodell @energy(1) fn AssessThreat( temp_high: f64, // gemessene Temperatur (verrauscht) pressure_ok: bool, // Drucksensor: zuverlässiger, binär motion_prob: f64 // Bewegungswahrscheinlichkeit vom PIR (0.0–1.0) ): bool { // Temperatur: Messung hat ±1 °C Unschärfe — ab 78 °C beginnt der Graubereich var temp_q: qbool := temp_high > 82.0 ? 0.98q : temp_high > 79.0 ? 0.7q : temp_high > 76.0 ? 0.3q : 0.05q; // Druck: zuverlässiger Sensor → deterministischer qbool var pressure_q: qbool := pressure_ok ? QBoolTrue() : QBoolFalse(); // Bewegung: PIR-Sensor gibt direkt eine Wahrscheinlichkeit var motion_q: qbool := Maybe(motion_prob); // Gesamt-Bedrohungswahrscheinlichkeit var threat: qbool := QBoolAnd(temp_q, QBoolOr(pressure_q, motion_q)); // Entscheidung: Handeln wenn > 70 % wahrscheinlich if (GetProbability(threat) > 0.70) { return true; } return Observe(threat); // Im Grenzbereich: stochastische Entscheidung } fn main(): int64 { var alarm: bool := AssessThreat(80.5, true, 0.6); Print(alarm ? "Alarm ausgelöst\n" : "Kein Handlungsbedarf\n"); return 0; } ==== Anwendungsfall: KI-Entscheidungslogik ==== import std.qbool; // Spieler-KI: Soll der Agent angreifen? @energy(2) fn DecideAttack( enemy_health_low: f64, // Gegner schwach? (0.0–1.0) own_health: f64, // Eigene Gesundheit (0.0–1.0) cover_available: bool // Deckung vorhanden? ): bool { var enemy_weak: qbool := Maybe(enemy_health_low); var own_ok: qbool := Maybe(own_health); var cover_q: qbool := cover_available ? 0.8q : 0.2q; // Angriff sinnvoll wenn: Gegner schwach UND (eigene Stärke OK ODER Deckung) var attack: qbool := QBoolAnd(enemy_weak, QBoolOr(own_ok, cover_q)); return Observe(attack); } Die ''std.qbool''-Unit enthält fertige Vorlagen für Wettervorhersage (''WeatherPrediction''), medizinische Diagnose (''Diagnose'') und Spieler-KI (''GameAI''). ---- ===== 10. Energy-Level und Determinismus ===== Aus Safety-Perspektive gibt es einen wichtigen Unterschied: ^ Eigenschaft ^ Level 1–3 ^ Level 4–5 ^ | Deterministisches Ergebnis | Ja | Ja | | Deterministischer Energieverbrauch | Weitgehend | Nein (thermisches Throttling) | | WCET-Berechenbarkeit | Gut | Eingeschränkt (AVX-512 Frequency Scaling) | | DO-178C-geeignet | Ja | Bedingt (Level 4 möglich, Level 5 problematisch) | Für sicherheitskritische Software gilt: ''@energy(1)'' bis ''@energy(3)'' sind WCET-berechenbar. Level 4 ist grenzwertig (Loop Unrolling kann zu Instruction-Cache-Misses führen), Level 5 ist für Zertifizierungspfade nicht empfohlen. ---- ===== 11. Typische Einsatzmuster ===== Häufig verwendete Einsatzmuster: ==== IoT-Sensorknoten (Batterie, ARM Cortex-M) ==== // Global: Level 1 (per --target-energy=1) // Nur der Verarbeitungsblock bekommt mehr Energie @energy(1) fn SleepPoll(): int64 { // Im Normalfall: schläft, fragt selten ab SysSleep(9000); // 9 Sekunden schlafen return MemRead32(SENSOR_PORT); } @energy(3) fn ProcessReading(raw: int64): f64 { // Kurze Rechenphase — normales Performance-Level reicht return (raw as f64) * 0.0625 - 40.0; // Temperatur-Umrechnung } fn main(): int64 { while (true) limit(2147483647) { var raw: int64 := SleepPoll(); if (raw > 0) { var temp: f64 := ProcessReading(raw); TransmitReading(temp); } } return 0; } ==== Server: Hintergrundjobs vs. Request-Handler ==== // Request-Handler: muss schnell sein @energy(4) fn HandleRequest(req: HttpRequest): HttpResponse { var body: int64 := ParseJSON(req.body); var result: int64 := ComputeResult(body); return BuildResponse(200, result); } // Health-Check: läuft periodisch, nicht zeitkritisch @energy(1) fn HealthCheck(): bool { return DBIsAlive() && CacheIsAlive(); } // Log-Rotation: Hintergrundjob, sparsam @energy(1) fn RotateLogs(): void { var old_log: int64 := OpenFile("/var/log/app.log.1"); CompressGzip(old_log, "/var/log/app.log.1.gz"); CloseFile(old_log); } ==== Signalverarbeitung (DSP, Level 5) ==== // FFT-Kern: kurze Burst-Phase, braucht SIMD @energy(5) fn ComputeFFT(samples: int64, n: int64, output: int64): void { // Compiler setzt AVX-512 / NEON SVE ein // 32× Loop Unrolling, FMA, Prefetch FFTButterfly(samples, n, output); } // Audio-I/O: wartet meistens, sparsam @energy(1) fn AudioCallback(buf: int64, frames: int64): void { var n: int64 := ReadAudioDevice(buf, frames); if (n > 0) { ComputeFFT(buf, n, fft_output); } } ---- ===== 12. Best Practices ===== Empfehlungen für den produktiven Einsatz: ^ Situation ^ Empfehlung ^ | Polling-Loops, Idle-Warten | ''@energy(1)'' — SIMD-Aufwachkosten vermeiden | | Dauerbetrieb Sensor/Akku | ''@energy(2)'' — skalare SIMD OK, kein Prefetch | | Periodische Sensor-Auswertung | ''@energy(2)'' oder ''@energy(3)'' | | Rechenintensive Kerne (FFT, Krypto, ML) | ''@energy(4)'' oder ''@energy(5)'' | | Safety-Code (DAL-A/B) | Maximal ''@energy(2)'' für DAL-A, ''@energy(3)'' für DAL-B | | Unsichere Sensorwerte, Heuristiken | ''qbool'' + ''@energy(1)'' | | Globaler Default | ''--target-energy=3'' (entspricht ''-O2'') | | Energie messen | ''--energy-stats'' + Custom Energy Model + Hardware-Profiling | | AVX-512 auf Embedded | Nicht empfohlen — thermisches Throttling, kein DAL | | ''qbool'' in Safety-Code | Nur für nicht-deterministische Heuristiken, nie im Regelzyklus | | L1-Code-Footprint zu groß | Energy-Level senken oder Inlining-Grenzen mit ''@noinline'' setzen | | @energy(5) in @energy(1)-Kontext | Erlaubt — Callee behält eigenes Level; Aufwachkosten trotzdem beachten | | WCET-Analyse nötig | ''--wcet-analysis'' + ''@stack_limit'' + Level ≤ 3 | | Struct-Datenlayout | Hot-Felder in erste 64 Bytes (eine Cache-Line), Cold-Felder ans Ende | | Loop mit Pointer-Indirektion | Daten vorab in sequenziellen Puffer kopieren — Cache-Miss-Rate sinkt | → [[lyx_-_programmiersprache:units:qbool|std.qbool — Vollständige Funktionsreferenz]]\\ → [[lyx_-_programmiersprache:guides:do-178c|DO-178C und DAL-Annotationen]]\\ → [[lyx_-_programmiersprache:sprache:memory-management|Memory Management — Stack, Heap, @stack_limit]] Letzte Aktualisierung: 2026-06-05