Введение: зачем профилировать Rust код?
Rust славится своей скоростью и безопасностью, но даже самый быстрый код может работать медленно из-за неочевидных узких мест. Профилирование — это процесс измерения времени выполнения, использования памяти и других ресурсов, который помогает найти "бутылочные горлышки". Без профилирования оптимизация превращается в гадание: вы можете "улучшать" участки кода, которые вообще не влияют на производительность.
В этой статье мы разберем основные инструменты профилирования Rust: perf, flamegraph, cargo-flamegraph и встроенный бенчмаркинг. Вы узнаете, как находить медленные функции, аллокации и кэш-промахи, а также как интерпретировать полученные данные.
Инструменты профилирования Rust: выбираем правильный подход
1. Встроенный бенчмаркинг с #[bench]
Первый шаг к профилированию — написание бенчмарков. В Rust есть встроенная поддержка бенчмарков (нестабильная, требуется nightly). Пример:
// В файле 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(|| {
// Тестируемая функция
let _ = data.iter().sum::<i32>();
});
}
Запуск: cargo bench. Вы получите точные замеры времени, но это не покажет, где именно тратится время внутри функции.
2. perf — профилировщик на уровне ядра
Инструмент perf (Linux) собирает выборки счётчиков процессора. Он показывает, какие строки кода выполняются чаще всего.
Установка: sudo apt install linux-tools-common linux-tools-generic.
Пример использования:
# Компилируем с отладочными символами
cargo build --release
# Запускаем профилирование
sudo perf record ./target/release/my_program
# Смотрим отчёт
sudo perf report
В отчёте вы увидите функции, отсортированные по проценту времени выполнения. Это первый кандидат на оптимизацию.
3. flamegraph — визуализация стека вызовов
Flamegraph — это SVG-диаграмма, где ширина каждого прямоугольника соответствует времени выполнения функции. Чем шире прямоугольник, тем больше времени тратится в этой функции или её дочерних вызовах.
Установка cargo-flamegraph:
cargo install flamegraph
Запуск:
cargo flamegraph --bin my_program
Результат — файл flamegraph.svg. Откройте его в браузере. Ищите широкие "плато" — это узкие места.
Практический пример: находим узкое место в Rust коде
Рассмотрим простую программу, которая обрабатывает большой массив данных:
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);
}
Скомпилируем и запустим профилирование:
cargo build --release
cargo flamegraph --bin my_program
Открыв flamegraph, мы видим, что основное время тратится на цикл for и вызов wrapping_mul. Заметим, что Vec::push может вызывать переаллокации, если ёмкость не рассчитана точно. В нашем случае with_capacity задан правильно, но сам цикл — узкое место.
Оптимизация: используем итераторы и map:
fn process_optimized(data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| b.wrapping_mul(2)).collect()
}
Повторный замер показывает ускорение на 10-15% за счёт векторизации (автовекторизации LLVM).
Продвинутые техники: аллокации и кэш-промахи
Профилирование аллокаций с dhat
Иногда узким местом являются не CPU-циклы, а аллокации памяти. Крейт dhat позволяет отслеживать каждое выделение и освобождение памяти.
Добавьте в Cargo.toml:
[dependencies]
dhat = "0.1"
Пример использования:
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
let _profiler = dhat::Profiler::new_heap();
// ваш код
let v = vec![0u8; 1000];
drop(v);
}
После запуска вы получите отчёт о количестве аллокаций, их размерах и местах в коде. Это помогает найти лишние копирования или частые выделения маленьких объектов.
Кэш-промахи с perf stat
Кэш-промахи (cache misses) — частая причина медленной работы. Проверить их можно с помощью perf stat:
perf stat -e cache-references,cache-misses ./target/release/my_program
Высокий процент cache-misses (более 20%) указывает на неэффективное использование памяти. Оптимизация: изменить структуру данных (например, использовать Vec вместо HashMap), переупорядочить поля структур или использовать #[repr(C)].
Заключение
Профилирование Rust кода — это не роскошь, а необходимость для создания быстрых приложений. Мы рассмотрели четыре основных инструмента:
- Бенчмарки — для точных замеров отдельных функций.
- perf — для поиска горячих точек на уровне CPU.
- Flamegraph — для визуализации стека вызовов.
- dhat — для анализа аллокаций памяти.
Помните: всегда измеряйте, прежде