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)] fn main() { 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); }); } }#![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
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; 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
Тем не менее, оптимизатор все еще может вносить нежелательные изменения в определенных случаях, даже при использовании любого из вышеописанных приемов.