====== 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 { PrintStr(msg); PrintStr("\n"); } // 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 ===== ==== 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 3 — Balanced (Default) ==== * Volle Integer- und FPU-Pipeline genutzt * SSE4.1 / NEON aktiv für Vektoroperationen * 8× Loop Unrolling für enge Schleifen * Selektives Prefetching bei erkennbaren Zugriffsmustern * Constant Folding, Dead Code Elimination, Inlining ==== Level 5 — Extreme ==== * **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. ---- ===== 6. 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 PrintStr("P(beide): "); PrintF64(GetProbability(both)); PrintStr("\n"); var either: qbool := QBoolOr(high_conf, low_conf); // P(A ∨ B) = 1 - (1 - 0.92) × (1 - 0.35) = 0.948q PrintStr("P(mindestens eine): "); PrintF64(GetProbability(either)); PrintStr("\n"); var negated: qbool := QBoolNot(high_conf); // P(¬A) = 1 - 0.92 = 0.08q PrintStr("P(Gegenteil): "); PrintF64(GetProbability(negated)); PrintStr("\n"); // Deterministisch prüfen if (QBoolIsDeterministic(certain)) { PrintStr("certain ist exakt — kein Zufallsanteil\n"); } 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); PrintStr(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''). ---- ===== 7. 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. ---- ===== 8. Typische 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); } } ---- ===== 9. Best Practices ===== ^ Situation ^ Empfehlung ^ | Polling-Loops, Idle-Warten | ''@energy(1)'' — SIMD-Aufwachkosten vermeiden | | 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(3)'' — WCET-Berechenbarkeit | | Unsichere Sensorwerte, Heuristiken | ''qbool'' + ''@energy(1)'' | | Globaler Default | ''--target-energy=3'' (entspricht ''-O2'') | | Energie messen | ''--energy-stats'' + Hardware-Profiling | | AVX-512 auf Embedded | Nicht empfohlen — thermisches Throttling | | ''qbool'' in Safety-Code | Nur für nicht-deterministische Heuristiken, nie im Regelzyklus | → [[lyx_-_programmiersprache:units:qbool|std.qbool — Vollständige Funktionsreferenz]]