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.

 
PrintF64 ist seit lyxc 1.0.17C ein Builtin (#1254, #1284) — kein Import nötig, auch nicht std.io. Es gibt sechs Nachkommastellen mit abschließendem Zeilenumbruch aus: PrintF64(1.25)1.250000. Die Ausgabe rundet sichtbar (3.141593.141589); für exakte Stellenzahl FloatToStr nehmen.

Ebenfalls in std.io: PrintFloatLn(x: f64) (mit Umbruch) und FloatToStr(val: f64, prec: int64) (Zeichenkette, etwa Print(FloatToStr(3.75, 2)) für Ausgabe ohne Umbruch).

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

 
Der Vergleich bezieht sich auf GCC und Clang. Die gleichnamigen Schalter von lyxc selbst — -O0 bis -O3 und --no-opt — sind wirkungslos (#1525); es gibt keinen IR-Optimierer. --target-energy ist damit der einzige Weg, die Codeerzeugung zu beeinflussen, und übernimmt die Rolle, die anderswo -O hat. Gemessen reicht die Spanne von 33 KB (Stufe 1) bis 238 KB (Stufe 5) beim selben Programm; Stufe 4 ist derzeit identisch mit Stufe 3.

Ohne @energy und ohne –target-energy gilt seit lyxc 1.0.15C Level 3 — der ausgewogene Standard muss nicht mehr ausdrücklich angefordert werden.

 
Level 3 und 4 erzeugen identischen Code — es sind vier unterscheidbare Stufen, nicht fünf. Die Zeilen 3 und 4 der Tabelle beschreiben insofern denselben Zustand; mit Gleitkomma-Code gegengeprüft.

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: pchar): 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"c);
        }
    }
    return 0;
}

@energy überschreibt –target-energy für die markierte Funktion, während der Rest des Programms weiterhin mit dem globalen Level läuft. Die Rangfolge lautet seit lyxc 1.0.15C: Attribut an der Funktion > @energy(N); als Direktive im Quelltext > –target-energy > Vorgabe 3.


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

 
Das Flag –energy-stats gibt es nicht. Es steht weder in –help noch im Compiler-Quelltext. Seit lyxc 1.0.15D wird es nicht mehr still verschluckt, sondern mit lyxc: unbekannter Schalter '–energy-stats' und Exit 2 abgewiesen. Eine Energie-Statistik gibt der Compiler damit nicht aus.
Dieser Abschnitt beschreibt einen geplanten Stand; die gezeigten Ausgaben sind Entwürfe, keine Programmausgaben.

Mit dem Flag –energy-stats soll der Compiler nach dem Build eine Schätzung des Energieverbrauchs auf Basis eines architekturspezifischen Energie-Modells erzeugen:

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 (Attributes & Pragmas)-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:

# Aufrufgraph als Grundlage der WCET-Abschaetzung
lyxc flight_computer.lyx --target=arm64 --target-energy=1 \
    --call-graph --static-analysis -o flight.elf

 
--wcet-analysis und --wcet-output gibt es nicht — beide brechen mit unbekannter Schalter und Exit 2 ab (nachgemessen mit 1.1.15B). Eine Berichtsdatei mit Worst-Case-Pfad und Zyklenzahl je Funktion erzeugt der Compiler nicht.

Was er stattdessen liefert: --call-graph gibt den statischen Aufrufgraphen aus, --static-analysis die Stack-Analyse. Die @wcet-Schranke selbst prueft der Compiler seit 1.0.14L bei jeder Uebersetzung, ganz ohne Schalter — ebenso @stack_limit. Die Zyklenzahl muss aus dem Aufrufgraphen und den Angaben des Zielprozessors von Hand ermittelt werden.

9. QBool — Probabilistisches Computing

Ein besonderer Baustein des Energy-Aware-Modells ist QBool aus der Unit std.qbool. Der Typ speichert keinen binären Wahrheitswert, sondern eine Wahrscheinlichkeit zwischen 0.0 und 1.0. Es handelt sich um einen gewöhnlichen Struct — einen eingebauten Sprachtyp qbool und Literale wie 0.92q gibt es nicht.

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. 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
    var high_conf: QBool := QBoolNew(0.92);   // 92 % Wahrscheinlichkeit "wahr"
    var low_conf:  QBool := QBoolNew(0.35);   // 35 % Wahrscheinlichkeit "wahr"
    var certain:   QBool := QBoolNew(1.0);    // deterministisch wahr
    var impossible: QBool := QBoolNew(0.0);   // 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.322
    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.948
    Print("P(mindestens eine): ");
    PrintF64(GetProbability(either));
    PrintLn("");

    var negated: QBool := QBoolNot(high_conf);
    // P(¬A) = 1 - 0.92 = 0.08
    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;
    if (temp_high > 82.0) {
        temp_q := QBoolNew(0.98);
    } else if (temp_high > 79.0) {
        temp_q := QBoolNew(0.7);
    } else if (temp_high > 76.0) {
        temp_q := QBoolNew(0.3);
    } else {
        temp_q := QBoolNew(0.05);
    }

    // Druck: zuverlässiger Sensor → deterministischer QBool
    var pressure_q: QBool;
    if (pressure_ok) { pressure_q := QBoolTrue(); } else { pressure_q := 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);
    if (alarm) { Print("Alarm ausgelöst\n"); } else { Print("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;
    if (cover_available) { cover_q := QBoolNew(0.8); } else { cover_q := QBoolNew(0.2); }

    // 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 Level 3 (entspricht -O2) — gilt ohne weitere Angabe
Energie messen Hardware-Profiling (–energy-stats ist noch nicht umgesetzt)
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 + @stack_limit (beide werden bei jeder Uebersetzung geprueft) + –call-graph + 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

std.qbool — Vollständige Funktionsreferenz
DO-178C und DAL-Annotationen
Memory Management — Stack, Heap, @stack_limit

Letzte Aktualisierung: 2026-08-11

Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).