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