Merge pull request #462 from AllanDowney/patch-1

Update: unified format
pull/467/head
Sunface 3 years ago committed by GitHub
commit 8ad5cb4c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,7 @@
# Cell 和 RefCell # Cell 和 RefCell
Rust 的编译器之严格可以说是举世无双。特别是在所有权方面Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。 Rust 的编译器之严格可以说是举世无双。特别是在所有权方面Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell``RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用) 但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell``RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)
> 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中 > 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
@ -76,7 +76,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
`RefCell` 正是**用于你确信代码是正确的,而编译器却发生了误判时**。 `RefCell` 正是**用于你确信代码是正确的,而编译器却发生了误判时**。
对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段, 主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。 对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。
总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell` 总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell`
@ -229,7 +229,7 @@ RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
#### 性能损耗 #### 性能损耗
相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。 相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针,事实上,`C++` 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。 首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针事实上C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
#### 内存损耗 #### 内存损耗
两者结合的数据结构与下面类似: 两者结合的数据结构与下面类似:
@ -252,24 +252,24 @@ struct Wrapper<T> {
#### CPU 损耗 #### CPU 损耗
从CPU来看损耗如下 从CPU来看损耗如下
- 对 `Rc<T>` 解引用是免费的(编译期),但是*带来的间接取值并不免费 - 对 `Rc<T>` 解引用是免费的(编译期),但是 `*` 带来的间接取值并不免费
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0``usize::Max` 进行一次比较,然后将计数值加 1 - 克隆 `Rc<T>` 需要将当前的引用计数跟 `0``usize::Max` 进行一次比较,然后将计数值加 1
- 释放 (drop)`Rc<T>` 需要将计数值减1 然后跟 `0` 进行一次比较 - 释放drop `Rc<T>` 需要将计数值减 1 然后跟 `0` 进行一次比较
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1然后跟 `0` 进行比较 - 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1然后跟 `0` 进行比较
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减 1 - 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减 1
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减 1 - 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减 1
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加 1 - 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加 1
其实这些细节不必过于关注,只要知道 `CPU` 消耗也非常低,甚至编译器还会对此进行进一步优化! 其实这些细节不必过于关注,只要知道 CPU 消耗也非常低,甚至编译器还会对此进行进一步优化!
#### CPU 缓存 Miss #### CPU 缓存 Miss
唯一需要担心的可能就是这种组合数据结构对于 `CPU` 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。 唯一需要担心的可能就是这种组合数据结构对于 CPU 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
总之,分析这两者组合的性能还挺复杂的,大概总结下: 总之,分析这两者组合的性能还挺复杂的,大概总结下:
- 从表面来看,它们带来的内存和 CPU 损耗都不大 - 从表面来看,它们带来的内存和 CPU 损耗都不大
- 但是由于 `Rc` 额外的引入了一次间接取值(*),在少数场景下可能会造成性能上的显著损失 - 但是由于 `Rc` 额外的引入了一次间接取值`*`,在少数场景下可能会造成性能上的显著损失
- CPU 缓存可能也不够亲和 - CPU 缓存可能也不够亲和
## 通过 `Cell::from_mut` 解决借用冲突 ## 通过 `Cell::from_mut` 解决借用冲突

@ -114,12 +114,15 @@ fn main() {
drop(gadget_owner); drop(gadget_owner);
// 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息 // 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息
// 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅 drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个引用指向底层的 owner 数据,引用计数尚未清零 // 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅
// drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个
// 引用指向底层的 owner 数据,引用计数尚未清零
// 因此 owner 数据依然可以被使用 // 因此 owner 数据依然可以被使用
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name); println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name); println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
// 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层数据也被清理释放 // 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层
// 数据也被清理释放
} }
``` ```

Loading…
Cancel
Save