Выпуск Rust 1.27
Команда разработчиков 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:
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
hint::unreachable_unchecked
os::unix::process::parent_id
process::id
ptr::swap_nonoverlapping
slice::rsplit_mut
slice::rsplit
slice::swap_with_slice
Подробности смотрите в примечаниях к выпуску.
Улучшения в Cargo
В этом выпуске Cargo получил два небольших улучшения. Во-первых,
появился новый флаг --target-dir
,
который можно использовать для изменения целевой директории выполнения.
Дополнительно, доработан подход Cargo к тому, как обрабатывать цели. Cargo
пытается обнаружить тесты, примеры и исполняемые файлы в рамках вашего
проекта. Однако иногда требуется явная конфигурация. Но в первоначальной
реализации это сделать было проблематично. Скажем, у вас есть два примера,
и Cargo их оба обнаруживает. Вы хотите сконфигурировать один из них, для чего
добавляете [[example]]
в Cargo.toml
, чтобы указать параметры примера.
В настоящее время Cargo увидит, что вы определили пример явно, и поэтому не
будет пытаться делать автоматическое определение других. Это слегка огорчает.
Поэтому мы добавили несколько ‘auto’-ключей в Cargo.toml
.
Мы не можем исправить такое поведение без возможной поломки проектов, которые
по неосторожности на него полагались. Поэтому если вы хотите сконфигурировать
некоторые цели, но не все, вы можете установить ключ autoexamples
в true
в секции [package]
.
Подробности смотрите в примечаниях к выпуску.
Разработчики 1.27.0
Множество людей участвовало в разработке Rust 1.27. Мы не смогли бы завершить работу без участия каждого из вас.