Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Тема
Описание
Доп.

crate cargo-remark

crate cargo-pgo

Подкоманда Cargo для проверки замечаний по оптимизации LLVM, сгенерированных программами Rust

github/cargo-pgo

github/cargo-remark

rustup default nightly
cargo install cargo-remark
cargo remark build  --open

cargo install cargo-pgo
rustup component add llvm-tools-preview
cargo pgo build
cargo pgo run
cargo remark wrap -- pgo optimize

Open target/remarks/web/index.html 

crate cargo-bloat для анализа размера бинарника

cargo install cargo-bloat

Затем выполните:
cargo bloat --release

Это покажет размер бинарника и разбиение на модули, чтобы вы могли понять, что занимает место.

crate criterion - Статистически точный инструмент для сравнительного анализа библиотек

crate divan - Простая, но мощная библиотека для сравнительного анализа с профилированием распределения

crate hyperfine - Инструмент для сравнительного анализа скомпилированных двоичных файлов (похож на команду unix time, но лучше)

crate cargo-wizard - автоматизировать процесс настройки Cargo

cargo install cargo-wizard

$ cargo wizard apply <template> <profile>
# For example, apply `fast-runtime` template to the `dist` profile
$ cargo wizard apply fast-runtime dist

Changed Cargo.toml:
[profile.dist]
inherits = "release"
opt-level = 3
debug = false
strip = "none"
lto = true
codegen-units = 1
incremental = false
panic = "abort"

fast-compile - минимизирует время компиляции Отключает генерацию отладочной информации и использует более быстрый компоновщик. В ночном режиме он также включает бэкэнд кодегенерации Cranelift и параллельный фронтэнд.

fast-runtime - максимизирует производительность во время выполнения Включает LTO и другие настройки, предназначенные для максимизации производительности во время выполнения.

min-size - минимизирует размер двоичного файла Аналогично fast-runtime, но использует флаги оптимизации, предназначенные для небольшого размера двоичного файла.

Доступ к этим значениям может занять нетривиальное количество времени. Если к нескольким таким значениям обычно обращаются вместе, может быть лучше поместить их в одну оболочку.

struct S { ❌ 
    x: Arc<Mutex<u32>>,
    y: Arc<Mutex<u32>>,
}

// может быть лучше представлено так:

struct S {✅
    xy: Arc<Mutex<(u32, u32)>>,
}
# The development profile, used for `cargo build`.
[profile.dev]
opt-level = 0       # Контролирует уровень оптимизации, установлен на минимальный/ controls the `--opt-level` the compiler builds with
debug = true       # Включает генерацию отладочной информации `-g`
rpath = false       # Отключает передачу `-C rpath` компилятору
lto = false            # Отключает оптимизацию на этапе связывания (Link Time Optimization) / controls `-C lto` for binaries and staticlibs
debug-assertions = true # Включает отладочные проверки (assertions)
codegen-units = 1  # Устанавливает количество юнитов кодогенерации на 1 `-C codegen-units`
                   # `codegen-units` is ignored when `lto = true`
panic = 'unwind'   # panic strategy (`-C panic=...`), can also be 'abort'
 

# The release profile, used for `cargo build --release`.
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = false
debug-assertions = false
codegen-units = 1
panic = 'unwind'

# The testing profile, used for `cargo test`.
[profile.test]
opt-level = 0
debug = true
rpath = false
lto = false
debug-assertions = true
codegen-units = 1
panic = 'unwind'

# The benchmarking profile, used for `cargo bench`.
[profile.bench]
opt-level = 3
debug = false
rpath = false
lto = false
debug-assertions = false
codegen-units = 1
panic = 'unwind'

# The documentation profile, used for `cargo doc`.
[profile.doc]
opt-level = 0
debug = true
rpath = false
lto = false
debug-assertions = true
codegen-units = 1
panic = 'unwind'

Пример bench

Примеры из llogiq.github.io

#![allow(unused)]

fn main() {
extern crate test;
extern crate rand;

use test::Bencher;
use rand::Rng;
use std::mem::replace;

#[bench]
fn empty(b: &mut Bencher) {
    b.iter(|| 1)
}

#[bench]
fn setup_random_hashmap(b: &mut Bencher) {
    let mut val : u32 = 0;
    let mut rng = rand::IsaacRng::new_unseeded();
    let mut map = std::collections::HashMap::new();

    b.iter(|| { map.insert(rng.gen::() as usize, val); val += 1; })
}

#[bench]
fn setup_random_vecmap(b: &mut Bencher) {
    let mut val : u32 = 0;
    let mut rng = rand::IsaacRng::new_unseeded();
    let mut map = std::collections::VecMap::new();

    b.iter(|| { map.insert((rng.gen::()) as usize, val); val += 1; })
}

#[bench]
fn setup_random_vecmap_cap(b: &mut Bencher) {
    let mut val : u32 = 0;
    let mut rng = rand::IsaacRng::new_unseeded();
    let mut map = std::collections::VecMap::with_capacity(256);

    b.iter(|| { map.insert((rng.gen::()) as usize, val); val += 1; })
}
}

benchmark замер времени работы функции


 // выдает постоянный итератор чисел, берем n
 fn random(n: usize) -> Vec {
    let mut r = 92;
    std::iter::repeat_with(move || {
        r ^= r << 13;
        r ^= r >> 17;
        r ^= r << 5;
        r
    }).take(n).collect()
}

#[inline(never)]
fn run_benchmark T, T>(name: &str, f: F) -> Vec {
    println!("{}:", name);
    let n = 300;
    let mut res = Vec::with_capacity(n);
    let mut times = Vec::with_capacity(n);
    for _ in 0..n {
        let start = std::time::Instant::now();
        res.push(f());
        times.push(start.elapsed());
    }
    println!("{:?}", times.into_iter().min().unwrap());
    println!("\n");
    res
}

// Ф-ция для замера
// используем get_unchecked — unsafe функцию, не делающую проверку индексов
#[inline(never)]
unsafe fn i_sum_unchecked(xs: &[u32], indexes: &[usize]) -> u32
{
    let mut sum = 0u32;
    for &idx in indexes {
        let x = unsafe { *xs.get_unchecked(idx) };  
        sum = sum.wrapping_add(x);
    }
    sum
}
// Ф-ция для замера
// запрещаем inline , чтобы не соптимизировать в константу;
//  indexes — массив [0, 1, ... xs.len()] , но компилятор об
// этом не знает (из-за #[inline(never)] );
// используем wrapping_add чтобы избежать проверок на переполнение;
#[inline(never)]  
fn i_sum(xs: &[u32], indexes: &[usize]) -> u32
{
    let mut sum = 0u32;
    for &idx in indexes {
        let x = xs[idx];
        sum = sum.wrapping_add(x);  
    }
    sum
}

fn main() {
     let n: usize = 100_000_000;;
    let indexes: Vec = (0..n).collect();
    let xs: Vec = random(n);
    //println!("{:?}",xs);// [24873849, 1921449235, 163429281]
  
 
     let r1 = run_benchmark("i_sum", || {
        i_sum(&xs, &indexes)
     });
     
     let r2 = run_benchmark("i_sum_unchecked", || {
        unsafe {i_sum_unchecked(&xs, &indexes)}
     });
     assert_eq!(r1, r2);
}

Использование cargo флагов

Используйте флаги cargo для оптимизации времени сборки:

# Используйте этот флаг для параллельной компиляции
$ export CARGO_BUILD_JOBS=$(nproc)

Параллельная компиляция

Убедитесь, что ваш Cargo.toml настроен на использование параллельной компиляции:

[profile.dev]
codegen-units = 16  # Default is 16, adjust as needed
incremental = true  # Enable incremental compilation

[profile.release]
codegen-units = 1  # Reduce for better optimizations
incremental = false  # Disable for final release builds
lto = true  # Enable Link Time Optimization

Кэширование и инкрементальная компиляция

Инкрементальная компиляция может значительно ускорить повторные сборки:

[profile.dev]
incremental = true

[profile.release]
incremental = false

Использование crate sccache

Установка и использование sccache для кэширования сборки может сильно ускорить сборку:

# Установка sccache
$ cargo install sccache

# Настройка использования sccache
$ export RUSTC_WRAPPER=sccache

Минимизация зависимостей в [dev-dependencies]

Убедитесь, что зависимости для разработки установлены только для тестов и не используются в основной сборке.

Обновление зависимости

Обновление зависимости до последних версий может иногда улучшить производительность:

$ cargo update

Использование crate cargo-udeps

(неиспользуемые зависимости)

cargo-udeps поможет вам обнаружить неиспользуемые зависимости:

$ cargo install cargo-udeps

Run:

$ cargo +nightly udeps --all-targets

Добавление исключений для cargo-udeps

Если cargo-udeps все еще находит ложные срабатывания, вы можете настроить Cargo.toml, чтобы игнорировать некоторые зависимости:

Cargo.toml:

[package.metadata.cargo-udeps.ignore]
dependencies = ["anyhow", "tokio-stream"]

Полная настройка

cargo install sccache
export RUSTC_WRAPPER=sccache
export CARGO_BUILD_JOBS=$(nproc)

File Cargo.toml:

build = "build.rs"

[profile.dev]
codegen-units = 16
incremental = true

[profile.release]
codegen-units = 1
incremental = false
lto = true

File build.rs:


use std::{env, thread};
fn main() -> Result<(), Box> {
    env::set_var("RUSTC_WRAPPER", "sccache");
    if let Ok(par) = thread::available_parallelism() {
        env::set_var("CARGO_BUILD_JOBS", par.get().to_string());
    }
   Ok(())
} 

File rust-toolchain.toml:

[toolchain]
channel = "stable"  
components = ["sccache"]
profile = "minimal" 

Что нужно для бенчмаркинга

Во-первых, вам нужны рабочие нагрузки для измерения. В идеале у вас должно быть множество рабочих нагрузок, которые представляют реалистичное использование вашей программы. А микробенчмаркинг и стресс-тесты могут быть полезны в умеренных количествах

Во-вторых, вам нужен способ запуска рабочих нагрузок, что также будет определять используемые показатели.

  1. crates bencher
  2. crates criterion является более сложной альтернативой.

crates bencher

File my_projects/benches/example.rs:

#[macro_use]
extern crate bencher;
extern crate criterion_example;
use my_project::add_two2; 
use bencher::Bencher;
fn a(bench: &mut Bencher) {
    bench.iter(|| {
        (0..1000).fold(0, |x, y| x + y)
    })
}
fn b(bench: &mut Bencher) {
    const N: usize = 1024;
    bench.iter(|| {
        vec![0u8; N]
    });
    bench.bytes = N as u64;
}
fn bench_add_two(b: &mut Bencher) {
    b.iter(|| add_two2(2));
}
benchmark_group!(benches, a, b, bench_add_two);
benchmark_main!(benches);

File my_projects/lib.rs:
pub fn add_two2(a: i32) -> i32 {
    a + 2
}

File my_projects/Cargo.toml:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
bencher = "0.1"
[[bench]]
name = "example"
harness = false

Запуск:

$ cargo bench

Output:

running 3 tests
test a             ... bench:           0 ns/iter (+/- 0)
test b             ... bench:          94 ns/iter (+/- 191) = 10893 MB/s
test bench_add_two ... bench:           2 ns/iter (+/- 0)

Что такое микробенчмаркинг?

Рабочие нагрузки с использованием реальных входных данных нужно тестировать производительность целого блока

what-is-microbenchmarking

микробенчмаркинг — это измерение производительности чего-то «маленького», например системного вызова ядра операционной системы.

Опасность заключается в том, что люди могут использовать любые результаты, полученные в результате микробенчмаркинга, для определения оптимизации. И как мы все знаем:

Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация — корень всех зол», — Дональд Кнут.

Может быть много факторов, которые искажают результат микробенчмарков. Оптимизация компилятора — одна из них. Если измеряемая операция занимает так мало времени, что все, что вы используете для ее измерения, занимает больше времени, чем сама операция, ваши микротесты также будут искажены.

Например, кто-то может взять микробенчмарк накладных расходов на for циклы:

void TestForLoop()
{
    time start = GetTime();

    for(int i = 0; i < 1000000000; ++i)
    {
    }

    time elapsed = GetTime() - start;
    time elapsedPerIteration = elapsed / 1000000000;
    printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
}

Очевидно, что компиляторы могут видеть, что цикл абсолютно ничего не делает и вообще не генерирует никакого кода для цикла. Таким образом, значение elapsed и elapsedPerIteration в значительной степени бесполезно.

Даже если цикл что-то делает:

void TestForLoop()
{
    int sum = 0;
    time start = GetTime();

    for(int i = 0; i < 1000000000; ++i)
    {
        ++sum;
    }

    time elapsed = GetTime() - start;
    time elapsedPerIteration = elapsed / 1000000000;
    printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
}

Компилятор может увидеть, что переменная sum не будет использоваться ни для чего, и оптимизировать ее, а также оптимизировать цикл for. Но ждать! Что, если мы сделаем это:

void TestForLoop()
{
    int sum = 0;
    time start = GetTime();

    for(int i = 0; i < 1000000000; ++i)
    {
        ++sum;
    }

    time elapsed = GetTime() - start;
    time elapsedPerIteration = elapsed / 1000000000;
    printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
    printf("Sum: %d\n", sum); // Added
}

Компилятор может быть достаточно умен, чтобы понять, что sum это всегда будет постоянное значение, и также оптимизировать все это. Многие были бы удивлены оптимизирующими возможностями современных компиляторов.

Но как быть с вещами, которые компиляторы не могут оптимизировать?

void TestFileOpenPerformance()
{
    FILE* file = NULL;
    time start = GetTime();

    for(int i = 0; i < 1000000000; ++i)
    {
        file = fopen("testfile.dat");
        fclose(file);
    }

    time elapsed = GetTime() - start;
    time elapsedPerIteration = elapsed / 1000000000;
    printf("Time elapsed for each file open: %d\n", elapsedPerIteration);
}

Даже это не полезный тест! Операционная система может видеть, что файл открывается очень часто, поэтому может предварительно загрузить его в память для повышения производительности. Практически все операционные системы делают это. То же самое происходит, когда вы открываете приложения — операционные системы могут определить топ-5 приложений, которые вы открываете чаще всего, и предварительно загрузить код приложения в память при загрузке компьютера!

Фактически, в игру вступает бесчисленное множество переменных: локальность ссылок (например, массивы и связанные списки), влияние кешей и пропускной способности памяти, встраивание компилятора, реализация компилятора, переключатели компилятора, количество процессорных ядер, оптимизация на уровне процессора. , планировщики операционной системы, фоновые процессы операционной системы и т. д.

Так что во многих случаях микробенчмаркинг не совсем полезная метрика. Он определенно не заменяет тесты всей программы четко определенными тестовыми примерами (профилированием). Сначала напишите читаемый код, а затем профилируйте, чтобы увидеть, что нужно сделать, если нужно.

Хочу подчеркнуть, что микробенчмарки сами по себе не зло , но пользоваться ими нужно осторожно (это верно и для многих других вещей, связанных с компьютерами)

cargo bench // Benchmark tests

Контрольные тесты производительности

std::hint::black_box()

book/benchmark-tests

book/criterion_rs

std::hint::black_box

Rust поддерживает тесты производительности, которые могут проверить производительность вашего кода.

С помощью тестовых тестов вы можете тестировать и измерять скорость кода, однако эталонные тесты по-прежнему нестабильны. Чтобы включить тесты в вашем грузовом проекте, вам нужна ночная ржавчина, поставьте тесты интеграции в папку benches/ в корне вашего проекта Cargo и запустите cargo bench

/// cargo bench --verbose
/// cargo bench --verbose -- fib_20
///
// "fib_20" - любое уникальное имя теста
// Использование std::hint::black_box() функции не позволяет компилятору свернуть всю функцию константой и заменить ее константой.
fn criterion_benchmark(c: &mut Criterion) {
    //c.bench_function("fib_20", |b:&mut criterion::Bencher| b.iter(|| fibonacci(black_box(20))));
    //c.bench_function("fib_20", |b:&mut criterion::Bencher| b.iter(|| fibonacci_2(black_box(20))));

    // Передача данных для теста
    /*let size: u64 = 20;
    c.bench_with_input(BenchmarkId::new("fib_20", size), &size, |b, &s| {
        b.iter(|| fibonacci_2(s));
    });*/

    // Множество данных для теста
   /* let size: usize = 20;
    let mut group = c.benchmark_group("fib_20");
    for s in [size, 2 + size, 4 + size, 8 + size, 16 + size].iter() {
        group.throughput(Throughput::Bytes(*s as u64));
        group.bench_with_input(BenchmarkId::from_parameter(s), s, |b, &s| {
            b.iter(|| std::iter::repeat(0u8).take(s).collect::<Vec<_>>());
        });
    }
    group.finish();
    */

    // Сравнение функций
    // cargo bench --verbose -- Fibonacci
    let mut group = c.benchmark_group("Fibonacci");
    for i in [20u64, 21u64].iter() {
        group.bench_with_input(BenchmarkId::new("Recursive", i), i, |b, i| b.iter(|| fibonacci(*i)));
        group.bench_with_input(BenchmarkId::new("Iterative", i), i, |b, i| b.iter(|| fibonacci_2(*i)));
    }
    group.finish();
}
fn main(){
// Здесь мы вызываем макрос criterion_group! (link), чтобы сгенерировать группу тестов под названием
// benches, содержащую criterion_benchmark функцию, определенную ранее.
    criterion_group!(benches, criterion_benchmark);
// мы вызываем макрос criterion_main! (link), чтобы сгенерировать основную функцию, которая выполняет benches группу.
    criterion_main!(benches);
}
#![allow(unused)]

fn main() {
pub fn factorial(n: u128) -> u128 {
    match n {
        0 => 1,
        n => n * factorial(n - 1),
    }
}
}

При фиксированных входных данных и небольшом объеме тестируемого кода компилятор способен оптимизировать итерацию и напрямую выдавать результат, что приводит к нереалистично оптимистичному результату.

❌ Наивный тест для этого кода:

#![feature(test)]
extern crate test;

#[bench]
fn bench_factorial(b: &mut test::Bencher) {
    b.iter(|| {
        let result = factorial(15);
        assert_eq!(result, 1_307_674_368_000);
    });
}

дает невероятно положительные результаты:

test bench_factorial             ... bench:           0 ns/iter (+/- 0)

✅ Переносим код бенчмарка, чтобы использовать эту подсказку:

#[bench]
fn bench_factorial(b: &mut test::Bencher) {
    b.iter(|| {
        let result = factorial(std::hint::black_box(15));
        assert_eq!(result, 1_307_674_368_000);
    });
}

дает более реалистичные результаты:

test blackboxed::bench_factorial ... bench:          16 ns/iter (+/- 3)

Criterion.rs — это инструмент микро-бенчмаркинга, основанный на статистике. Это порт Rust библиотеки Haskell Criterion.

Бенчмарки Criterion.rs собирают и хранят статистическую информацию от запуска к запуску и могут автоматически обнаруживать снижение производительности, а также измерять оптимизацию.

Cargo.toml:

[dev-dependencies]
criterion = "0.3"

[[bench]]
name = "my_benchmark"
harness = false

/src/lib.rs

Что тестировать

#[inline]
pub fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 1,
        1 => 1,
        n => fibonacci(n-1) + fibonacci(n-2),
    }
}

#[inline]
pub fn fibonacci_2(n: u64) -> u64 {
    let mut a = 0;
    let mut b = 1;

    match n {
        0 => b,
        _ => {
            for _ in 0..n {
                let c = a + b;
                a = b;
                b = c;
            }
            b
        }
    }
}

Минимальный пример

File benches/sample.rs:

#![feature(test)]   // #[bench] is still experimental
extern crate test; 
                
use test::{black_box, Bencher};

#[bench]
fn my_algo(b: &mut Bencher) {
    b.iter(|| black_box(my_crate::f())); // `black_box` prevents `f` from being optimized away.
}

File /bench/benches/my_benchmark.rs:


use criterion::{black_box, criterion_group, criterion_main, Criterion,BenchmarkId, Throughput};
use bench::{fibonacci,fibonacci_2};
/*
fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 1,
        1 => 1,
        n => fibonacci(n-1) + fibonacci(n-2),
    }
}*/
/// cargo bench --verbose
/// cargo bench --verbose -- fib_20
///
// "fib_20" - любое уникальное имя теста
// Использование black_box функции не позволяет компилятору свернуть всю функцию константой и заменить ее константой.
fn criterion_benchmark(c: &mut Criterion) {
    // Простой тест
    //c.bench_function("fib_20", |b:&mut criterion::Bencher| b.iter(|| fibonacci(black_box(20))));
    //c.bench_function("fib_20", |b:&mut criterion::Bencher| b.iter(|| fibonacci_2(black_box(20))));

    // Передача данных для теста
    /*let size: u64 = 20;
    c.bench_with_input(BenchmarkId::new("fib_20", size), &size, |b, &s| {
        b.iter(|| fibonacci_2(s));
    });*/

    // Множество данных для теста
   /* let size: usize = 20;
    let mut group = c.benchmark_group("fib_20");
    for s in [size, 2 + size, 4 + size, 8 + size, 16 + size].iter() {
        group.throughput(Throughput::Bytes(*s as u64));
        group.bench_with_input(BenchmarkId::from_parameter(s), s, |b, &s| {
            b.iter(|| std::iter::repeat(0u8).take(s).collect::>());
        });
    }
    group.finish();
    */

    // Сравнение функций
    // cargo bench --verbose -- Fibonacci
    let mut group = c.benchmark_group("Fibonacci");
    for i in [20u64, 21u64].iter() {
        group.bench_with_input(BenchmarkId::new("Recursive", i), i, |b, i| b.iter(|| fibonacci(*i)));
        group.bench_with_input(BenchmarkId::new("Iterative", i), i, |b, i| b.iter(|| fibonacci_2(*i)));
    }
    group.finish();
}
fn main(){
// Здесь мы вызываем макрос criterion_group! (link), чтобы сгенерировать группу тестов под названием
// benches, содержащую criterion_benchmark функцию, определенную ранее.
    criterion_group!(benches, criterion_benchmark);
// мы вызываем макрос criterion_main! (link), чтобы сгенерировать основную функцию, которая выполняет benches группу.
    criterion_main!(benches);
}

Для просмотра графика выполнения - открыть /target/criterion/fib_20/report/index.html

Вывод

Время затраченное на каждую итерацию:
time:   [3.9235 ns 3.9376 ns 3.9516 ns]
Где нижный предел 3.9235 и самый долгий временной предел 3.9516, а 3.9376 среднее значение

Изменения относительно предыдущего теста: (т.е. какое влияние нестабильности производительности ОС)
change: [+0.7533% +6.3205% +15.259%] (p = 0.06 > 0.05)
                        "Никаких изменений в производительности не обнаружено." (т.е. хорошая стабильная работа при замере, маленькое отклонение)

Погрешность:
Найдено 14 "сильных провалов" среди 100 измерений(14.00%)
  2 (2.00%) high mild (выше среднего)
  12 (12.00%) high severe (высокая степень серьезности)

Вывод

Criterion.rs пытается обнаружить необычно высокие или низкие выборки и сообщает о них как о выбросах. Большое количество выбросов говорит о том, что результаты тестов зашумлены и к ним следует относиться с должным скептицизмом. В этом случае вы можете видеть, что некоторые образцы занимали намного больше времени, чем обычно. Это может быть вызвано непредсказуемой нагрузкой на компьютер, на котором выполняются тесты, планированием потоков или процессов, или отклонениями во времени, затрачиваемом тестируемым кодом.

Чтобы обеспечить надежные результаты, тесты производительности следует запускать на тихом компьютере и рассчитывать на выполнение примерно одинакового объема работы для каждой итерации.

fib 20                time:   [3.9235 ns 3.9376 ns 3.9516 ns]
                        change: [-99.976% -99.976% -99.975%] (p = 0.00 < 0.05)
                        "Производительность улучшилась" (т.е. не стабильная работа, должно быть "Никаких изменений в производительности не обнаружено" )
Найдено 3 "сильных провалов" среди 100 измерений (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe

Вывод

Сравнение двух функций:
Fibonacci/Recursive/20  time:   [17.965 us 17.989 us 18.014 us]
Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) low mild
  3 (3.00%) high mild
  3 (3.00%) high severe
Fibonacci/Iterative/20  time:   [4.3525 ns 4.3606 ns 4.3696 ns]
Found 9 outliers among 100 measurements (9.00%)
  6 (6.00%) high mild
  3 (3.00%) high severe
Fibonacci/Recursive/21  time:   [29.094 us 29.130 us 29.168 us]
Found 8 outliers among 100 measurements (8.00%)
  6 (6.00%) high mild
  2 (2.00%) high severe
Fibonacci/Iterative/21  time:   [4.9448 ns 4.9546 ns 4.9649 ns]
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low severe
  2 (2.00%) high mild

График зависимости данных и времени

Сравнительный анализ с диапазоном значений даем на вход 20 22 24 28 32 данные для работы

/// Сравнительный анализ с диапазоном значений
/// cargo bench --verbose -- lots_of_data_benchmark_3
fn my_benchmark_3(c: &mut Criterion) {
    let size: usize = 20;
    let mut group = c.benchmark_group("lots_of_data_benchmark_3");
    for s in [size, 2 + size, 4 + size, 8 + size, 16 + size].iter() {//20 22 24 28 32
        group.throughput(Throughput::Bytes(*s as u64));//  сообщает что тест работает с size байтами за итерацию, будет использовать это для оценки количества байтов в секунду
        group.bench_with_input(
            BenchmarkId::from_parameter(s), 
            s, 
            |b, &s|{b.iter(|| std::iter::repeat(0u8).take(s).collect::<Vec<_>>());}
        );
    }
    group.finish();
}

Кастомная настройка замера

/// cargo +nightly bench --verbose -- LinkedList_remove
fn benchmark_remove(c: &mut Criterion) {
    let mut group = c.benchmark_group("LinkedList_remove");
    group.sample_size(30);// min 10 => 40 итераций лямбды, при 20 => 50 итераций лямбды
    //Argument to Bencher::iter_batched and Bencher::iter_batched_ref  Количество итераций b.iter может быть 1млрд при BatchSize::SmallInput = 2150 при BatchSize::LargeInput = 3450
    
    for count in [1_000_i32,5_000,10_000].iter() {
        group.bench_with_input(
            BenchmarkId::new("default_ownership", count), 
            count, 
            |b, count| { 
                b.iter_custom(|iters| { // Запускается один раз
                    // load
                    let mut list = LinkedList::new();
                    for i in 0..*count {
                        list.push_back(NodeLinkedList::new(format!("{}",i),None));        
                    }
                    
                    let mut index = *count - 1;
                    // time
                    let start = std::time::Instant::now();
                    for _i in 0..*count-1 { // вместо count используется iters он обычно 2млн итераций делает
                        index-=1;
                        black_box(remove_default_ownership(&mut list,index));
                    }
                    start.elapsed()
                });
            }
        );
}
    group.finish();
}

Выполнение

Генерируем группу тестов my_benches_push_back

criterion_group!(my_benches_remove, benchmark_remove);

Создание функции выполнения для my_benches_push_back

criterion_main!(my_benches_remove);

Посмотреть график выполнения - Открыть /target/criterion/<NAME YOUR BENCH TEST>/report/index.html

plots_and_graphs

Передача новых данных на каждую итерацию

  group.bench_with_input(
    BenchmarkId::new("std", count), 
    &count, 
    |b, count| {  
        let mut list: LinkedList<String> = LinkedList::new();
        for i in 1..=*count {
            list.push_back(format!("{}",i));      
        }

        b.iter_batched_ref(|| {
            list.clone() // тут каждую итерацию будут новые данные
        }, |mut list| {
            remove_std(&mut list,*count)
        }, BatchSize::LargeInput);
  }); 

use std::sync::{Mutex, RwLock};

#[macro_use]
extern crate bma_benchmark;

#[benchmark_stage(i=10_000_000)]
fn benchmark_mutex(mutex: Mutex) {
    let _a = mutex.lock().unwrap();
}

#[benchmark_stage(i=10_000_000,name="rwlock-read")]
fn benchmark_rwlock(rwlock: RwLock) {
    let _a = rwlock.read().unwrap();
}
fn main(){
    let mutex = Mutex::new(0);
    let rwlock = RwLock::new(0);
    benchmark_mutex(mutex);
    benchmark_rwlock(rwlock);
    staged_benchmark_print_for!("rwlock-read");
}

Показывает относительное время выполнения функций.

Еще один инструмент, доступный разработчикам на Rust для обнаружения горячих точек в своих приложениях, требующих оптимизации, - это флеймографы


cargo flamegraph -- my-command --my-arg my-value -m -f

Flamegraph

Install:
    sudo apt install linux-tools-common linux-tools-generic linux-tools-`uname -r`
    echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
Run:
    cargo +nightly flamegraph --example example_doubly_linked_deque_weak  
    cargo +nightly flamegraph --test test_name
Show:
    google-chrome $PWD/flamegraph.svg    

Как читать Flame Graph Нижный слой(прямоугольник, кадр стека) это вход в программы,а верхний ее листья т.е. конечные вызовы Чем шире слой тем дольше времени он выполнялся. Чем выше широкий слой тем хуже т.е. работа не делегировалась. Рекомендуется устранять верхнии широкие слои корректируя код и производя замеры времени выполнения

Ширина каждого поля показывает общее время, в течение которого эта функция находится на ЦП или является частью стека вызовов. Если поле функции шире, чем у других, это означает, что она потребляет больше процессорного времени на выполнение, чем другие функции, или что она вызывается чаще, чем другие функции.

crate samply

The best way is the following:

  1. cargo install samply

  2. Create a global cargo profile called profiling, see below how. To create the profiling cargo profile, create a text file at ~/.cargo/config.toml with the following content:

[profile.profiling]
inherits = "release"
debug = true
  1. Compile with cargo build --profile profiling

  2. Record with samply record ./target/profiling/yourrustprogram

Профилирование программы подразумевает запуск версии программы, в которую встроен специальный код, определяющий количество времени, затрачиваемое на выполнение разными частями программы. Этот метод помогает выявлять части программы, на которые следует обратить внимание с точки зрения оптимизации.

Профильная оптимизация (PGO) - это способ компилятора получить информацию о том, как программа, вероятно, будет выполняться, чтобы он мог попытаться выполнить более конкретную оптимизацию на основе этого при повторной компиляции той же программы.

Пример перекомпиляции существующей исполняемой версии

#!/usr/bin/env bash

set -e

export PATH="$PATH:~/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/bin"
PROGRAM="phone_encoder"

# STEP 0: Make sure there is no left-over profiling data from previous runs
rm -rf /tmp/pgo-data || true

# STEP 1: Build the instrumented binaries
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" \
  cargo build --release

# STEP 2: Run the instrumented binaries with some typical data
./target/release/$PROGRAM ../../../dictionary.txt ../../../phones_50_000.txt > /dev/null
./target/release/$PROGRAM ../../../dictionary.txt ../../../phones_100_000_with_empty.txt > /dev/null
./target/release/$PROGRAM ../../../dictionary.txt ../../../phones_200_000_with_empty.txt > /dev/null

# STEP 3: Merge the `.profraw` files into a `.profdata` file
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data

# STEP 4: Use the `.profdata` file for guiding optimizations
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" \
    cargo build --release

File Cargo.toml:

[profile.release] 
lto = "fat" 
panic = "abort" 
opt-level = 3

Где:

  • lto = "fat" - пытается выполнить оптимизацию для всех ящиков в графе зависимостей.

  • panic = "abort"- это изменение профиля выпуска по умолчанию, то есть unwind.

  • opt-level - по умолчанию в профиле выпуска уже 3, но я подумал, что должен упомянуть, что вы можете установить это значение между 0 и 3.

User наша программа и System являясь ядром.

$ hyperfine --warmup 3 --min-runs 20 'target/release/fibonacci 100000000 100000000'  'target/release/fibonacci 100000000 100000000' 
Benchmark 1: target/release/fibonacci 100000000 100000000
  Time (mean ± σ):     343.6 ms ±  50.9 ms    [User: 95.6 ms, System: 241.1 ms]
  Range (min … max):   288.1 ms … 480.5 ms    20 runs
 
Benchmark 2: target/release/fibonacci 100000000 100000000
  Time (mean ± σ):     339.8 ms ±  32.8 ms    [User: 93.6 ms, System: 240.0 ms]
  Range (min … max):   285.6 ms … 407.1 ms    20 runs
 
Summary
  target/release/fibonacci 100000000 100000000 ran
    1.01 ± 0.18 times faster than target/release/fibonacci 100000000 100000000

Мы смотрим на график пламени . Каждая горизонтальная полоса представляет собой функцию, и ее ширина равна доле времени выполнения функции. Желтые полосы находятся в пользовательском пространстве, а оранжевые — в ядре.

Интерактивная

Если вы хотите увидеть, как на самом деле выполнялась программа, а не просматривать агрегаты, переключитесь на вкладку «Стековая диаграмма»

profiler.firefox.com

# --call-graph=dwarf tells the profiler use the debug symbols we added
sudo perf record --call-graph=dwarf target/release/fibonacci 100000000 100000000

cargo install samply

sudo chown "$USER" perf.data # because we recorded the profile with `sudo`chown "$USER" perf.data # because we recorded the profile with `sudo`

samply load perf.data
[profile.release]
opt-level = 0 # 0,1,2,3
debug = true

[features]
dhat-heap = []    # if you are doing heap profiling
dhat-ad-hoc = []  # if you are doing ad hoc profiling

[dependencies]
dhat = "0.3" 

Run:

cargo run --features dhat-heap

Показывает занимаемую память heap в момент выполнения

Интерпретация результатов

Run example:

cargo +nightly run --example example_linked_list_prof_heap --features dhat-heap

Open url dh_view and send file dhat-heap.json

==11514== Total:     823,849,731 bytes in 3,929,133 blocks
 ==11514== At t-gmax: 133,485,082 bytes in 436,521 blocks
==11514== At t-end:  258,002 bytes in 2,129 blocks
==11514== Reads:     2,807,182,810 bytes
==11514== Writes:    1,149,617,086 bytes

Первая строка показывает, сколько блоков heap и байтов было выделено за все время выполнения.

Вторая строка показывает, сколько блоков heap и байтов было активным в t-gmax момент, то есть время, когда размер heap достиг своего глобального максимума (измеряется в байтах).

Третья строка показывает, сколько блоков heap и байтов были активны на t-end момент, т. е. в конце выполнения. Другими словами, сколько блоков и байтов не было освобождено явно.

Четвертая и пятая строки показывают, сколько байтов в блоках heap было прочитано и записано за все время выполнения.

Эти строки в лучшем случае умеренно интересны. Более полезную информацию можно увидеть с помощью средства просмотра DHAT.

Samply

современный профилировщик от команды Firefox

Он генерирует веб-интерфейс, где можно детально изучить каждый поток и вызов.

Команда: samply record ./target/release/my_project