Сравнение производительности циклов и итераторов

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

Мы запустили тест производительности, разместив всё содержимое книги "Приключения Шерлока Хомса" А. Конан Дойля (“The Adventures of Sherlock Holmes” by Sir Arthur Conan Doyle) в String и поискали слово "the" в данном тексте. Далее результаты теста для версии search с использованием цикла for и версии с использованием итераторов:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

Версия с использованием итераторов была немного быстрее! Мы не будем приводить здесь непосредственно код теста, поскольку задача не в том, чтобы доказать, что решения в точности эквивалентны, а в том, чтобы получить общее представление о том, как эти две реализации равны в производительности. Для более полного теста вам нужно проверить различные тексты разных размеров, разные слова, слова различной длины и всевозможные другие варианты. Дело в том, что итераторы, будучи высокоуровневой абстракцией, компилируются примерно в тот же код, как если бы вы написали его низкоуровневый вариант самостоятельно. Итераторы - это одна из абстракций с нулевой стоимостью (zero-cost abstractions) в Rust, под которой мы подразумеваем, что использование абстракции не накладывает дополнительных расходов во время выполнения так же, как Bjarne Stroustrup, дизайнер и разработчик C++, определяет нулевые накладные расходы (zero-overhead):

В целом, реализация C++ подчиняется принципу отсутствия накладных расходов: за то, чем вы не пользуетесь, платить не нужно. И далее: что бы вы ни использовали, нельзя сделать код ещё лучше.

  • Bjarne Stroustrup "Основы C++" (“Foundations of C++”)

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

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

Чтобы вычислить значение «предсказания», этот код перебирает каждое из 12 значений в «коэффициентах» и использует метод «zip» для объединения значений коэффициентов с предыдущими 12 значениями в «буфере». Затем для каждой пары мы перемножаем значения, суммируем все результаты и сдвигаем биты в сумме qlp_shift вправо.

Для вычислений в приложениях, таких как аудиодекодеры, часто требуется производительность. Здесь мы создаем итератор, используя два адаптера, потребляющих значение впоследствии. В какой ассемблерный код будет компилироваться этот код на Rust? Ну, на момент написания этой главы он компилируется в то же самое, что вы написали бы руками. Не существует цикла, соответствующего итерации по значениям в «коэффициентах»: Rust знает, что существует двенадцать итераций, поэтому он «разворачивает» цикл. Разворачивание - это оптимизация, которая устраняет издержки кода управления циклом и вместо этого генерирует повторяющийся код для каждой итерации цикла.

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

Теперь, когда вы это знаете, используйте итераторы и замыкания без страха! Они представляют код в более высокоуровневом виде, но без потери производительности во время выполнения.

Итоги

Замыкания и итераторы - это возможности Rust, вдохновленные идеями функционального языка программирования. Они помогают способности Rust четко выражать высокоуровненвые идеи с низкоуровневой производительностью. Реализации замыканий и итераторов таковы, что не влияют на производительность среды выполнения. Это является частью цели Rust, направленной на создание абстракций с нулевой стоимостью.

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