Einleitung: Warum Rust-Code profilieren?
Rust ist bekannt für seine Geschwindigkeit und Sicherheit, aber selbst der schnellste Code kann aufgrund unerwarteter Engpässe langsam laufen. Profilierung ist der Prozess der Messung von Ausführungszeit, Speichernutzung und anderen Ressourcen, der hilft, "Flaschenhälse" zu finden. Ohne Profilierung wird Optimierung zum Ratespiel: Sie können Codeabschnitte "verbessern", die die Leistung überhaupt nicht beeinflussen.
In diesem Artikel werden wir die wichtigsten Profilierungswerkzeuge für Rust behandeln: perf, flamegraph, cargo-flamegraph und integriertes Benchmarking. Sie erfahren, wie Sie langsame Funktionen, Allokationen und Cache-Fehltreffer finden und wie Sie die gewonnenen Daten interpretieren.
Rust-Profilierungswerkzeuge: Den richtigen Ansatz wählen
1. Integriertes Benchmarking mit #[bench]
Der erste Schritt zur Profilierung ist das Schreiben von Benchmarks. Rust bietet integrierte Unterstützung für Benchmarks (instabil, erfordert nightly). Beispiel:
// In der Datei benches/my_bench.rs#![feature(test)]extern crate test;
use test::Bencher;
#[bench]fn bench_my_function(b: &mut Bencher) { let data = vec![1, 2, 3, 4, 5]; b.iter(|| { // Getestete Funktion let _ = data.iter().sum::<i32>(); });}Aufruf: cargo bench. Sie erhalten präzise Zeitmessungen, aber dies zeigt nicht, wo genau innerhalb der Funktion Zeit verbraucht wird.
2. perf — Profiler auf Kernel-Ebene
Das Werkzeug perf (Linux) sammelt Stichproben von Prozessor-Zählern. Es zeigt, welche Codezeilen am häufigsten ausgeführt werden.
Installation: sudo apt install linux-tools-common linux-tools-generic.
Anwendungsbeispiel:
# Mit Debug-Symbolen kompilierencargo build --release
# Profilierung startensudo perf record ./target/release/my_program
# Bericht ansehensudo perf reportIm Bericht sehen Sie Funktionen, sortiert nach dem Prozentsatz der Ausführungszeit. Dies sind die ersten Kandidaten für die Optimierung.
3. flamegraph — Visualisierung des Aufrufstapels
Flamegraph ist ein SVG-Diagramm, bei dem die Breite jedes Rechtecks der Ausführungszeit der Funktion entspricht. Je breiter das Rechteck, desto mehr Zeit wird in dieser Funktion oder ihren Unteraufrufen verbraucht.
Installation von cargo-flamegraph:
cargo install flamegraphAufruf:
cargo flamegraph --bin my_programErgebnis ist eine Datei flamegraph.svg. Öffnen Sie sie im Browser. Suchen Sie nach breiten "Plateaus" – das sind die Engpässe.
Praktisches Beispiel: Einen Engpass in Rust-Code finden
Betrachten wir ein einfaches Programm, das ein großes Datenarray verarbeitet:
fn process(data: &[u8]) -> Vec<u8> { let mut result = Vec::with_capacity(data.len()); for &byte in data { result.push(byte.wrapping_mul(2)); } result}
fn main() { let data = vec![0u8; 10_000_000]; let _ = process(&data);}Kompilieren und Profilierung starten:
cargo build --releasecargo flamegraph --bin my_programBeim Öffnen des Flamegraphs sehen wir, dass die meiste Zeit in der for-Schleife und dem Aufruf von wrapping_mul verbraucht wird. Beachten Sie, dass Vec::push Neuallokationen verursachen kann, wenn die Kapazität nicht genau berechnet ist. In unserem Fall ist with_capacity korrekt gesetzt, aber die Schleife selbst ist der Engpass.
Optimierung: Verwenden von Iteratoren und map:
fn process_optimized(data: &[u8]) -> Vec<u8> { data.iter().map(|&b| b.wrapping_mul(2)).collect()}Eine erneute Messung zeigt eine Beschleunigung von 10-15% durch Vektorisierung (Auto-Vektorisierung von LLVM).