Памятка по профилированию кода на Rust
Недавно мы рассмотрели программу на Rust, чья производительность была не так высока, как хотелось бы. Озвучивалось несколько версий о том, как такое могло произойти, однако истинная причина так и осталась загадкой. Я подумал, что это отличная возможность познакомиться со средствами профилирования кода на Rust.
Напомню, что программа парсила логи Nginx при помощи регулярных выражений:
use regex::Regex;
use std::collections::HashMap;
use std::io::{self, BufRead};
const RE: &str = r#"^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d+)"#;
fn main() {
let regex = Regex::new(RE).unwrap();
let mut html_requests = HashMap::new();
// берем лок один раз, чтобы read_line() не брал-отпускал его постоянно
let mut stdin = io::stdin().lock();
// переиспользуемый буфер уменьшает количество аллокаций памяти
let mut input = String::new();
loop {
input.clear(); // очищаем строку, но не уменьшаем capacity
let Ok(bytes_read) = stdin.read_line(&mut input) else {
break;
};
if bytes_read == 0 {
break;
}
let captures = match regex.captures(&input) {
Some(captures) => captures,
None => continue,
};
let method = &captures[3];
let url = &captures[4];
let status_code: u32 = captures[5].parse().unwrap_or(0);
if status_code == 200 && method == "GET" && url.ends_with(".html") {
match html_requests.get_mut(url) {
Some(count) => *count += 1,
None => {
html_requests.insert(url.to_string(), 1);
}
}
}
}
// into_iter() - перемещает данные из HashMap, а не копирует их
// collect() - возвращает вектор кортежей (URL, счетчик)
let mut pairs: Vec<_> = html_requests.into_iter().collect();
// используем неустойчивую сортировку
pairs.sort_unstable_by_key(|(_, count)| std::cmp::Reverse(*count));
for (url, count) in pairs {
println!("{} {}", count, url);
}
}
Данный код отличается от опубликованного изначально. Бывалые программисты на Rust в лице @klebed и @bemyak предложили пару оптимизаций, см комментарии к коду. Увы, к большому ускорению данные оптимизации не привели. Разница в скорости между исходной программой и оптимизированной находится в пределах погрешности измерения. Раз такое дело, будем искать батлнек инструментально.
Далее предполагается, что читатель имеет некоторый опыт профилирования кода на C/C++, и объяснять, что такое perf и флеймграфы, не требуется. Если это не так, обратите внимание на статью Профилирование кода на C/C++ в Linux и FreeBSD.
Для Cargo есть удобный плагин flamegraph, которым мы и воспользуемся. Устанавливается он так:
$ cargo install flamegraph
Плагин работает под Windows, Linux и MacOS. Поскольку в настоящее время я сижу под Linux, то далее речь пойдет исключительно о нем.
Профилирование кода осуществляется на release-сборках. По умолчанию данные сборки не включают отладочные символы. Чтобы это исправить, в Cargo.toml дописываем:
[profile.release]
debug = true
Далее говорим:
$ cargo clean
$ cargo build -r
$ cargo flamegraph --bin nginx_log_analyzer < ./eaxme-2025-12.log
Лично я в первый раз получил ошибку:
Access to performance monitoring and observability operations is limited.
Consider adjusting /proc/sys/kernel/perf_event_paranoid setting ...
(... пропущено ...)
По умолчанию в Ubuntu 24.04 пользователи не могут профилировать даже собственные процессы. Исправим это, сказав:
$ sudo sysctl -w kernel.perf_event_paranoid=1
А чтобы настройка не слетала после перезагрузки, в /etc/sysctl.conf допишем:
kernel.perf_event_paranoid=1
Повторяем команду cargo flamegraph ... и получаем симпатичный файл flamegraph.svg, вроде такого (кликабельно):
Изучив четвертую строчку снизу, мы понимаем, что программа проводит ~70% времени в зависимостях модуля regex. Таким образом, мы установили, что бутылочном горлышком является конкретная реализация регулярных выражений. Чтобы ускорить программу, следует либо переписать ее без регулярных выражений, либо попробовать байндинги к условному libpcre.
Как видите, пользоваться cargo flamegraph – одно удовольствие. Больше информации можно почерпнуть из документации к плагину на crates.io и cargo flamegraph --help.