Rust Stable 1.45 中的 神奇 Bug 解读

原创
2020/07/26 17:43
阅读数 95
 

点击上方蓝字关注我们

理清头脑混沌,觉醒心智天地


今天,Rust 官方仓库里报告了一个神奇的Bug,该 Bug 似乎动摇了 Rust 的世界法则,让我们一探究竟到底是否如此。
Bug 描述


今天,Rust 官方仓库里报告了一个神奇的Bug,该 Issues 编号为:

#74739
https://github.com/rust-lang/rust/issues/74739

目前该 Bug 存在于Rust Stable 1.44 和 1.45 版本中。下周即将发布的1.45.1 会修复此 Bug。该 Bug 在 Nightly 下已被修复。

该 Bug 的表现如下:

struct Foo {    x: i32,}
fn main() { let mut foo = Foo { x: 42 }; let x = &mut foo.x; *x = 13; let y = foo; println!("{}", y.x); // -> 42; expected result: 13}
正常情况下,最后的 y.x  应该输出 “13”,但是现在还是 “42”。这个结果意味着,代码第 7行的可变引用并没有起到作用。
是不是很神奇?这个 Bug 让人感觉 Rust 世界的基本法则都崩塌了。
所以,这也激发了我的好奇心,就想一探究竟这个 Bug 到底是什么原因导致的,它到底是不是 Rust 世界法则的崩塌呢?
探索


该 Bug 既然在 Nightly 中已经被修复,那么可以先观察一下在 Stable 和 Nightly 中生成的 MIR (中级中间语言)有什么不同。
在 Playground 上选择 Stable 生成 MIR 查看如下:
图中右侧为输出的 MIR,红框里是关键的几个变量: "_1" 是变量 foo,"_2" 是变量 x,"_3" 是变量 y。
目测,最后 y 的值,被直接赋予了最初 "_1" 被赋予的 常量结构体实例:
_1 = const Foo { x: 42i32 };...(*_2) = const 13i32;  ..._3 = const Foo { x: 42i32 };

接下来再看看 Nightly 下的 MIR 输出:

看的出来,和 stable 下生成的 MIR 不同的地方就在于:
_1 = const Foo { x: 42i32 };...(*_2) = const 13i32;  ..._3 = move _1;
其实,字段 x 的值都被修改过了,只是在 Nightly 下,是直接把修改后 foo 移动给了 y。而在 Stable 下,则是把原始的那个常量结构体实例,又拷贝了过去。
所以,问题就来了,难道是因为Stable下不识别 Move ? 那继续实验一下

将代码修改如下:

#[derive(Copy, Clone)]struct Foo {    x: i32,}
fn main() { let mut foo = Foo { x: 42 }; let x = &mut foo.x; *x = 13; let y = foo; println!("{}", y.x); // -> 42; expected result: 13}
主要是给结构体 Foo 实现了 Copy trait,这样 Foo 的结构体实例就不会被 Move 了。我们依次查看一下修改后的代码输出的 MIR 差异。

在 Stable 下,输出依然不变。

但是在 Nightly 下变了,并没有发生 Move。 

这个结果虽然符合我们的预期,但是在 Stable 下还是存在那个 Bug,所以,该 Bug 的原因跟结构体是否可以 Move 没有关系。

那么到底是什么原因导致的呢?

Bug 产生根源


再次回头审视两次 Stable 下输出的 MIR ,发现 Bug 的直接原因就是因为 y 在赋值的时候,本来应该是:

_1 = const Foo { x: 42i32 };...(*_2) = const 13i32;  ..._3 = move _1;// 或者_3 = _1;
结果被改为了:
_1 = const Foo { x: 42i32 };...(*_2) = const 13i32;  ..._3 = const Foo { x: 42i32 };
感觉这个常量实例是被编译器给强制分发到了需要 "_1" 的位置。
为什么会这样呢?我再次打开了 #74739 Issues,看到下面有两个已经被合并的相关Issues 和 PR 。

解释一下:

上面的 Issues 是说,在 #71911 的 PR(该 PR 是对 Const Prop 的重构和整理)中引入了一个问题,导致了 Miri 中的一个测试中的断言失效了。该断言正好是为了测试  "常量传播(const propagator )“ 功能对引用的正确跟踪。

所以上面那个 PR,就是为了修复这个问题。

看来,这个神奇的 Bug 就是和常量传播这个功能有直接关系的。

然而,在修复这个问题的过程中,正好错过了 Beta 测试的截止期,于是,Bug就被引入到了 Stable 1.44 和 1.45 中。

到此为止,我们终于搞清楚了这个 Bug 的根源。看来,这个 Bug 其实并不是我前面所担心的 Rust 世界法则的崩塌,是我多虑了。

话说回来,这个常量传播到底是做什么的呢?

常量传播


常量传播(Const Propagator)实际上一种编译器优化技术。常量传播的目的在于充分利用代码中存在的常量,将变量的使用替换为对常量的引用,并尽可能地去计算常量表达式

比如上面的 Stable 输出的 MIR:

_1 = const Foo { x: 42i32 };...(*_2) = const 13i32;  ..._3 = const Foo { x: 42i32 };

只要是使用了 "_1" 则到处被替换为了结构体实例常量,这就是一种常量传播。只不过这里常量并没有传播到正确的位置,从而导致了这个 Bug。

Rust 官方在 2019年12月初公布了编译器引入了常量传播技术,用来优化 MIR 。相关链接:https://blog.rust-lang.org/inside-rust/2019/12/02/const-prop-on-by-default.html

目前,Rust 编译器能做到的程度如下:

1. 编译器可以自动识别代码中的常量,即便你没有显式声明。比如下面代码所示:

struct Point {  x: u32,  y: u32,}
let a = 2 + 2; // optimizes to 4let b = [0, 1, 2, 3, 4, 5][3]; // optimizes to 3let c = (Point { x: 21, y: 42 }).y; // optimizes to 42

上面的结构体 Point 在实例化的时候赋值为数字字面量,这个可以作为常量,那么编译器就会自动进行优化。

2. 消除控制流。比如下面代码所示:

const Foo: Option<u8> = Some(12);
let x = match Foo { None => panic!("no value"), Some(v) => v,};// 优化为const Foo: Option<u8> = Some(12);let x = 12;

这个非常有用。再比如,进行科学计算的时候:

let x = 2 + 4 * 6;

这行代码,实际上会生成很多检查:

let (_tmp0, overflowed) = CheckedMultiply(4, 6);assert!(!overflowed, "attempt to multiply with overflow");
let (_tmp1, overflowed) = CheckedAdd(_tmp0, 2);assert!(!overflowed, "attempt to add with overflow");
let x = _temp1;

这就增加了很多控制流。常量传播可以先将其简化为:

let _tmp0 = 24;assert!(!false, "attempt to multiply with overflow");
let _tmp1 = 26;assert!(!false, "attempt to add with overflow");
let x = 26;

再进一步简化为:

let x = 26;

减少Rust编译器处理的控制流数量会极大减少编译时间。 官方在 Debug 和 Release 模式下的各种测试用例的改进达到了 2-10%。 实例化的泛型函数的具体实例越多,这种优化的收益就越大。

总的来说,常量传播对于优化 MIR 有很积极的作用。

Bug 影响范围


有人担心,这个 Bug 会影响到很多真实世界的 Rust 代码。但是经过我们上面的分析,其实真实世界几乎不会触发这个 Bug 。因为触发这个 Bug,需要将值都设为常量,并且在它们之间不存在任何控制流或函数调用才行。

但是,还需要在下周 1.45.1 Stable 发布以后尽快升级。

小结


1. 该 Bug 看似违反了 Rust 的世界法则,其实不然。值是已经改变了,只不过因为常量传播而导致了问题。

2. 该 Bug 之所以能引入到 Stable,是因为测试用例失效,在修改这个问题的过程中,错过了 Beta 的截止日期,导致了 Bug 流入了 Stable。我们都知道, Rust的稳定流程是:Nightly -> Beta -> Stable。

3. 常量传播是为了进一步优化 MIR,可以对降低编译时间产生积极影响。

4. Rust 自身也是软件,产生 Bug 很正常。有人的地方就有 Bug,但是对于这种和 Rust 世界法则有悖的 Bug,还是需要刨根问底。


全文完,感谢阅读。

































本文分享自微信公众号 - 觉学社(WakerGroup)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
mir
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部