Бенчмаркинг, преобразования String в &str и обратно

Тех.вопросы
Feb 27, 2016
L

Добрый день. Разбираюсь со строками в Rust, возник следующий вопрос: есть код с 2-я функциями calculator - одна принимает String, другая соответственно - &str. Так же бенчмарки к ним с разной вариацией передачи внутри бенчмарка в эти функции параметров:

#[bench]
fn bench_calculator(b: &mut Bencher) {
    b.iter(|| calculator("+-*/", 8));
}

#[bench]
fn bench_calculator_with_string(b: &mut Bencher) {
    b.iter(|| calculator_with_string("+-*/".to_string(), 8));
}

#[bench]
fn bench_calculator_with_preconverted_string(b: &mut Bencher) {

    b.iter(||
        {
            let preconverted_string = "+-*/".to_string();
            calculator_with_string(preconverted_string, 8)
        }
    );
}

#[bench]
fn bench_calculator_with_string_to_str(b: &mut Bencher) {
    b.iter(||
    {
        let preconverted_string = "+-*/".to_string();
        calculator(&preconverted_string, 8);
    });
}

#[bench]
fn bench_calculator_with_string_to_precompiled_str(b: &mut Bencher) {
    b.iter(||
    {
        let preconverted_string = "+-*/".to_string();
        let preconverted_str = &preconverted_string;
        calculator(preconverted_str, 8);
    });
}

Результат бенчмарков таков:

Executing: cargo bench --verbose
Fresh hello_world v0.1.0 (file:///D:/Projects/Rust/hello_world)
Running D:\Projects\Rust\hello_world\target\release\hello_world-8986e24de7249bbb.exe --bench

running 5 tests
test tests::bench_calculator … bench: 7 ns/iter (+/- 0)
test tests::bench_calculator_with_preconverted_string … bench: 90 ns/iter (+/- 10)
test tests::bench_calculator_with_string … bench: 90 ns/iter (+/- 12)
test tests::bench_calculator_with_string_to_precompiled_str … bench: 85 ns/iter (+/- 3)
test tests::bench_calculator_with_string_to_str … bench: 85 ns/iter (+/- 2)

test result: ok. 0 passed; 0 failed; 0 ignored; 5 measured

Каким образом нужно написать код в bench, чтобы оптимизировать работу bench программы там где происходит преобразование String в &str? Единственное, что приходит в голову - это вынести инициализацию связанного имени строки за пределы цикла b.iter(…); но как это передать в замыкание? И ускорит ли это работу бенчмарка?

K

У тебя bench_calculator_with_preconverted_string и bench_calculator_with_string идентичны,
равно как bench_calculator_with_string_to_precompiled_str и bench_calculator_with_string_to_str
(что видно по таймингам). Сейчас из-за выделения памяти ты реально тестируешь свою функцию только в первом тесте, в остальных ты тестируешь скорость аллокатора.

Зачем вообще нужно морочиться с выделением памяти под строки для calculator(&str)? Что статичная строка, что нет — без разницы. Или хочется в рантайме генерировать строку для вычисления? Тогда создавай строку вне b.iter(||), а внутри используй срез от строки.

С calculator_with_string(String) сложнее, нужно каждый раз создавать клон строки, если её инициировать снаружи b.iter(||), что в принципе аналогично построению строки заново (String::from("abc") занимает примерно то же время, что и s.clone(), т.к. происходит выделение памяти под строку и копирование строки), так что и выносить создание строки за b.iter(||) смысла нет. Разве что алгоритм генерации строки достаточно сложный, тогда можно и вынести, а в теле замыкания просто делать s.clone(). Bencher::iter() принимает F: FnMut, поэтому передавать владение строкой в замыкание не надо, а вызов clone() владения не требует, так что будет работать на ура.

Другое дело, что я не вижу никакого смысла в существовании функции calculator_with_string(String). Я же правильно понимаю, что эта функция строку не модифицирует? Получается только лишняя передача владения, что не идиоматично и не эргономично.

P.S. Да, и попробуй заменить "abc".to_string() на String::from("abc") или "abc".to_owned(), должно стать быстрее.

L
Replying to #2

На самом деле цель не тестирования функции как таковой, а изучение разницы в скорости между работой со String, &str. А так же в том, как оптимизации компилятора могут уменьшить объём выделяемой памяти в случае им мутабельности используемого в b.iter(||) связанного имени. Вот результаты работы from и to_owned :

#[bench]
fn bench_calculator_with_string_from(b: &mut Bencher) {
    b.iter(|| {
        calculator_with_string(String::from("+-*/"), 8);
    });
}

#[bench]
fn bench_calculator_with_owned_str(b: &mut Bencher) {
    b.iter(|| {
        calculator_with_string("+-*/".to_owned(), 8);
    });
} 

Результаты:

test tests::bench_calculator_with_string_from … bench: 59 ns/iter (+/- 6)
test tests::bench_calculator_with_owned_str … bench: 68 ns/iter (+/- 2)

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

Меня больше интересует возможен ли случай когда оптимизатор поймёт, что строка статична и выделит под неё память лишь раз?(и это без выноса объявления связанного имени строки вне цикла с последующей её передачей) Ведь в текущем варианте программа выделяет для каждого вызова замыкания память для &str в своём отдельном стеке? Если нет, то поправьте.

P.S Есть ли встроенная возможность отслеживать, сколько было выделено памяти в среднем по всем итерациям? Подскажите каким образом, я мог бы включить подобную “профилировку”.

P.S.S В предыдущих бенчмарках я делал вынос объявления строки до передачи её в calculator, чтобы узнать заработает ли оно быстрее, указывая явно, что строка не изменяема.

K
Replying to #3

Разве что на уровне LLVM, компилятор раста, насколько мне известно, такие оптимизации не делает.
Статический литерал да, должен заинтерниться и существовать в одном экземпляре, но выделение памяти под String, будучи явным, должно происходить всегда по запросу.

Проверить можно, заглянув в сгенерированный IR.