16

Rust中的零成本抽象(二)(部分翻译)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/109189186
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rust中的零成本抽象(二)(部分翻译)

时间有限,翻译了关键的部分

大意是,Rust中的抽象的可读性高的代码和手动优化代码在调试时是有区别的,但是在发布时没有区别。当然手动优化的编译更快。

https://carette.xyz/posts/zero_cost_abstraction/​carette.xyz

Zero-cost abstractions in Rust

Zero-cost abstractions in Rust​carette.xyz

上一周, Ibrahim Dursun发布了一篇关于Rust零成本抽象的文章。
不幸的是,除了文章的一小部分, 这篇文章并没有正确地体现(我的拙见)什么是零抽象。

确实,零成本抽象(zero-cost abstraction)或者零开销原则(zero-overhead),是很难理解,并与编译优化分离,这导致它很容易被误解。

在这篇文章中,我会讨论它,并且给出Rust如何对抽象过的项目进行优化的例子。

注意:去年关于零成本抽象已经有篇好文(零成本抽象,作者:withoutboats),本文作为它的补充。

什么是零成本抽象

当你编程的时候,项目越来越复杂,通常你会增加抽象来让项目容易维护并增加功能。你肯定希望你的抽象不要在运行时增加成本。

所以,你的2个原则是:

  • 项目的可读性和易管理性比复杂的手写优化更重要
  • 但至少能在运行时发挥非常良好的性能

确实,改进代码可读性会限制优化和内存开销,并间接迟滞运行时的性能。

零成本抽象来自C++,由C++创始人Bjarne Stroustrup定义:

通常,C++的实现遵循零开销原则:你不使用的,你不负担成本。更进一步:你使用的,你也没法更优化。

总结他说的:

  1. 你不使用的功能,你不承担额外开销
  2. 高层的抽象会被编译成机器代码,这些代码很难更进一步优化

Rust最主要的抽象(或者说标准库)不会增加运行时成本。

在Rust中,零成本抽象是一种核心原则,尤其对下面的这些特性:

  • 编译时内存检查和静态垃圾回收(借用和所有权) - 作为提醒, Rust 在运行时不通过引用计数检查和回收内存,而是在编译时通过生命周期检查
  • traits, 是非常让人印象深刻的功能,来拓展你的类型
  • generics(泛型),
  • iterators(迭代器),
  • etc(其它).

This can explain why Diesel, a famous Rust ORM, was 30% faster than the raw postgres solution for Rust, using more abstractions.

我们用两个例子来解释Rust中的零成本抽象。

从1开始、小于自然数n的,所有的奇数的和。

两个版本的代码

第一个是通常的抽象版本

第二个是手写优化版本

fn sum_odd_numbers(n: u64) -> u64 {
    let mut acc = 0;
    for element in 0.. {
        if element >= n {
            break;
        }
        if element.is_odd() {
            acc += element;
        }
    }
    acc
}
fn sum_odd_numbers(n: u64) -> u64 {
    (0..)
        .take_while(|element| element < &n)
        .filter(|n| n.is_odd())
        .fold(0, |sum, element| sum + element)
}

下面一段解释了第二个代码使用了函数式编程,都干了什么:

The second code sample borrows functional programming concepts to compute the sum, and is seems more “compact” (and easier to understand) than the first one.
However, if we decompose this second sample, we can make wrong assumptions here:

  1. create an iterator that begins with a zero value,
  2. take all elements lower than n, a number passed as parameter by the user - as a reminder, if we explicit the final of the iterator, the compiler could decide to compute the final number at compile time, and store it in the executable -, and store the result in another array,
  3. loop all over the last array of elements to get only the odds, and store the result in another array,
  4. loop over the odds array to compute the sum, and return finaly return the value.

So, multiple array allocations just to compute a simple sum…

Fortunately, Rust does not do that: instead, the compiler writes as great code as the first solution provided here, by itself!

Let’s demonstrate it using benchmarks.

性能对比(Benchmarks)

以下的性能对比使用Intel Core i5 (3 GHz, 6 cores * 2 threads / core), 对于手写优化和抽象版使用不同的n都运行了10次,然后取中间数。

I used the time program to measure the user + system execution times each time, and computed the median of those values.

包括编译和发布运行时间对比。

发布是使用cargo build --release的版本:

cargo build --release # in the root of your cargo project

代码库见 https://github.com/k0pernicus/Rust_zero_cost_abstraction_illustration

性能对比

抽象版本在发布时被大大优化了,以至于和手写优化没有区别。(译者注:标准库中的功能可能大多数如此)

Unfortunately, zero-cost abstractions has a consequence: as the compiler has more work to do to optimize itself the abstracted code, it will run more longer.

If you take a look at the compile time of each versions below, you can observe that the abstracted version compiles more slower than the hand-written version, even for a simple program like this…

当你需要抽象的时候,不要犹豫。在Rust中,你永远不需要首先进行手写优化,而应该考虑抽象性、架构和设计、代码可读性。即使你十分在意运行时性能。
当然,手写优化的版本编译起来要快很多(即使这么简单的程序都能差这么多)。

Thanks to Robert Syme for his contribution.

Rust中哪些特性是零成本的,见下文(另外一人写的):


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK