Есть ли способ найти «скрытые» неиспользуемые переменные в Rust?

Линтер 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)

Расширение макроса tracing::debug нечувствительно к функции release_max_level_warn, и поэтому компилятор видит, что before всегда используется независимо: однако его использование включено в логическое значение, оцениваемое во время компиляции, поэтому по сути это то же самое, что в этом примере . Я считаю, что в режиме выпуска компилятор в таких случаях оптимизирует clone, но я не думаю, что есть какая-либо гарантия.

eggyal 13.06.2024 19:05

Хммм, согласно этому: reddit.com/r/rust/comments/jj7z7f/… (старый, Reddit и т. д.). По сути, компилятор добавляет вызов Drop::drop(var) в конце функции, поэтому они на самом деле не являются «неиспользуемыми», если только я не предполагаю, что функция drop ничего не делает (примитивы? не стековые?). Что соответствует моему эталону выше.

theflash 13.06.2024 19:15

Кажется, что клон был оптимизирован, когда я компилирую верхний код при проверке сборки (одна mov инструкция для записи). Однако ваш тестовый код отличается и включает Vec (то есть динамическую память), которую гораздо сложнее оптимизировать, особенно потому, что также используется Box (более динамическая память).

kmdreko 13.06.2024 19:17

Ага. Я специально использовал Vec/Box, чтобы лучше имитировать то, что есть в моей кодовой базе. Я предполагаю, что компилятор справится нормально (и влияние на производительность будет намного ниже) с примитивными типами/копиями.

theflash 13.06.2024 19:24

Немного актуальной дискуссии от УРЛО.

eggyal 13.06.2024 19:32

Радикальная идея: вы можете создавать свои собственные макросы trace/debug/info, которые заключают макросы трассировки в блок #[cfg(debug_assertions)] { ... }. Это вызовет предупреждение о «неиспользуемой переменной» для before в профиле --release.

kmdreko 13.06.2024 20:05

Да, я только что работал над этим маршрутом, и похоже, это то, что я хочу. Хотите, чтобы это был «ответ», и я нажму кнопку «Принять»? Я думаю, что меня запутала формулировка на ящике с отслеживанием их особенностей (моя ошибка, а не их). Я ожидал, что макрос debug! по сути просто расширится до нуля, когда функция так сказала, а не просто расширится до if false {. Я создал обертку debug!(), которая в нужный момент раскрывается до нуля, и теперь все неиспользуемые переменные помечены правильно.

theflash 13.06.2024 22:08
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
3
7
83
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Переменные, переданные в 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

Другие вопросы по теме