Тесты производительности
Rust поддерживает тесты производительности, которые помогают измерить
производительность вашего кода. Давайте изменим наш src/lib.rs
, чтобы он
выглядел следующим образом (комментарии опущены):
#![feature(test)]
extern crate test;
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
use test::Bencher;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
#[bench]
fn bench_add_two(b: &mut Bencher) {
b.iter(|| add_two(2));
}
}
Обратите внимание на включение возможности (feature gate) test
, что включает
эту нестабильную возможность.
Мы импортировали контейнер test
, который включает поддержку измерения
производительности. У нас есть новая функция, аннотированная с помощью атрибута
bench
. В отличие от обычных тестов, которые не принимают никаких аргументов,
тесты производительности в качестве аргумента принимают &mut Bencher
.
Bencher
предоставляет метод iter
, который в качестве аргумента принимает
замыкание. Это замыкание содержит код, производительность которого мы хотели бы
протестировать.
Запуск тестов производительности осуществляется командой cargo bench
:
$ cargo bench
Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
Running target/release/adder-91b3e234d4ed382a
running 2 tests
test tests::it_works ... ignored
test tests::bench_add_two ... bench: 1 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured
Все тесты, не относящиеся к тестам производительности, были проигнорированы. Вы,
наверное, заметили, что выполнение cargo bench
занимает немного больше времени
чем cargo test
. Это происходит потому, что Rust запускает наш тест несколько
раз, а затем выдает среднее значение. Так как мы выполняем слишком мало полезной
работы в этом примере, у нас получается 1 ns/iter (+/- 0)
, но была бы выведена
дисперсия, если бы был один.
Советы по написанию тестов производительности:
- Внутри
iter
цикла пишите только тот код, производительность которого вы хотите измерить; инициализацию выполняйте за пределамиiter
цикла - Внутри
iter
цикла пишите код, который будет идемпотентным (будет делать «то же самое» на каждой итерации); не накапливайте и не изменяйте состояние - Вне
iter
цикла пишите код который также будет идемпотентным; скорее всего, он будет запущен много раз во время теста - Внутри
iter
цикла пишите код, который будет коротким и быстрым, так чтобы запуски тестов происходили быстро и калибратор мог настроить длину пробега с точным разрешением - Внутри
iter
цикла пишите код, делающий что-то простое, чтобы помочь в выявлении улучшения (или уменьшения) производительности
Особенности оптимизации
А вот другой сложный момент, относящийся к написанию тестов производительности: тесты, скомпилированные с оптимизацией, могут быть значительно изменены оптимизатором, после чего тест будет мерить производительность не так, как мы этого ожидаем. Например, компилятор может определить, что некоторые выражения не оказывают каких-либо внешних эффектов и просто удалит их полностью.
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
(0..1000).fold(0, |old, new| old ^ new);
});
}
выведет следующие результаты
running 1 test
test bench_xor_1000_ints ... bench: 0 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
Движок для запуска тестов производительности оставляет две возможности,
позволяющие этого избежать. Либо использовать замыкание, передаваемое в метод
iter
, которое возвращает какое-либо значение; тогда это заставит оптимизатор
думать, что возвращаемое значение будет использовано, из-за чего удалить
вычисления полностью будет не возможно. Для примера выше этого можно достигнуть,
изменив вызова b.iter
# struct X;
# impl X { fn iter<T, F>(&self, _: F) where F: FnMut() -> T {} } let b = X;
b.iter(|| {
// note lack of `;` (could also use an explicit `return`).
(0..1000).fold(0, |old, new| old ^ new)
});
Либо использовать вызов функции test::black_box
, которая представляет собой
«черный ящик», непрозрачный для оптимизатора, тем самым заставляя его
рассматривать любой аргумент как используемый.
#![feature(test)]
extern crate test;
# fn main() {
# struct X;
# impl X { fn iter<T, F>(&self, _: F) where F: FnMut() -> T {} } let b = X;
b.iter(|| {
let n = test::black_box(1000);
(0..n).fold(0, |a, b| a ^ b)
})
# }
В этом примере не происходит ни чтения, ни изменения значения, что очень дешево
для малых значений. Большие значения могут быть переданы косвенно для уменьшения
издержек (например, black_box(&huge_struct)
).
Выполнение одного из вышеперечисленных изменений дает следующие результаты измерения производительности
running 1 test
test bench_xor_1000_ints ... bench: 131 ns/iter (+/- 3)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
Тем не менее, оптимизатор все еще может вносить нежелательные изменения в определенных случаях, даже при использовании любого из вышеописанных приемов.