原文:
Why is Rust so popular?
What is Rust and why is it so popular?
Zero-cost abstractions in Rust
Zero Cost Abstractions
Rust zero cost abstractions in action
Rust Is The Future of JavaScript Infrastructure
Hello, Rust
从前端视角认识 Rust 编程语言
三万言|2021 年 Rust 行业调研报告
image.png
思考几个问题?

1、JS是单线程的吗?为什么?
2、JS的垃圾回收机制是怎样工作的?
3、JS为什么会产生内存泄露?
4、JS的内存管理,堆和栈?
5、JS执行慢的原因有哪些?(多多益善)
6、V8对JS做了哪些优化,新版本的JS有做那些优化?
7、你Webpack编译项目慢的原因有哪些,你都做过哪些优化措施?

【1】Why Rust?

根据Survey2016201720182019,2020 StackOverflow surveys,2021 StackOverflow surveys,连续六年 Rust都当选是最受喜爱的编程语言,并且有超过80%的人想要学习并且愿意在自己的工作中使用它。

Rust不仅仅为开发者提供了性能和安全的组合,而且为初级开发者提供了快速交付代码的工具。
Rust在2015年正式发布,为了成为最被开发者喜爱的语言, 经历了一系列的修改和升级。

Rust:什么是Rust以及为什么要用它?

Rust 是静态类型的,多范式的编程语言,主要聚焦在安全和性能上。**Rust**是建立在安全、快速和高效的思想上。

它提供了零成本抽象、泛型、函数式特性,对于很多的开发者来讲,他解决了很多低级语言的问题,例如内存错误和构建并发项目。

一些案例:
1passwordNPMmozillaatlassianDropBox

Rust也经常作为系统编程语言的选择,由于它整合了同类最优的速率同时更少的资源占用,同时提供了标准服务端语言的安全性。Rust解决了与C/C++相关的问题,例如垃圾回收和安全。

Rust的收益

零成本抽象 zero-const abstractions

Rust最好的特性之一就是零成本抽象,这意味着你不必为一些你不需要的特性付出代价,所以不管你是使用抽象 还是’手动‘ 实现,速度的开销,内存的消费,以及其他 都是一样的。

在零成本抽象的特性下,编译时间内存检查,垃圾回收,Rust不会在运行时收集检查和收集内存,但是会在编译时期跟踪代码的生命周期。这意味着如果你使用了循环或者闭包都没关系,他们都会编译成同样的汇编代码。

对于很多工程师来讲,很多关于系统编程的问题都是内存问题。他们的目标是设计一个项目 — 具有高质量的代码管理、高可读性,以及运行时高性能。

为了实现这些目标,工程师们尝试限制代码的优化和内存过载,这导致间接的内存访问,会引起运行时的性能问题。Rust通过零成本抽象解决了这个问题。

Rust内置对并发的支持 (built-in support for concurrency)

并发仅仅发生在执行期间,同一个程序的多个拷贝在同时运行,这些程序的拷贝互相之间通信。

Rust内置了对多线程的支持,并且有他自己的所有权和借用规则,开发这个能够使用RUST写出更好的同步代码,这都是由于Rust在运行期间禁止数据竞争。

性能对比
image.png

Rust提供所有权和安全性 (ownership and security)

所有权(Ownership)也被认为是Rust最独特的特性。所有权(Ownership)特性可以促使Rust制造并保证内存安全,而不需要垃圾回收器。对于低级编程语言,有两种类型的内存:堆和栈。

栈是用于分配动态内存的,比如intergerstring。堆是用于将来会发生更改的内存,这意味着大多数的开发者会在堆中预先安排一些空间,并在栈中签署他们。这个方法有个问题就是两个变量会在堆中分配同样的数据。

为了解决这个问题,Rust只允许一个变量拥有一块数据。一旦数据被分配给另一个变量,它或者被移动或者被复制。

讲到防护和安全,Rust被认为是安全第一的语言之一。不像其他的语言,Rust会在编译期间分析程序的内存,在内存不足的情况下,会阻止一些bug和error的产生。这样,当然,垃圾回收对于Rust应用就没有必要了。

Rust也允许开发者自定义一些自定义配置和实现,通过添加unsafe关键字。

完善的文档

Rust这门语言,有一个陡峭的学习曲线,对初学者并不友好。尽管如此,Rust的健全的文档仍然是学好这门语言的保证。
image.png

成长的社区

Rust社区在逐渐壮大中。

什么时候该用Rust

Rust是有用的,由于他的安全特性,在编译期间可以发现一系列的bug。但是更适合那些对性能要求高和有大量数据需要处理的应用。

  • 构建嵌入式应用和区块链应用:很多的区块链应用都是基于Rust这麽语言的,这是由于他有在同一个网络里处理多个请求,并且没有或者很少的计算负载的能力。Rust可以用于构建区块链系统例如polkadot,处理多任务的区块链网络,和parity.io,以太坊的替换网络选择。

  • 网页浏览器和引擎:在开发servo浏览器引擎的时候,Rust被Mozilla的设计者们重新定义。甚至微软也说要用Rust重新设计一些对性能要求高的组件。

  • 操作系统: 一些语言被Rust完全重写,比如:redox,一种类UNIX的操作系统,FireCracker,开源可视化工具来构建安全容器和还有Serverless服务的虚拟机。

  • 网页项目:Rust也会用于开发一些基于网页的项目,例如Rocketgotham

  • 命令行:Rust也是用于开发命令行的一个选择,由于他的快速、安全和跨平台支持的特性。

【2】其他摘录

[1]什么是零成本抽象

当你开发一个项目的时候,随着项目变的越来越复杂,你通常会在你的代码里引入一些抽象来使得你的代码更加容易维护和自定义,以便添加更多的特性。在这种情况下,你最后的愿望是这些抽象会在运行时惩罚你项目的性能。

所以,作为一个开发者,你想要两件事情:

  • 设计你的项目,代码的可读性和可维护性比复杂的手写优化更重要。


  • 仍然在运行时受益于优秀的软件性能

实际上,提升代码的可读性意味着限制代码的优化和内存的负担,紧接着是间接的内存访问,这会造成运行时的性能问题。

零成本抽象的概念来自于C++,是被Bjarne Stroustrup(C++的建立者)定义的.

总之,C++的实现遵循了零负担原则: 当你使用不到,你不会为此付出代价;更进一步,你用到的,你不会写的更好。

这意味着,很多被编程语言提供的抽象不能在运行时增加额外的开销。

在Rust中, 零成本抽象的核心原则来自于:

  • 编译期间内存检查和静态的垃圾回收,Rust不会在运行时检查和收集内存,但会跟踪你代码实体的生命周期,用来替换引用计数和垃圾回收。
  • traits ,Rust中最令人印象深刻的特性用来扩展类型。
  • generics,泛型
  • iterators,迭代器
  • 其他

[2]零成本抽象

零成本抽象这个观点是非常重要的这对于一些编程语言来讲,例如Rust和C++,可以帮助用户写出更优秀、性能更高的项目,同时在相对较少的努力下。

这个观点被C++的开发者Bjarne Stroustrup总结如下:

当你使用不到,你不会为此付出代价;更进一步,你用到的,你不会写的更好。

在这个定义下,有两个因素是使零成本抽象正确应用的关键:

  • 没有全局的开销。零成本抽象不应该对不使用它的程序的性能产生负面影响。例如它不能要求每一个程序都有一个的繁重的语言运行时,以使他唯一使用这个特性的程序收益。

  • 最佳性能。一个零成本抽象应该编译成解决方案的最佳实现,而这个实现应该是某人用低级语言写的。他不应该引入额外的开销,而这些可以在没有抽象的情况下避免。

另外还有第三个必要条件对于实现零成本抽象是非常重要的。他经常被忽略,这是因为它仅仅是所有优秀抽象的必要条件,无论是不是零成本。

  • 提升用户体验 抽象的点在于提供一个新的工具,通过底层组件编译,这使用户能够更轻松的写他们想写的程序。一个零成本抽象,像所有的抽象,相比其他的方案必须提供一个更好的体验。

列举下Rust中真正优秀的零成本抽象:

  • Ownership and borrowing.(所有权和借用)。 毫无疑问这是最大的一个,保证了内存和线程安全,在没有垃圾回收的前提下,是Rust最原创、最成功的的故事。

  • Iterator and closure APIs。(迭代器和闭包的API)。这是另外一个经典。有很多的案例,内部的迭代可能被优化的很好,在切片上写map,filter,循环遍历,等等,能被优化的和手写的C一样 是非常令人震惊的。

  • Async/await and Futures. FuturesAPI 是一个非常重要的例子,最早版本的futures实现零成本抽象中的‘零成本’实现的很好但是没有提供足够好的用户体验来驱动采用。通过添加对async/await的支持,跨awaits的引用,以及等等,我们已经做了一个产品,我觉得能解决用户的问题,使得Rust适合编写高性能的网络服务。

  • Unsafe and the module boundary。(**unsafe**和模块边界)。在所有的这些下面,每一个Rust成功的故事背后,是unsafe区块和隐私的概念,允许我们深入原始的指针操作来构建零成本抽象。 None of Rust’s brilliant features would be possible without this really fundamental ability to break the rules locally to extend the system beyond what the typechecker can handle.

[3]理解Rust 中的 ownership

Rust保证内存安全的特性叫做ownershipownership和垃圾回收器工作方式是不一样的,他仅仅是一组规则,需要编译器在编译时间检查。如果没有遵循ownership的规则那么编译就不会通过。有一个borrow checker可以确保你的代码遵循ownership

对于没有垃圾回收的语言,你需要精确的回收和整理内存空间。当你的代码量变的庞大后,这会是一个有挑战性的任务。

Rust编译器使用ownership模型来进行内存管理。一个Rust编译器会自动插入drop语句来释放内存。Rust使用ownership模型来决定在哪里释放内存,当owner超出了作用域,内存就开始释放。

  1. fn main(){
  2. {
  3. let x =5;
  4. // x is dropped here since it goes out of the scope
  5. }
  6. }

什么是堆和栈

栈中的数据是后进先出,按照顺序存储数据,但是删除的时候按照相反的方向。还有很重要的一点是,在编译期间,所有存储在栈中的数据都有确定的大小。

堆中的数据是无序的,当你分配一块空间的时候,一般是大空间,所以我们并不知道存储在堆空间中值确切的大小。

由于存储在栈中的数据总是有序的,而且大小已知的。当你取值的时候,只需要获取最上面的值即可,效率高。在堆中存储值,首先你要在内存中查询是否有足够大的内存空间可以存储你的请求,然后返回要存储在栈中的内存地址。从堆中取值需要用一个指针来检索。无论是检索还是取值,都是非常消耗性能的。

ownership 规则

ownership有三个基本的规则可以预测 ,内存是如何存储在堆上还是栈上。

  1. 每一个Rust值都有一个变量叫 ‘owner’。

    1. let x = 5; // x is the owner of the value "5"
  2. 每一个值只会拥有 ‘owner’ 一次。

  3. **owner**超出边界,他就会被删除。
    1. fn main() {
    2. {// scope begins
    3. let s = String::from("hello"); // s comes into scope
    4. }// the value of s is dropped at this point, it is out of scope
    5. }

    ownership是怎么工作

ownership中,开发者自己请求内存,当owner超出作用域,value会被删除,并且释放内存。

  1. <// memory allocation in the stack
  2. fn main() {
  3. { // a is not valid here
  4. let a = 5; // a is valid here
  5. // do stuff with a
  6. }// println!("{}", a)a is no longer valid at this point, it is out of scope
  7. }

上面的例子,展示了变量(a)在栈上分配了空间,并且值是5。然而并不总是这样的,有时候,你需要为增长的值分配内存空间,在编译期间你并不知道它的大小。

下面的例子,必须在堆上分配内存空间。

  1. fn main() {
  2. {
  3. let mut s = String::from("hello"); // s is valid from this point forward
  4. s.push_str(", world!"); // push_str() appends a literal to a String
  5. println!("{}", s); // This will print `hello, world!`
  6. }
  7. // s is no longer valid here
  8. }

由于s是可更改的,我们可以在后面增加更多的字符串,这使得他在编译期间,很难知道内存的大小。所以我们需要在程序中请求string大小的内存空间。

  1. let mut s = String::from("hello") // requesting for space in the heap, the size of a String.

当变量超出作用域,Rust的ownership特性会释放内存。

克隆和拷贝 Clone and copy

我们看下 Rust中ownership是如何影响一些特性的,从clonecopy开始。

  1. fn main() {
  2. let a = "5";
  3. let b = a; // copy the value a into b
  4. println!("{}", a) // 5
  5. println!("{}", b) // 5
  6. }

上面的例子,由于a是存在与栈上的,所以是很轻松的复制另外一个拷贝b

  1. fn main() {
  2. let a = String::from("hello");
  3. let b = a; // copy the value a into b
  4. println!("{}", a)
  5. // This will throw an error because a has been moved or ownership has been transferred
  6. println!("{}", b) // hello
  7. }

当运行上面的命令时,你会得到一个error[E0382]: borrow of moved value: "a",在Rust中,move的含义是指内存的ownership已经被转移到另外的owner
image.png
当你拷贝一个在堆中的值时,系统会自动的拷贝指针,而不会管他的堆中数据。由于数据只有一个owner,字符串的ownershipa转给了b
image.png
这会使得Rust在渲染a的时候不再有效,就会得到一个double free error的错误。

要同时访问ab,需要用下面的方法。

  1. fn main() {
  2. let a = String::from("hello");
  3. let b = a.clone(); // creates a copy of data on the heap and return pointer to it
  4. println!("a = {}, b = {}", a, b);// a=hello, b=hello
  5. }

Ownership和函数

给函数传值也是遵循ownership规则的,这意味着他们一次只能有一个owner,而且超出作用域时释放一次内存。

  1. fn main() {
  2. let s1 = givesOwnership(); // givesOwnership moves its return
  3. // value into s1
  4. let s2 = String::from("hello"); // s2 comes into scope
  5. let s3 = takesAndGivesBack(s2); // s2 is moved into
  6. // takesAndGivesBack, which also
  7. // moves its return value into s3
  8. } // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  9. // moved, so nothing happens. s1 goes out of scope and is dropped.
  10. fn givesOwnership() -> String { // givesOwnership will move its
  11. // return value into the function
  12. // that calls it
  13. let someString = String::from("hello"); // someString comes into scope
  14. someString // someString is returned and
  15. // moves out to the calling
  16. // function
  17. }
  18. // takesAndGivesBack will take a String and return one
  19. fn takesAndGivesBack(aString: String) -> String { // aString comes into
  20. // scope
  21. aString // aString is returned and moves out to the calling function
  22. }

引用和借用(References and borrowing)

  1. fn main() {
  2. let s1 = &givesOwnership(); // moves its return value into s1 that borrows the value
  3. let s2 = String::from("hello"); // s2 comes into scope
  4. let s3 = takesAndGivesBack(s2); // s2 is moved into s3
  5. println!("{}", s1);
  6. println!("{}", s3); // takesAndGivesBack, which moves its return value into s3
  7. } // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  8. // moved, so nothing happens. s1 goes out of scope and is dropped.
  9. fn givesOwnership() -> String { // givesOwnership will transfer ownership of its
  10. // return value to the caller of the function
  11. let someString = String::from("hello"); // someString comes into scope
  12. someString // someString is returned and
  13. // moves out to the calling
  14. // function
  15. }
  16. // takesAndGivesBack will take a String and return one
  17. fn takesAndGivesBack(aString: String) -> String { // aString comes
  18. // scope
  19. aString // aString is returned and moves out to the calling function
  20. }

切片 Slice

  1. fn main() {
  2. let s = String::from("Nigerian");
  3. // &str type
  4. let a = &s[0..4]; // doesn't transfer ownership, but references/borrow the first four letters.
  5. let b = &s[4..8]; // doesn't transfer ownership, but references/borrow the last four letters.
  6. println!("{}", a); // prints Nige
  7. println!("{}", b); // prints rian
  8. let v=vec![1,2,3,4,5,6,7,8];
  9. // &[T] type
  10. let a = &v[0..4]; // doesn't transfer ownership, but references/borrow the first four element.
  11. let b = &v[4..8]; // doesn't transfer ownership, but references/borrow the last four element.
  12. println!("{:?}", a); // prints [1, 2, 3, 4]
  13. println!("{:?}", b); // prints [5, 6, 7, 8]
  14. }

[4]数据竞赛

Fearless Concurrency with Rust

【3】Rust与前端

通常,使用Rust(以及其他语言除了JS)开发前端,你需要使用WebAssembly

Rust的关键优势是直接编译成机器码,所以没有虚拟机/运行时,可以直接运行在设备上。

image.png
根据Node vs Node的对比,基于Rust 的REST API的性能对比,Rust在每秒处理的请求是72,000,而基于Nodejs每秒处理请求数是8,000,而且Rust只用了1m的内存,Node用了19m。

Rust处理请求数量平均来看大概是Node的100倍。

Rust是前端基建的未来

一些前端基建工具也被Rust重写,以及一些webpack的替换工具,比如DenoSWCRome