Inhaltsverzeichnis

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) 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:

Level 3 — Balanced (Default)

Level 5 — Extreme

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

std.qbool — Vollständige Funktionsreferenz