发布 Rust 1.27

2018 年 6 月 21 日 · Rust 核心团队

Rust 团队很高兴宣布 Rust 的新版本 1.27.0。Rust 是一门专注于安全性、速度和并发性的系统编程语言。

如果您之前已经通过 rustup 安装了 Rust,获取 Rust 1.27.0 的方法非常简单,只需执行以下命令:

$ rustup update stable

如果您还没有安装 rustup,可以从我们网站上的相应页面获取 rustup,并在 GitHub 上查看 1.27.0 的详细发布说明

此外,我们想提醒大家一件事:就在 1.27.0 发布之前,我们在 1.26.0 引入的“default match bindings”特性中发现了一个 bug,它可能会导致 unsoundness(不健全性)。由于这个 bug 在发布流程的非常后期才被发现,并且自 1.26.0 版本以来一直存在,我们决定坚持我们的发布列车模式。我们预计很快会发布带有此修复的 1.27.1 版本,如果有需求,也可能会发布 1.26.3 版本。有关此问题的更多详细信息将在后续的发布公告中说明。

1.27.0 stable 版本有什么新内容?

此版本带来了两个备受期待的重大语言特性。但首先,先来说说文档方面的一个小变化:《Rust 书架》中的所有书籍现在都可以搜索了!例如,这里是在《Rust 编程语言》中搜索“borrow”的结果。这有望让您更容易找到所需的内容。此外,还新增了一本书:《rustc 指南》。这本书解释了如何直接使用 rustc,以及其他一些有用的信息,比如所有 lint 的列表。

SIMD

好的,现在来说大新闻: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”的花哨说法。想象一下 ab 都有 16 个元素长。每个元素都是一个 u8,这意味着每个切片都是 128 位数据。使用 SIMD,我们可以将 ab 都放入 128 位寄存器中,通过单条指令将它们相加,然后将结果的 128 位复制到 c 中。这样会快得多!

虽然 stable 版本的 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! 宏将生成代码来检测您的 CPU 是否支持 AVX2,如果支持,则调用 foo_avx2 函数。如果不支持,则回退到非 AVX 实现 foo_fallback。这意味着我们的代码在支持 AVX2 的 CPU 上会运行得超快,但在不支持的 CPU 上也能工作,尽管速度会慢一些。

如果这一切看起来有点底层和繁琐,那确实如此!std::arch 特别是用于构建此类功能的原语。我们希望将来最终稳定一个包含更高级功能的 std::simd 模块。但现在提供基础功能,可以让生态系统从今天开始尝试更高级的库。例如,看看 faster crate。这是一个没有 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>>();

要通过 faster 在此代码中使用 SIMD,您需要将其更改为:

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 crate 已经添加了支持,新版本将包含这些 SIMD 带来的速度提升,而您无需做任何事情!

dyn Trait

Rust 的 trait object 语法是我们最终感到遗憾的一个方面。如果您还记得,给定一个 trait Foo,这是一个 trait object:

Box<Foo>

然而,如果 Foo 是一个 struct,它就只是一个放置在 Box<T> 内部的普通 struct。在设计语言时,我们认为这里的相似性是一件好事,但经验表明这会令人困惑。而且不仅仅是 Box<Trait> 的情况;impl SomeTrait for SomeOtherTrait 在技术上也是有效的语法,但您几乎总是想写 impl<T> SomeTrait for T where T: SomeOtherTraitimpl SomeTrait 也是如此,它看起来像会添加方法或可能的默认实现,但实际上是给 trait object 添加了固有方法。最后,随着最近添加了 impl Trait 语法,在解释时就变成了 impl Trait vs Trait,这让人觉得 Trait 是应该使用的,因为它更短,但实际上并非总是如此。

因此,在 Rust 1.27 中,我们稳定了一个新语法,dyn Trait。现在 trait object 看起来像这样:

// old => new
Box<Foo> => Box<dyn Foo>
&Foo => &dyn Foo
&mut Foo => &mut dyn Foo

类似地,对于其他指针类型,Arc<Foo> 现在是 Arc<dyn Foo> 等等。出于向后兼容性的考虑,我们不能移除旧的语法,但我们包含了一个 lint,默认设置为允许,称为 bare-trait-object。如果您想对旧语法进行 lint 检查,可以开启它。我们认为目前默认开启它会产生太多警告。

顺便说一下,我们正在开发一个名为 rustfix 的工具,它可以自动将您的代码升级到新的惯用法。它使用这些 lint 来实现这一目标。敬请期待未来公告中有关 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::cloneIterator::collectToOwned::to_owned 的结果,它们都会开始发出警告,帮助您发现可能意外丢弃的昂贵操作。

有关更多详细信息,请参阅详细发布说明

库稳定性改进

此版本中稳定了几个新的 API:

有关更多详细信息,请参阅详细发布说明

Cargo 特性

Cargo 在此版本中有两个小升级。首先,如果您想更改特定调用中的目标目录,它现在接受一个 --target-dir 标志

此外,Cargo 处理目标的方式进行了一项调整。Cargo 会尝试自动发现项目中的测试、示例和二进制文件。然而,有时需要显式配置。但最初的实现存在一个问题:假设您有两个示例,并且 Cargo 正在自动发现它们。您想调整其中一个,于是您在 Cargo.toml 中添加一个 [[example]] 来配置其设置。Cargo 目前会看到您已显式设置了一个,因此不再尝试对其他示例进行自动检测。这相当令人意外。

因此,我们Cargo.toml 中添加了几个 'auto' 键。在不破坏可能无意中依赖此行为的项目的情况下,我们无法修复此行为,因此,如果您想配置某些目标但不配置其他目标,可以在 [package] 部分将 autoexamples 键设置为 true

有关更多详细信息,请参阅详细发布说明

1.27.0 的贡献者

许多人共同努力创建了 Rust 1.27。没有你们,我们不可能做到。感谢大家!