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


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


$ 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). Рассмотрим функцию:


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, стало возможно использовать эти виды инструкций напрямую, то есть теперь мы не обязаны полагаться только на интеллектуальную компиляцию. Дополнительно у нас появилась возможность выбирать конкретную реализацию в зависимости от различных критериев. Например:


#[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 — своя. Мы также можем выбирать и во время выполнения:


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:


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, вам потребуется изменить его так:


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 можно так определить типаж-объект:


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. Типажи-объекты теперь выглядят так:


// было => стало
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>. Но теперь вы можете делать так:


#[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. Мы не смогли бы завершить работу без участия каждого из вас.


Спасибо!


От переводчика: выражаю отельную благодарность участникам сообщества ruRust и лично ozkriff за помощь с переводом и вычиткой

Комментарии (6)


  1. ozkriff
    25.06.2018 12:36

    Судя по проектам, которые включили #![warn(bare_trait_objects)], код от dyn Trait и правда на практике не сильно раздувается и уж точно не теряет в читабельности — например, https://github.com/ggez/ggez/commit/ec3c1e0f6dd


  1. PsyHaSTe
    25.06.2018 12:40

    SIMD — отличная тема, очень нужно.


    #[must_use] и стабилизация — тоже неплохо.


    А вот в необходимости dyn Trait меня все-таки не убедили. Основной посыл — "мы не знаем, тут трейт объект или структура, а это влияет на производительность". Ну, влиять-то оно влияет, да, но какая разница? Это ж блин сигнатура функции, она должна говорить про типы параметров, а не требования к производительности определять.


    Аргумент с ортогональностью impl Trait тоже неубедителен — в одном случае это кодогенерация, которая делает возможным прежде недоступные вещи, а другое — просто алиас для уже существующей синтаксической конструкции, которую придется деприкейтить и писать тулзы для миграции.


    1. Tanriol
      25.06.2018 14:08
      +1

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

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


      Аргумент с ортогональностью impl Trait тоже неубедителен — в одном случае это кодогенерация, которая делает возможным прежде недоступные вещи, а другое — просто алиас для уже существующей синтаксической конструкции, которую придется деприкейтить и писать тулзы для миграции.

      Аргумент с ортогональностью — он скорее не про возможности, а про обучение. У новичков разница между dyn Trait и impl Trait будет, предположительно, вызывать сильно мешьную путаницу, чем между Trait и impl Trait при использовании в местах, где могут быть оба варианта. Rust и так имеет непростую кривую обучения, так что её локальное упрощение имеет смысл даже ценой небольших миграций.


    1. snuk182
      25.06.2018 14:14

      У dyn Trait ноги растут из хайпа вокруг мономорфизации, которую сейчас пытаются пихать куда надо и не надо. При этом находятся пользователи, которые не смогли в ошибки компилятора в случае попыток запихать что-то недозволенное в типаж-объект. Решение о явном зашумлении динамического полиморфизма принято из данных статистики, что статика используется намного чаще динамики (не в последнюю очередь из-за низкоуровневости проектов-объектов статистики, язык все же замена С/С++, и писать что-то динамическое просто не видят смысла , для этого есть Electron). Другого объяснения я не нахожу.


    1. freecoder_xx Автор
      25.06.2018 16:47
      +1

      У меня возникала пару раз ситуация, когда однозначность `dyn Trait` избавила бы от необходимости уточнять, что именно имелось ввиду в коде. Но насколько оправдано использование дополнительного слова во всех случаях использования типажей-объектов — практика и время покажут.


    1. BlessMaster
      26.06.2018 11:51
      +1

      Имхо, явное, в данном случае, лучше неявного.
      Отличить типаж от типа, не закапываясь в "где же он определён" бывает достаточно сложно — далеко не всегда исходники доводится читать в IDE.
      И в целом очень удобно явно видеть, что "здесь у нас trait-object" (на который кроме вышеупомянутых "штрафов" скорости, действует ещё целый ряд ограничений).