据我所知,引用/指针别名会阻碍编译器生成优化代码的能力,因为它们必须确保生成的二进制文件在两个引用/指针确实别名的情况下正确运行。例如,在下面的 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 # load a into EAX
2: 03 06 add (%rsi),%eax # load-and-add b
4: 89 07 mov %eax,(%rdi) # store into a
6: 03 06 add (%rsi),%eax # load-and-add b again
8: 89 07 mov %eax,(%rdi) # store into a again
a: c3 retq
此处代码存储回 (%rdi)
两次,以防 int *a
和 int *b
别名。
当我们明确告诉编译器这两个指针不能使用 restrict
关键字别名时:
void adds(int * restrict a, int * restrict b) {
*a += *b;
*a += *b;
}
然后 Clang 将发出一个更优化的版本,有效地执行 *a += 2 * (*b)
,如果(如 restrict
所承诺的)*b
没有通过分配给 *a
来修改,则等效:
0000000000000000 <adds>:
0: 8b 06 mov (%rsi),%eax # load b once
2: 01 c0 add %eax,%eax # double it
4: 01 07 add %eax,(%rdi) # *a += 2 * (*b)
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
仍有可能出现别名?
unsafe
代码中,也不允许对可变引用进行别名并导致在未定义的行为中。您可以使用别名原始指针,但 unsafe
代码实际上不允许您忽略 Rust 标准规则。这只是一个常见的误解,因此值得指出。
+=
操作是否} 可以重新解释为 *a = *a + *b + *b
。如果指针没有别名,它们可以,您甚至可以在第二个 asm 列表中看到相当于 b* + *b
的内容:2: 01 c0 add %eax,%eax
。但是如果他们做别名,他们就不能,因为当您第二次添加 *b
时,它将包含与第一次不同的值(您存储在第一个 asm 的第 4:
行的值)清单)。
*a += 2 * (*b)
等效项。
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++ 程序员使用 restrict
的频率不如 Rust 中使用的 &mut
频繁。
这已经发生了多次。
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 问题
当前情况 nalgebra 的 Matrix::swap_rows() 的代码生成不正确 #54462 一旦 LLVM 不再错误编译它们,默认情况下重新启用 noalias 注释 #54878 Enable mutable noalias for LLVM >= 12 #82834 Regression: Miscompilation due to bug in "mutable noalias " 逻辑 #84958
nalgebra 的 Matrix::swap_rows() 的代码生成不正确 #54462
一旦 LLVM 不再错误编译它们,默认情况下重新启用 noalias 注释 #54878
为 LLVM >= 12 启用可变 noalias #82834
回归:由于“可变 noalias”逻辑中的错误导致的错误编译 #84958
以前的案例解决 LLVM 优化器错误,不将 &mut 指针标记为 noalias #31545 一旦 LLVM 不再错误编译它们,将 &mut 指针标记为 noalias #31681
通过不将 &mut 指针标记为 noalias 来解决 LLVM 优化器错误 #31545
一旦 LLVM 不再错误编译它们,将 &mut 指针标记为 noalias #31681
其他使用 LLVM 的范围 noalias 元数据 #16515 错过优化:来自指针的引用不被视为 noalias #38941 noalias 不够 #53105 mutable noalias:永久重新启用,仅用于恐慌=中止或稳定标志? #45029
利用 LLVM 的范围 noalias 元数据 #16515
错过的优化:来自指针的引用不被视为 noalias #38941
noalias 不够 #53105
mutable noalias:永久重新启用,仅用于恐慌=中止或稳定标志? #45029
不定期副业成功案例分享
restrict
并在 Clang 和 GCC 上错误编译的 C 代码示例。它不仅限于“C++ 不够”的语言,除非you count C++ itself in that group。noalias
指针。它基于输入指针创建了新指针,即使新指针做了别名,也会不正确地复制noalias
属性。