Команда разработчиков 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:
DoubleEndedIterator::rfind
DoubleEndedIterator::rfold
DoubleEndedIterator::try_rfold
Duration::from_micros
Duration::from_nanos
Duration::subsec_micros
Duration::subsec_millis
HashMap::remove_entry
Iterator::try_fold
Iterator::try_for_each
NonNull::cast
Option::filter
String::replace_range
Take::set_limit
slice::rsplit_mut
slice::rsplit
slice::swap_with_slice
hint::unreachable_unchecked
os::unix::process::parent_id
ptr::swap_nonoverlapping
process::id
Подробности смотрите в примечаниях к выпуску.
Улучшения в 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)
PsyHaSTe
25.06.2018 12:40SIMD — отличная тема, очень нужно.
#[must_use]
и стабилизация — тоже неплохо.
А вот в необходимости
dyn Trait
меня все-таки не убедили. Основной посыл — "мы не знаем, тут трейт объект или структура, а это влияет на производительность". Ну, влиять-то оно влияет, да, но какая разница? Это ж блин сигнатура функции, она должна говорить про типы параметров, а не требования к производительности определять.
Аргумент с ортогональностью
impl Trait
тоже неубедителен — в одном случае это кодогенерация, которая делает возможным прежде недоступные вещи, а другое — просто алиас для уже существующей синтаксической конструкции, которую придется деприкейтить и писать тулзы для миграции.Tanriol
25.06.2018 14:08+1Это ж блин сигнатура функции, она должна говорить про типы параметров, а не требования к производительности определять.
Да, поэтому сигнатуру, которая сразу же ограничивает "а эта функция точно не сможет работать максимально быстро вне зависимости от реализации из-за отсутствия инлайнинга и т. п.", лучше делать явно видимой. Кроме того,
dyn Trait
не только в сигнатурах может возникать, но и, например, в структурах данных, где подобные ограничения также лучше видеть.
Аргумент с ортогональностью
impl Trait
тоже неубедителен — в одном случае это кодогенерация, которая делает возможным прежде недоступные вещи, а другое — просто алиас для уже существующей синтаксической конструкции, которую придется деприкейтить и писать тулзы для миграции.Аргумент с ортогональностью — он скорее не про возможности, а про обучение. У новичков разница между
dyn Trait
иimpl Trait
будет, предположительно, вызывать сильно мешьную путаницу, чем междуTrait
иimpl Trait
при использовании в местах, где могут быть оба варианта. Rust и так имеет непростую кривую обучения, так что её локальное упрощение имеет смысл даже ценой небольших миграций.
snuk182
25.06.2018 14:14У
dyn Trait
ноги растут из хайпа вокруг мономорфизации, которую сейчас пытаются пихать куда надо и не надо. При этом находятся пользователи, которые не смогли в ошибки компилятора в случае попыток запихать что-то недозволенное в типаж-объект. Решение о явном зашумлении динамического полиморфизма принято из данных статистики, что статика используется намного чаще динамики (не в последнюю очередь из-за низкоуровневости проектов-объектов статистики, язык все же замена С/С++, и писать что-то динамическое просто не видят смысла, для этого есть Electron). Другого объяснения я не нахожу.
freecoder_xx Автор
25.06.2018 16:47+1У меня возникала пару раз ситуация, когда однозначность `dyn Trait` избавила бы от необходимости уточнять, что именно имелось ввиду в коде. Но насколько оправдано использование дополнительного слова во всех случаях использования типажей-объектов — практика и время покажут.
BlessMaster
26.06.2018 11:51+1Имхо, явное, в данном случае, лучше неявного.
Отличить типаж от типа, не закапываясь в "где же он определён" бывает достаточно сложно — далеко не всегда исходники доводится читать в IDE.
И в целом очень удобно явно видеть, что "здесь у нас trait-object" (на который кроме вышеупомянутых "штрафов" скорости, действует ещё целый ряд ограничений).
ozkriff
Судя по проектам, которые включили
#![warn(bare_trait_objects)]
, код отdyn Trait
и правда на практике не сильно раздувается и уж точно не теряет в читабельности — например, https://github.com/ggez/ggez/commit/ec3c1e0f6dd