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 |
