假设两个可变引用不能别名,为啥 Rust 编译器不优化代码?
Posted
技术标签:
【中文标题】假设两个可变引用不能别名,为啥 Rust 编译器不优化代码?【英文标题】:Why does the Rust compiler not optimize code assuming that two mutable references cannot alias?假设两个可变引用不能别名,为什么 Rust 编译器不优化代码? 【发布时间】:2019-12-07 02:32:30 【问题描述】:据我所知,引用/指针别名会阻碍编译器生成优化代码的能力,因为它们必须确保生成的二进制文件在两个引用/指针确实别名的情况下正确运行。例如,在下面的 C 代码中,
void adds(int *a, int *b)
*a += *b;
*a += *b;
当clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
使用-O3
标志编译时,它会发出
0000000000000000 <adds>:
0: 8b 07 mov (%rdi),%eax
2: 03 06 add (%rsi),%eax
4: 89 07 mov %eax,(%rdi) # The first time
6: 03 06 add (%rsi),%eax
8: 89 07 mov %eax,(%rdi) # The second time
a: c3 retq
这里代码存储回(%rdi)
两次,以防int *a
和int *b
别名。
当我们明确告诉编译器这两个指针不能使用 restrict
关键字别名时:
void adds(int * restrict a, int * restrict b)
*a += *b;
*a += *b;
然后 Clang 会发出更优化的二进制代码:
0000000000000000 <adds>:
0: 8b 06 mov (%rsi),%eax
2: 01 c0 add %eax,%eax
4: 01 07 add %eax,(%rdi)
6: c3 retq
由于 Rust 确保(在不安全的代码中除外)两个可变引用不能别名,我认为编译器应该能够发出更优化的代码版本。
当我使用下面的代码进行测试并使用rustc 1.35.0
和-C opt-level=3 --emit obj
编译它时,
#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32)
*a += *b;
*a += *b;
它生成:
0000000000000000 <adds>:
0: 8b 07 mov (%rdi),%eax
2: 03 06 add (%rsi),%eax
4: 89 07 mov %eax,(%rdi)
6: 03 06 add (%rsi),%eax
8: 89 07 mov %eax,(%rdi)
a: c3 retq
这没有利用a
和b
不能别名的保证。
这是因为当前的 Rust 编译器仍在开发中,尚未结合别名分析来进行优化吗?
这是因为即使在安全的 Rust 中,a
和 b
仍有可能使用别名?
【问题讨论】:
godbolt.org/z/aEDINX,奇怪 旁注:“由于 Rust 确保(在不安全的代码中除外)两个可变引用不能别名”——值得一提的是,即使在unsafe
代码中,不允许使用别名可变引用并导致未定义的行为。您可以使用别名原始指针,但 unsafe
代码实际上不允许您忽略 Rust 标准规则。这只是一个常见的误解,因此值得指出。
我花了一段时间才弄清楚这个例子的意思,因为我不擅长阅读 asm,所以如果它对其他人有帮助:归结为这两个 @987654342 adds
主体中的 @ 操作可以重新解释为 *a = *a + *b + *b
。如果指针没有别名,它们可以,您甚至可以在第二个 asm 列表中看到等于 b* + *b
的内容:2: 01 c0 add %eax,%eax
。但是如果他们做别名,他们就不能,因为当您第二次添加*b
时,它将包含与第一次不同的值(您存储在第一个asm 的4:
行上的值)列表)。
【参考方案1】:
Rust 最初确实启用了 LLVM 的 noalias
属性,但是这个 caused miscompiled code。当所有受支持的 LLVM 版本不再错误编译代码时,it will be re-enabled。
如果您将-Zmutable-noalias=yes
添加到编译器选项,您将获得预期的程序集:
adds:
mov eax, dword ptr [rsi]
add eax, eax
add dword ptr [rdi], eax
ret
简单地说,Rust 放置了相当于 C 的 restrict
关键字everywhere,比任何通常的 C 程序都流行得多。这使 LLVM 的极端情况超出了它能够正确处理的范围。事实证明,C 和 C++ 程序员根本不会像在 Rust 中使用 &mut
那样频繁地使用 restrict
。
这种情况发生了多次。
Rust 1.0 到 1.7 —noalias
已启用
Rust 1.8 到 1.27 — noalias
禁用
Rust 1.28 到 1.29 — noalias
已启用
Rust 1.30 到 1.54 — noalias
禁用
Rust 1.54 到 ??? — noalias
根据编译器使用的 LLVM 版本有条件地启用
相关的 Rust 问题
当前案例
Incorrect code generation for nalgebra's Matrix::swap_rows() #54462 Re-enable noalias annotations by default once LLVM no longer miscompiles them #54878 Enable mutable noalias for LLVM >= 12 #82834 Regression: Miscompilation due to bug in "mutable noalias" logic #84958以前的案例
Workaround LLVM optimizer bug by not marking &mut pointers as noalias #31545 Mark &mut pointers as noalias once LLVM no longer miscompiles them #31681其他
make use of LLVM's scoped noalias metadata #16515 Missed optimization: references from pointers aren't treated as noalias #38941 noalias is not enough #53105 mutable noalias: re-enable permanently, only for panic=abort, or stabilize flag? #45029【讨论】:
这并不奇怪。尽管它广泛声称对多语言友好,但 LLVM 是专门设计为 C++ 后端的,它总是有一种强烈的趋势,会扼杀那些看起来不够像 C++ 的东西。 @MasonWheeler 如果您点击某些问题,您可以找到使用restrict
并在 Clang 和 GCC 上错误编译的 C 代码示例。它不仅限于“C++ 不够”的语言,除非you count C++ itself in that group。
@MasonWheeler:我不认为 LLVM 真的是围绕 C 或 C++ 的规则设计的,而是围绕 LLVM 的规则设计的。它假设通常适用于 C 或 C++ 代码,但据我所知,该设计基于静态数据依赖模型,该模型无法处理棘手的极端情况。如果它悲观地假设无法证明的数据依赖关系,那将是可以的,但它会将其视为无操作操作,这些操作将使用与它所持有的相同的位模式写入存储,并且具有潜在但不可证明的数据依赖关系读和写。
@supercat 我读过你的 cmets 几次,但我承认我很困惑——我不知道他们与这个问题或答案有什么关系。未定义的行为在这里没有发挥作用,这“只是”多个优化通道彼此交互不佳的情况。
@avl_sweden 重申一下,它是just a bug。循环展开优化步骤在执行时确实(是吗?)没有完全考虑noalias
指针。它基于输入指针创建了新指针,即使新指针做了别名,也会不正确地复制noalias
属性。以上是关于假设两个可变引用不能别名,为啥 Rust 编译器不优化代码?的主要内容,如果未能解决你的问题,请参考以下文章