Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Optimierungen: Der Kompromiss zwischen Geschwindigkeit und Größe

Jeder möchte, dass sein Programm extrem schnell und extrem klein ist, doch meist lassen sich nicht beide Eigenschaften zugleich verwirklichen. Dieser Abschnitt behandelt die verschiedenen Optimierungsstufen von rustc und deren Auswirkungen auf die Ausführungszeit sowie die Größe der Binärdatei eines Programms.

Keine Optimierungen

Dies ist die Standardeinstellung. Wenn Sie cargo build aufrufen, wird das Entwicklungs-Profil (auch bekannt als dev-Profil) verwendet. Da dieses Profil für das Debugging optimiert ist, werden Debug-Informationen aktiviert, jedoch keinerlei Optimierungen vorgenommen; es wird also -C opt-level=0 verwendet.

Zumindest bei der Bare-Metal-Entwicklung verursachen Debug-Informationen keine Kosten im Sinne von Speicherplatzbedarf im Flash- oder ROM-Speicher. Daher empfehlen wir, Debug-Informationen auch im Release-Profil zu aktivieren – standardmäßig sind sie dort deaktiviert. Dies ermöglicht Ihnen die Verwendung von Breakpoints beim Debuggen von Release-Builds.

[profile.release]
# Symbole sind praktisch und vergroessern die Dateigroesse im Flash nicht
debug = true

Der Verzicht auf Optimierungen ist ideal für das Debugging, da sich das schrittweise Durchlaufen des Codes anfühlt, als würde man das Programm Anweisung für Anweisung ausführen; zudem lassen sich Stack-Variablen und Funktionsargumente in GDB per print ausgeben. Bei optimiertem Code führt der Versuch, Variablen auszugeben, hingegen zur Meldung $0 = <value optimized out>.

Der größte Nachteil des dev-Profils besteht darin, dass die resultierende Binärdatei sehr groß und langsam ist. Meist wiegt vor allem die Größe schwer, da nicht optimierte Binärdateien Dutzende KiB an Flash-Speicher belegen können – Speicherplatz, über den das Zielgerät womöglich gar nicht verfügt. Die Folge: Die nicht optimierte Binärdatei passt nicht auf das Gerät!

Gibt es eine Möglichkeit, kleinere und dennoch für das Debugging geeignete Binärdateien zu erhalten? Ja, es gibt einen Trick.

Optimierung der Abhängigkeiten

Cargo bietet mit Profil-Überschreibung eine Funktion, mit der Sie den Optimierungsgrad von Abhängigkeiten überschreiben können. So lassen sich alle Abhängigkeiten hinsichtlich ihrer Größe optimieren, während die oberste Crate unoptimiert und debuggfreundlich bleibt.

Beachten Sie, dass generischer Code manchmal zusammen mit der Crate optimiert werden kann, in der er instanziiert wird, anstatt mit der Crate, in der er definiert ist. Wenn Sie in Ihrer Anwendung eine Instanz einer generischen Struktur erstellen und feststellen, dass diese Code mit großem Speicherbedarf einbindet, kann es sein, dass eine Erhöhung des Optimierungsgrads der relevanten Abhängigkeiten keine Wirkung zeigt.

Hier ist ein Beispiel:

# Cargo.toml
[package]
name = "app"
# ..

[profile.dev.package."*"] # +
opt-level = "z" # +

Ohne die Überschreibung:

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 9060   0x8000400
.rodata               1708   0x8002780
.data                    0  0x20000000
.bss                     4  0x20000000

Mit der Überschreibung

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 3490   0x8000400
.rodata               1100   0x80011c0
.data                    0  0x20000000
.bss                     4  0x20000000

Das bedeutet eine Verringerung des Flash-Speicherverbrauchs um 6 KiB, ohne die Debug-Fähigkeit des Haupt-Crates zu beeinträchtigen. Wenn man in eine Abhängigkeit hineinspringt, erscheinen zwar wieder die Meldungen <value optimized out>, doch in der Regel möchte man das Haupt-Crate debuggen und nicht die Abhängigkeiten. Sollte es dennoch erforderlich sein, eine Abhängigkeit zu debuggen, lässt sich die Funktion profile-overrides nutzen, um diese spezifische Abhängigkeit von der Optimierung auszunehmen. Siehe dazu das folgende Beispiel:

# ..

# Optimiere das `cortex-m-rt`-Crate nicht
[profile.dev.package.cortex-m-rt] # +
opt-level = 0 # +

# optimieren Sie jedoch alle anderen Abhaengigkeiten
[profile.dev.package."*"]
codegen-units = 1 # better optimizations
opt-level = "z"

Jetzt sind das Top-Level-Crate und cortex-m-rt debuggerfreundlich!

Auf Geschwindigkeit optimieren

Seit dem 18.09.2018 unterstützt rustc drei Stufen der Geschwindigkeitsoptimierung: opt-level = 1, 2 und 3. Beim Ausführen von cargo build --release wird das Release-Profil verwendet, das standardmäßig auf opt-level = 3 eingestellt ist.

Sowohl opt-level = 2 als auch 3 optimieren auf Geschwindigkeit zulasten der Binärgröße; allerdings führt Stufe 3 mehr Vektorisierung und Inlining durch als Stufe 2. Insbesondere ist zu beobachten, dass LLVM bei einem opt-level von 2 oder höher Schleifen entrollt („Loop Unrolling“). Das Entrollen von Schleifen ist recht kostspielig im Hinblick auf den Flash-/ROM-Speicherbedarf (z. B. Anstieg von 26 auf 194 Bytes für eine Schleife zum Nullsetzen eines Arrays), kann aber unter geeigneten Bedingungen (etwa bei einer ausreichend hohen Anzahl von Iterationen) die Ausführungszeit halbieren.

Derzeit gibt es keine Möglichkeit, das Entrollen von Schleifen bei opt-level = 2 und 3 zu deaktivieren; wenn Sie sich diesen Speicheraufwand also nicht leisten können, sollten Sie Ihr Programm stattdessen auf eine geringe Größe hin optimieren.

Nach Größe optimieren

Seit dem 18.09.2018 unterstützt rustc zwei Stufen zur Größenoptimierung: opt-level = "s" und "z". Diese Bezeichnungen wurden von Clang/LLVM übernommen und sind nicht besonders aussagekräftig; allerdings soll das "z" signalisieren, dass damit kleinere Binärdateien erzeugt werden als mit "s".

Wenn Ihre Release-Binärdateien hinsichtlich der Größe optimiert werden sollen, ändern Sie die Einstellung profile.release.opt-level in der Datei Cargo.toml wie unten dargestellt.

[profile.release]
# oder "z"
opt-level = "s"

Diese beiden Optimierungsstufen senken den Inline-Schwellenwert von LLVM erheblich – einen Kennwert, der darüber entscheidet, ob eine Funktion „geinlined“ (direkt an der Aufrufstelle eingebettet) wird oder nicht. Eines der Prinzipien von Rust sind „Zero-Cost-Abstractions” (Abstraktionen ohne Laufzeitkosten); diese nutzen häufig sogenannte „Newtypes“ und kleine Funktionen zur Wahrung von Invarianten (z. B. Funktionen wie deref oder as_ref, die einen inneren Wert ausleihen). Ein niedriger Inline-Schwellenwert kann daher dazu führen, dass LLVM Optimierungsmöglichkeiten verpasst (etwa das Entfernen von nicht erreichbaren Programmzweigen oder das Inlinen von Closure-Aufrufen).

Bei der Optimierung auf eine geringe Binärgröße kann es sinnvoll sein, den Inline-Schwellenwert zu erhöhen und zu prüfen, ob sich dies auf die Größe der Binärdatei auswirkt. Die empfohlene Methode zur Änderung dieses Schwellenwerts besteht darin, das Flag -C inline-threshold zu den übrigen rustflags in der Datei .cargo/config.toml hinzuzufügen.

# .cargo/config.toml
# Dies setzt voraus, dass Sie die cortex-m-quickstart-Vorlage verwenden
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
  # ..
  "-C", "inline-threshold=123", # +
]

Welchen Wert verwenden? Ab Version 1.29.0 gelten für die verschiedenen Optimierungsstufen folgende Inline-Schwellenwerte:

  • opt-level = 3 verwendet 275
  • opt-level = 2 verwendet 225
  • opt-level = "s" verwendet 75
  • opt-level = "z" verwendet 25

Du solltest 225 und 275 ausprobieren, wenn du auf die Größe optimierst.