Линтер Rust отлично справляется с поиском неиспользуемых переменных. Однако меня интересуют переменные, которые при определенных условиях (режим выпуска, функции) не используются.
Простой пример выглядит примерно так:
pub struct Thing {
pub values: [u64; 5],
}
pub fn do_stuff(thing: &mut Thing) {
let before = thing.clone();
thing.values[0] = 1;
tracing::debug!("before: {:?}, after: {:?}", before, thing);
}
В режиме отладки мы хотели бы увидеть изменения в do_stuff
. Но в продакшене мы включаем tracing/release_max_level_warn
, чтобы избежать кучи отладочной печати (важно в разделах, критичных к производительности). В линтере явно не упоминается, что before
не используется, поскольку в некоторых случаях он используется.
По сути, этот вызов .clone()
в конечном итоге оптимизирован в сборках выпуска? Или есть способ найти неиспользуемые переменные, такие как «до», принимая во внимание флаги функций?
Вот тест, который выглядит так, будто его не оптимизируют (даже в виде неиспользуемой переменной, никакой путаницы в функциях)? Я использовал Box
, чтобы это не могло быть просто memcopy
, но я плохо разбираюсь в тестах, поэтому мне бы хотелось ошибиться.
#[derive(Clone, Debug)]
pub struct Thing {
pub values: Vec<Box<u64>>,
}
#[inline(never)]
pub fn cloned(thing: &mut Thing) {
let before = thing.clone();
thing.values[0] = Box::new(2);
}
#[inline(never)]
pub fn uncloned(thing: &mut Thing) {
thing.values[0] = Box::new(2);
}
#[cfg(test)]
mod tests {
use super::test::Bencher;
#[bench]
fn bench_clone_test(b: &mut Bencher) {
let mut thing = crate::Thing {
values: vec![Box::new(1); 1000],
};
b.iter(|| crate::cloned(&mut thing));
}
#[bench]
fn bench_uncloned_test(b: &mut Bencher) {
let mut thing = crate::Thing {
values: vec![Box::new(1); 1000],
};
b.iter(|| crate::uncloned(&mut thing));
}
}
Полученные результаты:
test tests::bench_clone_test ... bench: 54,280.90 ns/iter (+/- 241.36)
test tests::bench_uncloned_test ... bench: 31.91 ns/iter (+/- 0.39)
Хммм, согласно этому: reddit.com/r/rust/comments/jj7z7f/… (старый, Reddit и т. д.). По сути, компилятор добавляет вызов Drop::drop(var)
в конце функции, поэтому они на самом деле не являются «неиспользуемыми», если только я не предполагаю, что функция drop ничего не делает (примитивы? не стековые?). Что соответствует моему эталону выше.
Кажется, что клон был оптимизирован, когда я компилирую верхний код при проверке сборки (одна mov
инструкция для записи). Однако ваш тестовый код отличается и включает Vec
(то есть динамическую память), которую гораздо сложнее оптимизировать, особенно потому, что также используется Box
(более динамическая память).
Ага. Я специально использовал Vec/Box, чтобы лучше имитировать то, что есть в моей кодовой базе. Я предполагаю, что компилятор справится нормально (и влияние на производительность будет намного ниже) с примитивными типами/копиями.
Немного актуальной дискуссии от УРЛО.
Радикальная идея: вы можете создавать свои собственные макросы trace
/debug
/info
, которые заключают макросы трассировки в блок #[cfg(debug_assertions)] { ... }
. Это вызовет предупреждение о «неиспользуемой переменной» для before
в профиле --release
.
Да, я только что работал над этим маршрутом, и похоже, это то, что я хочу. Хотите, чтобы это был «ответ», и я нажму кнопку «Принять»? Я думаю, что меня запутала формулировка на ящике с отслеживанием их особенностей (моя ошибка, а не их). Я ожидал, что макрос debug!
по сути просто расширится до нуля, когда функция так сказала, а не просто расширится до if false {
. Я создал обертку debug!()
, которая в нужный момент раскрывается до нуля, и теперь все неиспользуемые переменные помечены правильно.
Переменные, переданные в tracing::debug!
, считаются «использованными», даже если они отключены функцией, поскольку макрос расширяется до следующего вида:
if tracing::Level::DEBUG <= tracing::level_filters::STATIC_MAX_LEVEL ... {
...
}
который, по мнению компилятора, все еще используется, даже если условие постоянно.
Чтобы действительно избежать компиляции этого кода, его необходимо скрыть за атрибутом #[cfg]
или макросом cfg!
. Это можно сделать с помощью макроса трассировки, но существует неписаное правило, согласно которому следует избегать ошибок и предупреждений, характерных для профиля или цели.
Чтобы добиться такого поведения для себя, вы можете создать свои собственные макросы, которые будут проксировать макросы трассировки, обернутые #[cfg(debug_assertions)]
(это неточный, но практичный способ обнаружения сборок релизов). Вот это работает в действии:
#[derive(Debug, Clone)]
pub struct Thing {
values: [u64; 5],
}
macro_rules! debug {
($($args:tt)*) => {
#[cfg(debug_assertions)] { tracing::debug!($($args)*) }
};
}
pub fn do_stuff(thing: &mut Thing) {
let before = thing.clone();
thing.values[0] = 1;
debug!("before: {:?}, after: {:?}", before, thing);
}
Создание этого кода с помощью --release
выдаст предупреждение:
warning: unused variable: `before`
--> src/lib.rs:13:9
|
13 | let before = thing.clone();
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_before`
|
= note: `#[warn(unused_variables)]` on by default
Расширение макроса
tracing::debug
нечувствительно к функцииrelease_max_level_warn
, и поэтому компилятор видит, чтоbefore
всегда используется независимо: однако его использование включено в логическое значение, оцениваемое во время компиляции, поэтому по сути это то же самое, что в этом примере . Я считаю, что в режиме выпуска компилятор в таких случаях оптимизируетclone
, но я не думаю, что есть какая-либо гарантия.