Выпуск Rust 1.27

оригинал: The Rust Core Team • перевод: XX • новости • поддержите на Patreon

Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1.27.0. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода.

Если у вас установлена предыдущая версия Rust с помощью rustup, то для обновления Rust до версии 1.27.0 вам достаточно выполнить:

1
$ rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта. С подробными примечаниями к выпуску Rust 1.27.0 можно ознакомиться на GitHub.

Также мы хотим обратить ваше внимание вот на что: перед выпуском версии 1.27.0 мы обнаружили ошибку в улучшении сопоставлений match, введённом в версии 1.26.0, которая может привести к некорректному поведению. Поскольку она была обнаружена очень поздно, уже в процессе выпуска данной версии, хотя присутствует с версии 1.26.0, мы решили не нарушать заведённый порядок и подготовить исправленную версию 1.27.1, которая выйдет в ближайшее время. И дополнительно, если потребуется, версию 1.26.3. Подробности вы сможете узнать из соответствующих примечаний к выпуску.

Что вошло в стабильную версию 1.27.0

В этом выпуске выходят два больших и долгожданных улучшения языка. Но сначала небольшой комментарий относительно документации: во всех книгах в библиотечке Rust теперь доступен поиск! Например, так можно найти «заимствование» («borrow») в книге «Язык программирования Rust» Надеемся, это облегчит поиск нужной вам информации. Кроме того, появилась новая Книга о rustc. В этой книге объясняется, как напрямую использовать rustc, а также как получить другую полезную информацию, такую как список всех статических проверок.

SIMD

Итак, теперь о важном: отныне в Rust доступны базовые возможности использования SIMD! SIMD означает «одиночный поток команд, множественный поток данных» (single instruction, multiple data). Рассмотрим функцию:

1
2
3
4
5
pub fn foo(a: &[u8], b: &[u8], c: &mut [u8]) {
    for ((a, b), c) in a.iter().zip(b).zip(c) {
        *c = *a + *b;
    }
}

Здесь мы берём два целочисленных среза, суммируем их элементы и помещаем результат в третий срез. Приведённый выше код демонстрирует самый простой способ сделать это: нужно пройтись по всему набору элементов, сложить их вместе и сохранить результат. Однако, компиляторы зачастую находят решение получше. LLVM часто «автоматически векторизует» подобный код, где такая затейливая формулировка означает просто «использует SIMD». Представьте, что срезы a и b имеют длину в 16 элементов оба. Каждый элемент — это u8, а значит срезы будут содержать по 128 бит данных каждый. Используя SIMD, мы можем разместить оба среза a и b в 128-битных регистрах, сложить их вместе одной инструкцией и затем скопировать результирующие 128 бит в c. Это будет работать намного быстрее!

Несмотря на то, что стабильная версия Rust всегда была в состоянии использовать преимущества автоматической векторизации, иногда компилятор просто недостаточно умён, чтобы понять, что можно её применить в данном случае. Кроме того, не все CPU поддерживают такие возможности. Поэтому LLVM не может использовать их всегда, так как ваша программа может работать на самых разных аппаратных платформах. Поэтому в Rust 1.27, с добавлением модуля std::arch, стало возможно использовать эти виды инструкций напрямую, то есть теперь мы не обязаны полагаться только на интеллектуальную компиляцию. Дополнительно у нас появилась возможность выбирать конкретную реализацию в зависимости от различных критериев. Например:

1
2
3
4
5
6
7
8
9
10
11
12
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"),
      target_feature = "avx2"))]
fn foo() {
    #[cfg(target_arch = "x86")]
    use std::arch::x86::_mm256_add_epi64;
    #[cfg(target_arch = "x86_64")]
    use std::arch::x86_64::_mm256_add_epi64;

    unsafe {
        _mm256_add_epi64(...);
    }
}

Здесь мы используем флаги cfg для выбора правильной версии кода в зависимости от целевой платформы: на x86 будет использоваться своя версия, а на x86_64 — своя. Мы также можем выбирать и во время выполнения:

1
2
3
4
5
6
7
8
9
10
fn foo() {
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    {
        if is_x86_feature_detected!("avx2") {
            return unsafe { foo_avx2() };
        }
    }

    foo_fallback();
}

Здесь у нас имеется две версии функции: одна использует AVX2 — специфический вид SIMD, который позволяет выполнять 256-битные операции. Макрос is_x86_feature_detected! сгенерирует код, который проверит, поддерживает ли процессор AVX2, и если да, то будет вызвана функция foo_avx2. Если нет, то мы прибегнем к реализации без AVX, foo_fallback. Значит наш код будет работать очень быстро на процессорах, поддерживающих AVX2, но также будет работать и на остальных процессорах, хотя и медленнее.

Все это выглядит слегка низкоуровневым и неудобным — да, так и есть! std::arch — это именно примитивы для такого рода вещей. Мы надеемся, что в будущем мы все-таки стабилизируем модуль std::simd с высокоуровневыми возможностями. Но появление базовых возможностей работы с SIMD позволяет теперь экспериментировать с высокоуровневой поддержкой различным библиотекам. Например, посмотрите пакет faster. Вот фрагмент кода без SIMD:

1
2
3
4
5
let lots_of_3s = (&[-123.456f32; 128][..]).iter()
    .map(|v| {
        9.0 * v.abs().sqrt().sqrt().recip().ceil().sqrt() - 4.0 - 2.0
    })
    .collect::<Vec<f32>>();

Для использования SIMD в этом коде с помощью faster, вам потребуется изменить его так:

1
2
3
4
5
let lots_of_3s = (&[-123.456f32; 128][..]).simd_iter()
    .simd_map(f32s(0.0), |v| {
        f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0)
    })
    .scalar_collect();

Он выглядит почти таким же: simd_iter вместо iter, simd_map вместо map, f32s(2.0) вместо 2.0. Но в итоге вы получаете SIMD-версию вашего кода.

Помимо этого, вы можете никогда не писать такое сами, но, как всегда, это могут делать библиотеки, от которых вы зависите. Например, в пакет regex уже добавили поддержку, и его новая версия будет иметь SIMD-ускорение без необходимости вам вообще что-либо делать!

dyn Trait

В конечном итоге мы пожалели о выбранном изначально синтаксисе типажей-объектов в Rust. Как вы помните, для типажа Foo можно так определить типаж-объект:

1
Box<Foo>

Однако, если Foo — была бы структура, это означало бы просто размещение структуры внутри Box<T>. При разработке языка мы думали, что такое сходство будет хорошей идеей, но опыт показал, что это приводит к путанице. И дело не только в Box<Trait>: impl SomeTrait for SomeOtherTrait также является формально корректным синтаксисом, но вам почти всегда требуется написать impl<T> SomeTrait for T where T: SomeOtherTrait вместо этого. То же самое и с impl SomeTrait, который выглядит так, будто добавляет методы или возможную реализацию по умолчанию в типаж, но на самом деле он добавляет собственные методы в типаж-объект. Наконец, по сравнению с недавно добавленным синтаксисом impl Trait, синтаксис Trait выглядит короче и предпочтительней к использованию, но на самом деле это не всегда верно.

Поэтому в Rust 1.27 мы стабилизировали новый синтаксис dyn Trait. Типажи-объекты теперь выглядят так:

1
2
3
4
// было => стало
Box<Foo> => Box<dyn Foo>
&Foo => &dyn Foo
&mut Foo => &mut dyn Foo

Аналогично и для других типов-указателей: Arc<Foo> теперь Arc<dyn Foo> и т. д. Из-за требования обратной совместимости мы не можем удалить старый синтаксис, но мы добавили статическую проверку bare-trait-object, которая по умолчанию разрешает старый синтаксис. Если вы хотите запретить его, то вы можете активировать данную проверку. Мы подумали, что с проверкой, включённой по умолчанию, сейчас будет выводиться слишком много предупреждений.

Кстати, мы работаем над инструментом под названием rustfix, который сможет автоматически обновлять ваш код до более новых идиом. Он будет использовать подобные статические проверки для этого. Следите за сообщениями о rustfix в будущих анонсах.

#[must_use] для функций

В заключении, было расширено действие атрибута #[must_use]: теперь он может использоваться для функций.

Раньше он применялся только к типам, таким как Result <T, E>. Но теперь вы можете делать так:

1
2
3
4
5
6
7
8
9
10
#[must_use]
fn double(x: i32) -> i32 {
    2 * x
}

fn main() {
    double(4); // warning: unused return value of `double` which must be used

    let _ = double(4); // (no warning)
}

С этим атрибутом мы также слегка улучшили стандартную библиотеку: Clone::clone, Iterator::collect и ToOwned::to_owned будут выдавать предупреждения, если вы не используете их возвращаемые значения, что поможет вам заметить дорогостоящие операции, результат которых вы случайно игнорируете.

Подробности смотрите в примечаниях к выпуску.

Стабилизация библиотек

В этом выпуске были стабилизированы следующие новые API:

Подробности смотрите в примечаниях к выпуску.

Улучшения в Cargo

В этом выпуске Cargo получил два небольших улучшения. Во-первых, появился новый флаг --target-dir, который можно использовать для изменения целевой директории выполнения.

Дополнительно, доработан подход Cargo к тому, как обрабатывать цели. Cargo пытается обнаружить тесты, примеры и исполняемые файлы в рамках вашего проекта. Однако иногда требуется явная конфигурация. Но в первоначальной реализации это сделать было проблематично. Скажем, у вас есть два примера, и Cargo их оба обнаруживает. Вы хотите сконфигурировать один из них, для чего добавляете [[example]] в Cargo.toml, чтобы указать параметры примера. В настоящее время Cargo увидит, что вы определили пример явно, и поэтому не будет пытаться делать автоматическое определение других. Это слегка огорчает.

Поэтому мы добавили несколько ‘auto’-ключей в Cargo.toml. Мы не можем исправить такое поведение без возможной поломки проектов, которые по неосторожности на него полагались. Поэтому если вы хотите сконфигурировать некоторые цели, но не все, вы можете установить ключ autoexamples в true в секции [package].

Подробности смотрите в примечаниях к выпуску.

Разработчики 1.27.0

Множество людей участвовало в разработке Rust 1.27. Мы не смогли бы завершить работу без участия каждого из вас.

Спасибо!


Обсуждение на форуме.