Профилирование Rust кода: находим узкие места и ускоряем программы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем профилировать 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 — для анализа аллокаций памяти.

Помните: всегда измеряйте, прежде

Блоги

Рекомендаций по книгам